Forráskód Böngészése

feat(store): 实现商铺装修营业时间、入口图和头图功能

- 开发营业时间设置页面,支持正常营业时间和特殊营业时间的增删改查- 实现入口图上传功能,包含图片尺寸和比例校验逻辑- 完成头图管理功能,支持单图和多图两种模式切换与上传
- 更新商铺经营板块接口路径,修正API调用地址
- 添加登录调试日志,便于开发阶段查看登录响应数据
spy 1 hónapja
szülő
commit
eb43f6af27

+ 1 - 1
src/api/modules/storeDecoration.ts

@@ -41,7 +41,7 @@ export const getStoreInfoList = (params: StoreUser.ReqUserParams) => {
 
 //获取商铺经营板块列表
 export const getBusinessSection = () => {
-  return http.get(PORT_NONE + `/store/info/getBusinessSection`);
+  return http.get(`/storePlatformRenovation/getBusinessSection`);
 };
 
 //获取商铺经营种类列表

BIN
src/assets/login/bg.jpg


BIN
src/assets/login/logo.png


BIN
src/assets/login/ud.png


BIN
src/assets/login/udzzApp.png


+ 12 - 11
src/views/login/index.vue

@@ -3,7 +3,7 @@
     <div class="left-box">
       <div class="left-box-title">
         <div style="display: flex; align-items: center">
-          <img src="@/assets/login/logo.png" style=" width: 49px;height: 44px" />
+          <img src="@/assets/login/logo.png" style="width: 49px; height: 44px" />
           <span class="left-box-title-text">U店商家端</span>
         </div>
         <div class="left-box-title-bottom-text">登录U店在这,轻松管店、快速核销</div>
@@ -11,14 +11,14 @@
       <img src="@/assets/login/login.png" class="login-class" />
       <div class="qrcode-box">
         <img src="@/assets/login/udzzApp.png" />
-        <div style=" width: 124px;height: 124px; background: black" />
+        <div style="width: 124px; height: 124px; background: black" />
         <span class="qrcode-box-text">扫码下载</span>
       </div>
     </div>
     <div class="right-box">
       <div class="right-box-title">
         欢迎登录
-        <img src="@/assets/login/ud.png" style=" width: 74px;height: 74px" />
+        <img src="@/assets/login/ud.png" style="width: 74px; height: 74px" />
         在这
       </div>
 
@@ -99,7 +99,7 @@
           <p style="display: flex; align-items: center; justify-content: center">“U 店在这” 商家端用户协议</p>
         </div>
       </template>
-      <div style=" height: 50vh; padding: 10px; overflow-y: auto; font-size: 14px;line-height: 1.6">
+      <div style="height: 50vh; padding: 10px; overflow-y: auto; font-size: 14px; line-height: 1.6">
         <p>
           您在使用爱丽恩严(大连)商务科技有限公司旗下U店在这APP软件提供的服务前,应当仔细认真阅读本《服务条款》(下称"本条款")中的全部规则、《用户协议》及发布的其他服务条款、专项产品或服务规则或规范的内容,尤其是以粗体或加下划线标示的条款,包括但不限于免除或者限爱丽恩严(大连)商务科技有限公司责任的条款、对用户权利进行限制的条款以及约定争议解决方式、司法管辖的条款,上述条款请您重点阅读。您有权选择同意或者不同意本协议。
         </p>
@@ -373,10 +373,10 @@
         </ul>
       </div>
       <template #footer>
-        <div style=" display: flex; align-items: center; justify-content: center;width: 100%">
+        <div style="display: flex; align-items: center; justify-content: center; width: 100%">
           <el-button
             @click="userShow = false"
-            style=" width: 406px; height: 60px; color: #ffffff;background-color: #6c8ff8; border-radius: 10px"
+            style="width: 406px; height: 60px; color: #ffffff; background-color: #6c8ff8; border-radius: 10px"
           >
             已阅读并同意协议内容
           </el-button>
@@ -389,7 +389,7 @@
           <p style="display: flex; align-items: center; justify-content: center">《[U店在这] 隐私协议》</p>
         </div>
       </template>
-      <div style=" height: 50vh; padding: 10px; overflow-y: auto; font-size: 14px;line-height: 1.6">
+      <div style="height: 50vh; padding: 10px; overflow-y: auto; font-size: 14px; line-height: 1.6">
         <p />
         <p>生效日期:2024 年 10 月 29 日</p>
         <p>
@@ -460,10 +460,10 @@
         <p>感谢您对 [U店在这]的信任和支持!</p>
       </div>
       <template #footer>
-        <div style=" display: flex; align-items: center; justify-content: center;width: 100%">
+        <div style="display: flex; align-items: center; justify-content: center; width: 100%">
           <el-button
             @click="privacyShow = false"
-            style=" width: 406px; height: 60px; color: #ffffff;background-color: #6c8ff8; border-radius: 10px"
+            style="width: 406px; height: 60px; color: #ffffff; background-color: #6c8ff8; border-radius: 10px"
           >
             已阅读并同意协议内容
           </el-button>
@@ -513,7 +513,7 @@
           <el-button
             :loading="loading"
             @click="handleForgotPasswordConfirm"
-            style=" width: 406px; height: 60px; color: #ffffff;background-color: #6c8ff8; border-radius: 10px"
+            style="width: 406px; height: 60px; color: #ffffff; background-color: #6c8ff8; border-radius: 10px"
           >
             确认
           </el-button>
@@ -568,7 +568,7 @@
         <div style="display: flex; align-items: center; justify-content: center">
           <el-button
             @click="handleRegister"
-            style=" width: 406px; height: 60px; color: #ffffff;background-color: #6c8ff8; border-radius: 10px"
+            style="width: 406px; height: 60px; color: #ffffff; background-color: #6c8ff8; border-radius: 10px"
           >
             注册
           </el-button>
@@ -873,6 +873,7 @@ const handleLogin = async () => {
       const res: any = await loginAccount(formData);
 
       if (res.data) {
+        console.log(res.data, "res.data");
         userStore.setToken(res.data.token);
         await initDynamicRouter();
 

+ 498 - 4
src/views/storeDecoration/businessHours/index.vue

@@ -1,7 +1,501 @@
-<script setup lang="ts"></script>
-
 <template>
-  <div>1</div>
+  <div class="business-hours-container">
+    <h2 class="page-title">营业时间</h2>
+
+    <!-- 正常营业时间 -->
+    <div class="section">
+      <h3 class="section-title">正常营业时间</h3>
+      <div class="hours-list">
+        <div v-for="(item, index) in normalHours" :key="index" class="hours-item">
+          <span class="hours-text">{{ formatNormalHours(item) }}</span>
+          <div class="action-buttons">
+            <el-button type="primary" link @click="editNormalHours(index)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteNormalHours(index)"> 删除 </el-button>
+          </div>
+        </div>
+      </div>
+      <el-button type="primary" @click="openAddNormalDialog"> 添加正常营业时间 </el-button>
+    </div>
+
+    <!-- 特殊营业时间 -->
+    <div class="section">
+      <h3 class="section-title">特殊营业时间</h3>
+      <div class="hours-list">
+        <div v-for="(item, index) in specialHours" :key="index" class="hours-item">
+          <span class="hours-text">{{ formatSpecialHours(item) }}</span>
+          <div class="action-buttons">
+            <el-button type="primary" link @click="editSpecialHours(index)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteSpecialHours(index)"> 删除 </el-button>
+          </div>
+        </div>
+      </div>
+      <el-button type="primary" @click="openAddSpecialDialog"> 添加特殊营业时间 </el-button>
+    </div>
+
+    <!-- 保存按钮 -->
+    <div class="footer">
+      <el-button type="primary" size="large" @click="handleSave"> 保存 </el-button>
+    </div>
+
+    <!-- 正常营业时间对话框 -->
+    <el-dialog v-model="normalDialogVisible" :title="normalDialogTitle" width="600px" @close="resetNormalForm">
+      <el-form :model="normalForm" label-width="140px">
+        <el-form-item label="请选择营业日">
+          <div class="day-buttons">
+            <el-button
+              v-for="day in weekDays"
+              :key="day.value"
+              :type="normalForm.selectedDays.includes(day.value) ? 'primary' : 'default'"
+              @click="toggleDay(day.value)"
+            >
+              {{ day.label }}
+            </el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="请选择营业时间段">
+          <el-radio-group v-model="normalForm.timeType">
+            <el-radio :value="'custom'"> 自定义 </el-radio>
+            <el-radio :value="'24hours'"> 24小时 </el-radio>
+          </el-radio-group>
+          <div v-if="normalForm.timeType === 'custom'" class="time-picker-group">
+            <el-time-picker
+              v-model="normalForm.startTime"
+              format="HH:mm"
+              value-format="HH:mm"
+              placeholder="开始时间"
+              style="width: 150px"
+            />
+            <span class="time-separator">至</span>
+            <el-time-picker
+              v-model="normalForm.endTime"
+              format="HH:mm"
+              value-format="HH:mm"
+              placeholder="结束时间"
+              style="width: 150px"
+            />
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="resetNormalForm"> 重置 </el-button>
+          <el-button type="primary" @click="confirmNormalHours"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 特殊营业时间对话框 -->
+    <el-dialog v-model="specialDialogVisible" title="添加特殊营业时间" width="600px" @close="resetSpecialForm">
+      <el-form :model="specialForm" label-width="140px">
+        <el-form-item label="请选择营业日">
+          <div class="holiday-buttons">
+            <el-button
+              v-for="holiday in holidays"
+              :key="holiday.value"
+              :type="specialForm.selectedHolidays.includes(holiday.value) ? 'primary' : 'default'"
+              @click="toggleHoliday(holiday.value)"
+            >
+              {{ holiday.label }}
+            </el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="请选择营业时间段">
+          <el-radio-group v-model="specialForm.timeType">
+            <el-radio :value="'custom'"> 自定义 </el-radio>
+            <el-radio :value="'24hours'"> 24小时 </el-radio>
+          </el-radio-group>
+          <div v-if="specialForm.timeType === 'custom'" class="time-picker-group">
+            <el-time-picker
+              v-model="specialForm.startTime"
+              format="HH:mm"
+              value-format="HH:mm"
+              placeholder="开始时间"
+              style="width: 150px"
+            />
+            <span class="time-separator">至</span>
+            <el-time-picker
+              v-model="specialForm.endTime"
+              format="HH:mm"
+              value-format="HH:mm"
+              placeholder="结束时间"
+              style="width: 150px"
+            />
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="resetSpecialForm"> 重置 </el-button>
+          <el-button type="primary" @click="confirmSpecialHours"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
-<style scoped lang="scss"></style>
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+// TODO: 导入营业时间相关的 API 接口
+// import { getBusinessHours, saveBusinessHours, deleteNormalHours, deleteSpecialHours, getHolidayList } from "@/api/modules/storeDecoration";
+
+// 周一到周日
+const weekDays = [
+  { label: "周一", value: 1 },
+  { label: "周二", value: 2 },
+  { label: "周三", value: 3 },
+  { label: "周四", value: 4 },
+  { label: "周五", value: 5 },
+  { label: "周六", value: 6 },
+  { label: "周日", value: 7 }
+];
+
+// 节假日列表
+// TODO: 如果节假日列表需要从接口获取,请调用 getHolidayList API 并在 onMounted 中初始化
+const holidays = ref([
+  { label: "元旦", value: "元旦" },
+  { label: "春节", value: "春节" },
+  { label: "情人节", value: "情人节" },
+  { label: "元宵节", value: "元宵节" },
+  { label: "清明节", value: "清明节" },
+  { label: "劳动节", value: "劳动节" },
+  { label: "儿童节", value: "儿童节" },
+  { label: "端午节", value: "端午节" },
+  { label: "七夕", value: "七夕" },
+  { label: "中秋节", value: "中秋节" },
+  { label: "国庆节", value: "国庆节" },
+  { label: "冬至", value: "冬至" },
+  { label: "平安夜", value: "平安夜" },
+  { label: "圣诞节", value: "圣诞节" }
+]);
+
+// 正常营业时间列表
+interface NormalHoursItem {
+  days: number[];
+  timeType: "custom" | "24hours";
+  startTime?: string;
+  endTime?: string;
+}
+
+// TODO: 页面初始化时从接口获取正常营业时间数据
+const normalHours = ref<NormalHoursItem[]>([
+  { days: [1, 2, 3, 4, 5, 6, 7], timeType: "custom", startTime: "07:00", endTime: "19:00" }
+]);
+
+// 特殊营业时间列表
+interface SpecialHoursItem {
+  holidays: string[];
+  timeType: "custom" | "24hours";
+  startTime?: string;
+  endTime?: string;
+}
+
+// TODO: 页面初始化时从接口获取特殊营业时间数据
+const specialHours = ref<SpecialHoursItem[]>([]);
+
+// 正常营业时间对话框
+const normalDialogVisible = ref(false);
+const normalEditIndex = ref<number | null>(null);
+const normalDialogTitle = computed(() => (normalEditIndex.value !== null ? "正常营业时间" : "正常营业时间"));
+
+const normalForm = reactive({
+  selectedDays: [] as number[],
+  timeType: "custom" as "custom" | "24hours",
+  startTime: "",
+  endTime: ""
+});
+
+// 特殊营业时间对话框
+const specialDialogVisible = ref(false);
+const specialEditIndex = ref<number | null>(null);
+
+const specialForm = reactive({
+  selectedHolidays: [] as string[],
+  timeType: "custom" as "custom" | "24hours",
+  startTime: "",
+  endTime: ""
+});
+
+// 格式化正常营业时间显示
+const formatNormalHours = (item: NormalHoursItem) => {
+  const sortedDays = [...item.days].sort((a, b) => a - b);
+
+  // 如果是连续的天数,使用"至"连接
+  let dayLabels = "";
+  if (sortedDays.length === 7) {
+    dayLabels = "周一至周日";
+  } else if (sortedDays.length > 1 && sortedDays[sortedDays.length - 1] - sortedDays[0] === sortedDays.length - 1) {
+    // 连续的天数
+    const startDay = weekDays.find(d => d.value === sortedDays[0])?.label;
+    const endDay = weekDays.find(d => d.value === sortedDays[sortedDays.length - 1])?.label;
+    dayLabels = `${startDay}至${endDay}`;
+  } else {
+    // 不连续的天数,用顿号连接
+    dayLabels = sortedDays.map(day => weekDays.find(d => d.value === day)?.label).join("、");
+  }
+
+  if (item.timeType === "24hours") {
+    return `${dayLabels} 24小时`;
+  }
+  return `${dayLabels} ${item.startTime}-${item.endTime}`;
+};
+
+// 格式化特殊营业时间显示
+const formatSpecialHours = (item: SpecialHoursItem) => {
+  const holidayLabels = item.holidays.join("、");
+  if (item.timeType === "24hours") {
+    return `${holidayLabels} 24小时`;
+  }
+  return `${holidayLabels} ${item.startTime}-${item.endTime}`;
+};
+
+// 切换选择日期
+const toggleDay = (day: number) => {
+  const index = normalForm.selectedDays.indexOf(day);
+  if (index > -1) {
+    normalForm.selectedDays.splice(index, 1);
+  } else {
+    normalForm.selectedDays.push(day);
+  }
+};
+
+// 切换选择节假日
+const toggleHoliday = (holiday: string) => {
+  const index = specialForm.selectedHolidays.indexOf(holiday);
+  if (index > -1) {
+    specialForm.selectedHolidays.splice(index, 1);
+  } else {
+    specialForm.selectedHolidays.push(holiday);
+  }
+};
+
+// 打开添加正常营业时间对话框
+const openAddNormalDialog = () => {
+  normalEditIndex.value = null;
+  resetNormalForm();
+  normalDialogVisible.value = true;
+};
+
+// 编辑正常营业时间
+const editNormalHours = (index: number) => {
+  normalEditIndex.value = index;
+  const item = normalHours.value[index];
+  normalForm.selectedDays = [...item.days];
+  normalForm.timeType = item.timeType;
+  normalForm.startTime = item.startTime || "";
+  normalForm.endTime = item.endTime || "";
+  normalDialogVisible.value = true;
+};
+
+// 删除正常营业时间
+const deleteNormalHours = (index: number) => {
+  // TODO: 调用删除正常营业时间接口 deleteNormalHours API
+  // const item = normalHours.value[index];
+  // await deleteNormalHours({ id: item.id });
+  normalHours.value.splice(index, 1);
+  ElMessage.success("删除成功");
+};
+
+// 重置正常营业时间表单
+const resetNormalForm = () => {
+  normalForm.selectedDays = [];
+  normalForm.timeType = "custom";
+  normalForm.startTime = "";
+  normalForm.endTime = "";
+  normalEditIndex.value = null;
+};
+
+// 确认正常营业时间
+const confirmNormalHours = () => {
+  if (normalForm.selectedDays.length === 0) {
+    ElMessage.warning("请选择营业日");
+    return;
+  }
+  if (normalForm.timeType === "custom" && (!normalForm.startTime || !normalForm.endTime)) {
+    ElMessage.warning("请选择营业时间段");
+    return;
+  }
+
+  const newItem: NormalHoursItem = {
+    days: [...normalForm.selectedDays],
+    timeType: normalForm.timeType,
+    startTime: normalForm.timeType === "custom" ? normalForm.startTime : undefined,
+    endTime: normalForm.timeType === "custom" ? normalForm.endTime : undefined
+  };
+
+  if (normalEditIndex.value !== null) {
+    normalHours.value[normalEditIndex.value] = newItem;
+    ElMessage.success("编辑成功");
+  } else {
+    normalHours.value.push(newItem);
+    ElMessage.success("添加成功");
+  }
+
+  normalDialogVisible.value = false;
+  resetNormalForm();
+};
+
+// 打开添加特殊营业时间对话框
+const openAddSpecialDialog = () => {
+  specialEditIndex.value = null;
+  resetSpecialForm();
+  specialDialogVisible.value = true;
+};
+
+// 编辑特殊营业时间
+const editSpecialHours = (index: number) => {
+  specialEditIndex.value = index;
+  const item = specialHours.value[index];
+  specialForm.selectedHolidays = [...item.holidays];
+  specialForm.timeType = item.timeType;
+  specialForm.startTime = item.startTime || "";
+  specialForm.endTime = item.endTime || "";
+  specialDialogVisible.value = true;
+};
+
+// 删除特殊营业时间
+const deleteSpecialHours = (index: number) => {
+  // TODO: 调用删除特殊营业时间接口 deleteSpecialHours API
+  // const item = specialHours.value[index];
+  // await deleteSpecialHours({ id: item.id });
+  specialHours.value.splice(index, 1);
+  ElMessage.success("删除成功");
+};
+
+// 重置特殊营业时间表单
+const resetSpecialForm = () => {
+  specialForm.selectedHolidays = [];
+  specialForm.timeType = "custom";
+  specialForm.startTime = "";
+  specialForm.endTime = "";
+  specialEditIndex.value = null;
+};
+
+// 确认特殊营业时间
+const confirmSpecialHours = () => {
+  if (specialForm.selectedHolidays.length === 0) {
+    ElMessage.warning("请选择营业日");
+    return;
+  }
+  if (specialForm.timeType === "custom" && (!specialForm.startTime || !specialForm.endTime)) {
+    ElMessage.warning("请选择营业时间段");
+    return;
+  }
+
+  const newItem: SpecialHoursItem = {
+    holidays: [...specialForm.selectedHolidays],
+    timeType: specialForm.timeType,
+    startTime: specialForm.timeType === "custom" ? specialForm.startTime : undefined,
+    endTime: specialForm.timeType === "custom" ? specialForm.endTime : undefined
+  };
+
+  if (specialEditIndex.value !== null) {
+    specialHours.value[specialEditIndex.value] = newItem;
+    ElMessage.success("编辑成功");
+  } else {
+    specialHours.value.push(newItem);
+    ElMessage.success("添加成功");
+  }
+
+  specialDialogVisible.value = false;
+  resetSpecialForm();
+};
+
+// 页面初始化时获取数据
+onMounted(async () => {
+  // TODO: 调用获取营业时间接口 getBusinessHours API,初始化 normalHours 和 specialHours
+  // const res = await getBusinessHours();
+  // if (res.data) {
+  //   normalHours.value = res.data.normalHours || [];
+  //   specialHours.value = res.data.specialHours || [];
+  // }
+  // TODO: 如果节假日列表需要从接口获取,调用 getHolidayList API
+  // const holidayRes = await getHolidayList({});
+  // if (holidayRes.data) {
+  //   holidays.value = holidayRes.data.map(item => ({ label: item.name, value: item.name }));
+  // }
+});
+
+// 保存
+const handleSave = async () => {
+  // TODO: 调用保存营业时间接口 saveBusinessHours API
+  // 需要将 normalHours 和 specialHours 数据格式化为接口所需格式
+  // const params = {
+  //   normalHours: normalHours.value,
+  //   specialHours: specialHours.value
+  // };
+  // await saveBusinessHours(params);
+  console.log("正常营业时间:", normalHours.value);
+  console.log("特殊营业时间:", specialHours.value);
+  ElMessage.success("保存成功");
+};
+</script>
+
+<style scoped lang="scss">
+.business-hours-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .page-title {
+    margin-bottom: 24px;
+    font-size: 18px;
+    font-weight: bold;
+    color: #000000;
+  }
+  .section {
+    margin-bottom: 32px;
+    .section-title {
+      margin-bottom: 16px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #000000;
+    }
+    .hours-list {
+      margin-bottom: 16px;
+      .hours-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 12px 0;
+        border-bottom: 1px solid #f0f0f0;
+        .hours-text {
+          font-size: 14px;
+          color: #000000;
+        }
+        .action-buttons {
+          display: flex;
+          gap: 8px;
+        }
+      }
+    }
+  }
+  .footer {
+    display: flex;
+    justify-content: center;
+    margin-top: 40px;
+  }
+}
+.day-buttons,
+.holiday-buttons {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  .el-button {
+    margin: 0;
+  }
+}
+.time-picker-group {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-top: 12px;
+  .time-separator {
+    color: #606266;
+  }
+}
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+</style>

+ 344 - 4
src/views/storeDecoration/storeEntranceMap/index.vue

@@ -1,7 +1,347 @@
-<script setup lang="ts"></script>
-
 <template>
-  <div>1</div>
+  <div class="entrance-map-container">
+    <div class="content-wrapper">
+      <!-- 左侧:上传区域 -->
+      <div class="left-section">
+        <h2 class="section-title">图片</h2>
+
+        <!-- 说明文字 -->
+        <div class="instructions">
+          <ol class="instruction-list">
+            <li>入口图建议上传能够代表店铺的图片,如环境范围、菜品样式、本店特色等。</li>
+            <li>建议上传1:1尺寸的图片,避免图片出现显示不全问题。</li>
+            <li>建议尺寸900*900像素以内,不超过10MB。</li>
+          </ol>
+        </div>
+
+        <!-- 图片上传组件 -->
+        <el-form ref="formRef" :model="formData" :rules="rules" label-width="0">
+          <el-form-item prop="entranceImage">
+            <UploadImg
+              v-model:image-url="formData.entranceImage"
+              :width="'400px'"
+              :height="'400px'"
+              :file-size="10"
+              :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+              :border-radius="'8px'"
+              :api="customUploadApi"
+            >
+              <template #tip>
+                <div class="upload-tip">上传图片 ({{ formData.entranceImage ? "1" : "0" }}/1)</div>
+              </template>
+            </UploadImg>
+          </el-form-item>
+        </el-form>
+
+        <!-- 保存按钮 -->
+        <div class="footer">
+          <el-button type="primary" size="large" :loading="loading" @click="handleSave"> 保存 </el-button>
+        </div>
+      </div>
+
+      <!-- 右侧:预览区域 -->
+      <div class="right-section">
+        <h2 class="section-title">示例</h2>
+        <div class="preview-container">
+          <div class="preview-card">
+            <div class="preview-image">
+              <img v-if="formData.entranceImage" :src="formData.entranceImage" alt="入口图" />
+              <div v-else class="preview-placeholder">示例图</div>
+            </div>
+            <div class="preview-content">
+              <div class="preview-rating">
+                <el-rate v-model="previewData.rating" disabled show-score text-color="#ff9900" score-template="{value}" />
+              </div>
+              <div class="preview-reviews">{{ previewData.reviews }}条</div>
+              <div class="preview-price">¥{{ previewData.price }}/人</div>
+              <div class="preview-address">
+                {{ previewData.address }}
+              </div>
+              <div class="preview-distance">
+                {{ previewData.distance }}
+              </div>
+            </div>
+          </div>
+          <div class="preview-placeholder-bottom" />
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
-<style scoped lang="scss"></style>
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+import UploadImg from "@/components/Upload/Img.vue";
+import { uploadImg } from "@/api/modules/upload";
+// TODO: 导入入口图相关的 API 接口
+// import { getEntranceMap, saveEntranceMap } from "@/api/modules/storeDecoration";
+
+const loading = ref(false);
+const formRef = ref<FormInstance>();
+
+// 表单数据
+const formData = reactive({
+  entranceImage: "" // 入口图URL
+});
+
+// 图片尺寸和比例校验函数
+const validateImageDimensions = (file: File): Promise<boolean> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e: ProgressEvent<FileReader>) => {
+      const img = new Image();
+      img.onload = () => {
+        const width = img.width;
+        const height = img.height;
+        const aspectRatio = width / height;
+
+        // 检查尺寸:900*900像素以内
+        if (width > 900 || height > 900) {
+          ElMessage.warning("图片尺寸不能超过900*900像素");
+          reject(new Error("图片尺寸超过限制"));
+          return;
+        }
+
+        // 检查比例:1:1(允许一定误差,比如0.95-1.05之间)
+        if (aspectRatio < 0.95 || aspectRatio > 1.05) {
+          ElMessage.warning("建议上传1:1尺寸的图片,避免图片出现显示不全问题");
+          reject(new Error("图片比例不符合要求"));
+          return;
+        }
+
+        resolve(true);
+      };
+      img.onerror = () => {
+        reject(new Error("图片加载失败"));
+      };
+      if (e.target?.result) {
+        img.src = e.target.result as string;
+      }
+    };
+    reader.onerror = () => {
+      reject(new Error("文件读取失败"));
+    };
+    reader.readAsDataURL(file);
+  });
+};
+
+// 自定义上传API,包含尺寸和比例校验
+const customUploadApi = async (formData: FormData): Promise<any> => {
+  const file = formData.get("file") as File;
+
+  // 先进行尺寸和比例校验
+  try {
+    await validateImageDimensions(file);
+  } catch (error) {
+    throw error;
+  }
+
+  // 校验通过后调用实际上传接口
+  return uploadImg(formData);
+};
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  entranceImage: [
+    { required: true, message: "请上传入口图", trigger: "change" },
+    {
+      validator: (rule, value, callback) => {
+        if (!value) {
+          callback(new Error("请上传入口图"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+});
+
+// 预览数据(示例数据)
+const previewData = reactive({
+  rating: 4.9,
+  reviews: 1127,
+  price: 70,
+  address: "某某街道 某某路",
+  distance: "616m"
+});
+
+// 页面初始化时获取数据
+onMounted(async () => {
+  // TODO: 调用获取入口图接口 getEntranceMap API
+  // const res = await getEntranceMap();
+  // if (res.data && res.data.entranceImage) {
+  //   formData.entranceImage = res.data.entranceImage;
+  // }
+});
+
+// 保存
+const handleSave = async () => {
+  if (!formRef.value) return;
+
+  // 表单校验
+  try {
+    await formRef.value.validate();
+  } catch (error) {
+    ElMessage.warning("请完成必填项");
+    return;
+  }
+
+  if (!formData.entranceImage) {
+    ElMessage.warning("请上传入口图");
+    return;
+  }
+
+  loading.value = true;
+  try {
+    // TODO: 调用保存入口图接口 saveEntranceMap API
+    // const params = {
+    //   entranceImage: formData.entranceImage
+    // };
+    // await saveEntranceMap(params);
+
+    ElMessage.success("保存成功");
+  } catch (error) {
+    ElMessage.error("保存失败,请重试");
+    console.error("保存失败:", error);
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.entrance-map-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .content-wrapper {
+    display: flex;
+    gap: 40px;
+    max-width: 1400px;
+    margin: 0 auto;
+    .left-section,
+    .right-section {
+      flex: 1;
+    }
+    .section-title {
+      margin-bottom: 20px;
+      font-size: 18px;
+      font-weight: bold;
+      color: #000000;
+    }
+    .left-section {
+      .instructions {
+        margin-bottom: 24px;
+        .instruction-list {
+          padding-left: 20px;
+          margin: 0;
+          font-size: 14px;
+          line-height: 24px;
+          color: #606266;
+          li {
+            margin-bottom: 8px;
+          }
+        }
+      }
+      .upload-section {
+        display: flex;
+        justify-content: center;
+        margin-bottom: 40px;
+        .upload-tip {
+          margin-top: 12px;
+          font-size: 14px;
+          color: #909399;
+          text-align: center;
+        }
+      }
+      .footer {
+        display: flex;
+        justify-content: center;
+        margin-top: 40px;
+      }
+    }
+    .right-section {
+      .preview-container {
+        min-height: 600px;
+        padding: 20px;
+        background-color: #f5f7fa;
+        border-radius: 8px;
+        .preview-card {
+          display: flex;
+          gap: 16px;
+          padding: 16px;
+          background-color: white;
+          border-radius: 8px;
+          box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
+          .preview-image {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 120px;
+            min-width: 120px;
+            height: 120px;
+            overflow: hidden;
+            background-color: #f0f0f0;
+            border-radius: 8px;
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+            .preview-placeholder {
+              font-size: 14px;
+              color: #909399;
+            }
+          }
+          .preview-content {
+            display: flex;
+            flex: 1;
+            flex-direction: column;
+            justify-content: space-between;
+            .preview-rating {
+              display: flex;
+              align-items: center;
+              margin-bottom: 8px;
+              :deep(.el-rate) {
+                .el-rate__icon {
+                  font-size: 16px;
+                }
+              }
+            }
+            .preview-reviews {
+              margin-bottom: 8px;
+              font-size: 14px;
+              color: #606266;
+            }
+            .preview-price {
+              margin-bottom: 8px;
+              font-size: 16px;
+              font-weight: bold;
+              color: #f56c6c;
+            }
+            .preview-address {
+              margin-bottom: 8px;
+              font-size: 14px;
+              color: #606266;
+            }
+            .preview-distance {
+              font-size: 14px;
+              color: #909399;
+            }
+          }
+        }
+        .preview-placeholder-bottom {
+          height: 200px;
+          margin-top: 20px;
+          background-color: #e4e7ed;
+          border-radius: 8px;
+          opacity: 0.3;
+        }
+      }
+    }
+  }
+}
+</style>

+ 615 - 4
src/views/storeDecoration/storeHeadMap/index.vue

@@ -1,7 +1,618 @@
-<script setup lang="ts"></script>
-
 <template>
-  <div>1</div>
+  <div class="head-map-container">
+    <div class="content-wrapper">
+      <!-- 顶部:模式选择预览 -->
+      <div class="mode-preview-section">
+        <!-- 单图模式预览 -->
+        <div class="preview-box" :class="{ active: mode === 'single' }" @click="mode = 'single'">
+          <div class="preview-card">
+            <div class="preview-image-large">
+              <img v-if="mode === 'single' && formData.singleImage" :src="formData.singleImage" alt="单图" />
+              <div v-else class="preview-placeholder">示例图</div>
+              <div class="overlay-button">
+                <span>相册</span>
+                <el-icon><ArrowRight /></el-icon>
+              </div>
+            </div>
+            <div class="preview-info">
+              <div class="store-name">示例门店</div>
+              <div class="store-rating">
+                <el-rate v-model="previewData.rating" disabled show-score text-color="#ff9900" score-template="{value}" />
+              </div>
+              <div class="store-reviews">{{ previewData.reviews }} 条评价</div>
+              <el-icon class="arrow-icon">
+                <ArrowRight />
+              </el-icon>
+            </div>
+          </div>
+          <div class="mode-label">
+            <span>单图模式</span>
+            <el-tag v-if="mode === 'single'" type="success" size="small"> 生效中 </el-tag>
+          </div>
+        </div>
+
+        <!-- 多图模式预览 -->
+        <div class="preview-box" :class="{ active: mode === 'multiple' }" @click="mode = 'multiple'">
+          <div class="preview-card">
+            <div class="preview-images-multiple">
+              <div v-for="(img, index) in displayImages" :key="index" class="preview-image-small">
+                <img v-if="img" :src="img" alt="多图" />
+                <div v-else class="preview-placeholder">示例图</div>
+              </div>
+              <div v-if="displayImages.length < 3" class="preview-image-small">
+                <div class="preview-placeholder">示例图</div>
+              </div>
+              <div class="overlay-button">
+                <span>相册</span>
+                <el-icon><ArrowRight /></el-icon>
+              </div>
+            </div>
+            <div class="preview-info">
+              <div class="store-name">示例门店</div>
+              <div class="store-rating">
+                <el-rate v-model="previewData.rating" disabled show-score text-color="#ff9900" score-template="{value}" />
+              </div>
+              <div class="store-reviews">{{ previewData.reviews }} 条评价</div>
+              <el-icon class="arrow-icon">
+                <ArrowRight />
+              </el-icon>
+            </div>
+          </div>
+          <div class="mode-label">
+            <span>多图模式</span>
+            <el-tag v-if="mode === 'multiple'" type="success" size="small"> 生效中 </el-tag>
+          </div>
+        </div>
+      </div>
+
+      <!-- 中间:上传区域 -->
+      <div class="upload-section">
+        <h2 class="section-title">图片</h2>
+
+        <!-- 说明文字 -->
+        <div class="instructions">
+          <ol class="instruction-list">
+            <li v-if="mode === 'single'">入口图建议上传能够代表店铺的图片,如环境范围、菜品样式、本店特色等。</li>
+            <li v-if="mode === 'single'">建议上传1:1尺寸的图片,避免图片出现显示不全问题。</li>
+            <li v-if="mode === 'single'">建议尺寸900*900像素以内,不超过10MB。</li>
+            <li v-if="mode === 'multiple'">图片会在店铺头图区域展示。</li>
+            <li v-if="mode === 'multiple'">至少上传3张图片,可拖拽排序。</li>
+            <li v-if="mode === 'multiple'">建议上传16:9尺寸的图片,大小不超过20MB 避免图片出现显示不全问题。</li>
+          </ol>
+        </div>
+
+        <!-- 单图模式上传 -->
+        <el-form v-if="mode === 'single'" ref="singleFormRef" :model="formData" :rules="singleRules" label-width="0">
+          <el-form-item prop="singleImage">
+            <UploadImg
+              v-model:image-url="formData.singleImage"
+              :width="'400px'"
+              :height="'400px'"
+              :file-size="10"
+              :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+              :border-radius="'8px'"
+              :api="singleImageUploadApi"
+            >
+              <template #tip>
+                <div class="upload-tip">上传图片 ({{ formData.singleImage ? "1" : "0" }}/1)</div>
+              </template>
+            </UploadImg>
+          </el-form-item>
+        </el-form>
+
+        <!-- 多图模式上传 -->
+        <el-form v-if="mode === 'multiple'" ref="multipleFormRef" :model="formData" :rules="multipleRules" label-width="0">
+          <el-form-item prop="multipleImages">
+            <div class="multiple-upload-wrapper">
+              <UploadImgs
+                v-model:file-list="formData.multipleImages"
+                :limit="6"
+                :file-size="20"
+                :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+                :width="'200px'"
+                :height="'112px'"
+                :border-radius="'8px'"
+                :api="multipleImageUploadApi"
+              >
+                <template #tip>
+                  <div class="upload-tip">上传图片 ({{ formData.multipleImages.length }}/6)</div>
+                </template>
+              </UploadImgs>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 底部:保存按钮 -->
+      <div class="footer">
+        <el-button type="primary" size="large" :loading="loading" @click="handleSave"> 保存 </el-button>
+      </div>
+    </div>
+  </div>
 </template>
 
-<style scoped lang="scss"></style>
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+import type { UploadUserFile } from "element-plus";
+import { ArrowRight } from "@element-plus/icons-vue";
+import Sortable from "sortablejs";
+import UploadImg from "@/components/Upload/Img.vue";
+import UploadImgs from "@/components/Upload/Imgs.vue";
+import { uploadImg } from "@/api/modules/upload";
+// TODO: 导入头图相关的 API 接口
+// import { getStoreHeadMap, saveStoreHeadMap } from "@/api/modules/storeDecoration";
+
+const loading = ref(false);
+const singleFormRef = ref<FormInstance>();
+const multipleFormRef = ref<FormInstance>();
+const sortableInstance = ref<Sortable | null>(null);
+
+// 模式:single 单图模式,multiple 多图模式
+const mode = ref<"single" | "multiple">("single");
+
+// 表单数据
+const formData = reactive({
+  singleImage: "", // 单图模式图片URL
+  multipleImages: [] as UploadUserFile[] // 多图模式图片列表
+});
+
+// 预览数据(示例数据)
+const previewData = reactive({
+  rating: 5.0,
+  reviews: 1853
+});
+
+// 显示的多图预览(最多显示3张)
+const displayImages = computed(() => {
+  if (mode.value === "multiple") {
+    return formData.multipleImages.slice(0, 3).map(item => item.url || "");
+  }
+  return [];
+});
+
+// 单图模式:图片尺寸和比例校验函数(1:1,900*900)
+const validateSingleImageDimensions = (file: File): Promise<boolean> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e: ProgressEvent<FileReader>) => {
+      const img = new Image();
+      img.onload = () => {
+        const width = img.width;
+        const height = img.height;
+        const aspectRatio = width / height;
+
+        // 检查尺寸:900*900像素以内
+        if (width > 900 || height > 900) {
+          ElMessage.warning("图片尺寸不能超过900*900像素");
+          reject(new Error("图片尺寸超过限制"));
+          return;
+        }
+
+        // 检查比例:1:1(允许一定误差,比如0.95-1.05之间)
+        if (aspectRatio < 0.95 || aspectRatio > 1.05) {
+          ElMessage.warning("建议上传1:1尺寸的图片,避免图片出现显示不全问题");
+          reject(new Error("图片比例不符合要求"));
+          return;
+        }
+
+        resolve(true);
+      };
+      img.onerror = () => {
+        reject(new Error("图片加载失败"));
+      };
+      if (e.target?.result) {
+        img.src = e.target.result as string;
+      }
+    };
+    reader.onerror = () => {
+      reject(new Error("文件读取失败"));
+    };
+    reader.readAsDataURL(file);
+  });
+};
+
+// 多图模式:图片尺寸和比例校验函数(16:9,20MB)
+const validateMultipleImageDimensions = (file: File): Promise<boolean> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e: ProgressEvent<FileReader>) => {
+      const img = new Image();
+      img.onload = () => {
+        const width = img.width;
+        const height = img.height;
+        const aspectRatio = width / height;
+        const targetRatio = 16 / 9; // 16:9比例
+
+        // 检查比例:16:9(允许一定误差,比如±0.1)
+        if (Math.abs(aspectRatio - targetRatio) > 0.1) {
+          ElMessage.warning("建议上传16:9尺寸的图片,避免图片出现显示不全问题");
+          reject(new Error("图片比例不符合要求"));
+          return;
+        }
+
+        resolve(true);
+      };
+      img.onerror = () => {
+        reject(new Error("图片加载失败"));
+      };
+      if (e.target?.result) {
+        img.src = e.target.result as string;
+      }
+    };
+    reader.onerror = () => {
+      reject(new Error("文件读取失败"));
+    };
+    reader.readAsDataURL(file);
+  });
+};
+
+// 单图模式:自定义上传API
+const singleImageUploadApi = async (formData: FormData): Promise<any> => {
+  const file = formData.get("file") as File;
+
+  // 先进行尺寸和比例校验
+  try {
+    await validateSingleImageDimensions(file);
+  } catch (error) {
+    throw error;
+  }
+
+  // 校验通过后调用实际上传接口
+  return uploadImg(formData);
+};
+
+// 多图模式:自定义上传API
+const multipleImageUploadApi = async (formData: FormData): Promise<any> => {
+  const file = formData.get("file") as File;
+
+  // 先进行比例校验
+  try {
+    await validateMultipleImageDimensions(file);
+  } catch (error) {
+    throw error;
+  }
+
+  // 校验通过后调用实际上传接口
+  return uploadImg(formData);
+};
+
+// 单图模式表单校验规则
+const singleRules = reactive<FormRules>({
+  singleImage: [
+    { required: true, message: "请上传入口图", trigger: "change" },
+    {
+      validator: (rule, value, callback) => {
+        if (!value) {
+          callback(new Error("请上传入口图"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+});
+
+// 多图模式表单校验规则
+const multipleRules = reactive<FormRules>({
+  multipleImages: [
+    {
+      validator: (rule, value, callback) => {
+        if (!value || value.length < 3) {
+          callback(new Error("至少上传3张图片"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+});
+
+// 监听模式切换,清空另一个模式的数据
+watch(
+  () => mode.value,
+  newMode => {
+    if (newMode === "single") {
+      formData.multipleImages = [];
+    } else {
+      formData.singleImage = "";
+      // 切换到多图模式时,初始化拖拽排序
+      nextTick(() => {
+        initDragSort();
+      });
+    }
+  }
+);
+
+// 监听多图列表变化,重新初始化拖拽排序
+watch(
+  () => formData.multipleImages.length,
+  () => {
+    if (mode.value === "multiple") {
+      nextTick(() => {
+        initDragSort();
+      });
+    }
+  }
+);
+
+// 初始化拖拽排序
+const initDragSort = () => {
+  // 如果已存在实例,先销毁
+  if (sortableInstance.value) {
+    sortableInstance.value.destroy();
+    sortableInstance.value = null;
+  }
+
+  const uploadList = document.querySelector(".el-upload-list--picture-card");
+  if (!uploadList) return;
+
+  sortableInstance.value = Sortable.create(uploadList as HTMLElement, {
+    animation: 300,
+    onEnd({ newIndex, oldIndex }) {
+      if (newIndex === undefined || oldIndex === undefined) return;
+      const newList = [...formData.multipleImages];
+      const [removedItem] = newList.splice(oldIndex, 1);
+      newList.splice(newIndex, 0, removedItem);
+      formData.multipleImages = newList;
+    }
+  });
+};
+
+// 页面初始化时获取数据
+onMounted(async () => {
+  // TODO: 调用获取头图接口 getStoreHeadMap API
+  // const res = await getStoreHeadMap();
+  // if (res.data) {
+  //   mode.value = res.data.mode || 'single';
+  //   if (res.data.mode === 'single') {
+  //     formData.singleImage = res.data.singleImage || '';
+  //   } else {
+  //     formData.multipleImages = res.data.multipleImages || [];
+  //     // 初始化拖拽排序
+  //     nextTick(() => {
+  //       initDragSort();
+  //     });
+  //   }
+  // }
+
+  // 如果默认是多图模式,初始化拖拽排序
+  if (mode.value === "multiple") {
+    nextTick(() => {
+      initDragSort();
+    });
+  }
+});
+
+// 保存
+const handleSave = async () => {
+  if (mode.value === "single") {
+    if (!singleFormRef.value) return;
+    try {
+      await singleFormRef.value.validate();
+    } catch (error) {
+      ElMessage.warning("请完成必填项");
+      return;
+    }
+  } else {
+    if (!multipleFormRef.value) return;
+    try {
+      await multipleFormRef.value.validate();
+    } catch (error) {
+      ElMessage.warning("至少上传3张图片");
+      return;
+    }
+  }
+
+  loading.value = true;
+  try {
+    // TODO: 调用保存头图接口 saveStoreHeadMap API
+    // const params = {
+    //   mode: mode.value,
+    //   singleImage: mode.value === 'single' ? formData.singleImage : '',
+    //   multipleImages: mode.value === 'multiple' ? formData.multipleImages.map(item => item.url) : []
+    // };
+    // await saveStoreHeadMap(params);
+
+    ElMessage.success("保存成功");
+  } catch (error) {
+    ElMessage.error("保存失败,请重试");
+    console.error("保存失败:", error);
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.head-map-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .content-wrapper {
+    max-width: 1400px;
+    margin: 0 auto;
+  }
+  .mode-preview-section {
+    display: flex;
+    gap: 24px;
+    margin-bottom: 40px;
+    .preview-box {
+      flex: 1;
+      cursor: pointer;
+      transition: all 0.3s;
+      &.active {
+        .preview-card {
+          border: 2px solid #409eff;
+        }
+      }
+      .preview-card {
+        padding: 16px;
+        background-color: white;
+        border: 2px solid transparent;
+        border-radius: 8px;
+        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
+        transition: all 0.3s;
+        .preview-image-large {
+          position: relative;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 200px;
+          margin-bottom: 16px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          border-radius: 8px;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .preview-placeholder {
+            font-size: 14px;
+            color: #909399;
+          }
+          .overlay-button {
+            position: absolute;
+            right: 12px;
+            bottom: 12px;
+            display: flex;
+            gap: 4px;
+            align-items: center;
+            padding: 6px 12px;
+            font-size: 12px;
+            color: white;
+            background-color: rgb(0 0 0 / 60%);
+            border-radius: 4px;
+          }
+        }
+        .preview-images-multiple {
+          position: relative;
+          display: flex;
+          gap: 4px;
+          width: 100%;
+          height: 200px;
+          margin-bottom: 16px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          border-radius: 8px;
+          .preview-image-small {
+            display: flex;
+            flex: 1;
+            align-items: center;
+            justify-content: center;
+            height: 100%;
+            overflow: hidden;
+            background-color: #e4e7ed;
+            border-radius: 4px;
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+            .preview-placeholder {
+              font-size: 12px;
+              color: #909399;
+            }
+          }
+          .overlay-button {
+            position: absolute;
+            right: 12px;
+            bottom: 12px;
+            display: flex;
+            gap: 4px;
+            align-items: center;
+            padding: 6px 12px;
+            font-size: 12px;
+            color: white;
+            background-color: rgb(0 0 0 / 60%);
+            border-radius: 4px;
+          }
+        }
+        .preview-info {
+          position: relative;
+          .store-name {
+            margin-bottom: 8px;
+            font-size: 16px;
+            font-weight: bold;
+            color: #000000;
+          }
+          .store-rating {
+            display: flex;
+            align-items: center;
+            margin-bottom: 4px;
+            :deep(.el-rate) {
+              .el-rate__icon {
+                font-size: 14px;
+              }
+            }
+          }
+          .store-reviews {
+            font-size: 14px;
+            color: #606266;
+          }
+          .arrow-icon {
+            position: absolute;
+            top: 50%;
+            right: 0;
+            font-size: 18px;
+            color: #909399;
+            transform: translateY(-50%);
+          }
+        }
+      }
+      .mode-label {
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        justify-content: center;
+        margin-top: 12px;
+        font-size: 14px;
+        color: #606266;
+        text-align: center;
+      }
+    }
+  }
+  .upload-section {
+    margin-bottom: 40px;
+    .section-title {
+      margin-bottom: 20px;
+      font-size: 18px;
+      font-weight: bold;
+      color: #000000;
+    }
+    .instructions {
+      margin-bottom: 24px;
+      .instruction-list {
+        padding-left: 20px;
+        margin: 0;
+        font-size: 14px;
+        line-height: 24px;
+        color: #606266;
+        li {
+          margin-bottom: 8px;
+        }
+      }
+    }
+    .multiple-upload-wrapper {
+      display: flex;
+      justify-content: center;
+    }
+    .upload-tip {
+      margin-top: 12px;
+      font-size: 14px;
+      color: #909399;
+      text-align: center;
+    }
+  }
+  .footer {
+    display: flex;
+    justify-content: center;
+    margin-top: 40px;
+  }
+}
+</style>