Bladeren bron

页面建立

congxuesong 1 maand geleden
bovenliggende
commit
717d39cda5

+ 26 - 13
src/assets/json/authMenuList.json

@@ -232,7 +232,7 @@
     {
       "path": "/licenseManagement",
       "name": "licenseManagement",
-      "redirect": "/licenseManagement/index",
+      "redirect": "/licenseManagement/businessLicense",
       "meta": {
         "icon": "Files",
         "title": "证照管理",
@@ -244,12 +244,12 @@
       },
       "children": [
         {
-          "path": "/licenseManagement/index",
-          "name": "licenseManagementIndex",
-          "component": "/licenseManagement/index",
+          "path": "/licenseManagement/businessLicense",
+          "name": "businessLicense",
+          "component": "/licenseManagement/businessLicense",
           "meta": {
-            "icon": "Files",
-            "title": "证照管理",
+            "icon": "Document",
+            "title": "营业执照",
             "isLink": "",
             "isHide": false,
             "isFull": false,
@@ -258,15 +258,28 @@
           }
         },
         {
-          "path": "/licenseManagementDetail",
-          "name": "licenseManagementDetail",
-          "component": "/licenseManagement/detail",
+          "path": "/licenseManagement/contractManagement",
+          "name": "contractManagement",
+          "component": "/licenseManagement/contractManagement",
           "meta": {
-            "icon": "Menu",
-            "title": "证照管理详情",
-            "activeMenu": "/licenseManagement",
+            "icon": "Picture",
+            "title": "合同管理",
             "isLink": "",
-            "isHide": true,
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/licenseManagement/foodBusinessLicense",
+          "name": "foodBusinessLicense",
+          "component": "/licenseManagement/foodBusinessLicense",
+          "meta": {
+            "icon": "Document",
+            "title": "食品经营许可证",
+            "isLink": "",
+            "isHide": false,
             "isFull": false,
             "isAffix": false,
             "isKeepAlive": false

+ 54 - 1
src/layouts/LayoutClassic/index.vue

@@ -18,8 +18,10 @@
         <div class="aside-box" :style="{ width: isCollapse ? '65px' : '210px' }">
           <el-scrollbar>
             <el-menu
+              ref="menuRef"
               :router="false"
               :default-active="activeMenu"
+              :default-openeds="openedMenus"
               :collapse="isCollapse"
               :unique-opened="accordion"
               :collapse-transition="false"
@@ -37,7 +39,7 @@
 </template>
 
 <script setup lang="ts" name="layoutClassic">
-import { computed } from "vue";
+import { computed, ref, watch, nextTick } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -45,6 +47,7 @@ import Main from "@/layouts/components/Main/index.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
+import type { ElMenu } from "element-plus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -55,6 +58,56 @@ const accordion = computed(() => globalStore.accordion);
 const isCollapse = computed(() => globalStore.isCollapse);
 const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
+
+const menuRef = ref<InstanceType<typeof ElMenu>>();
+const openedMenus = ref<string[]>([]);
+
+// 计算应该展开的父菜单路径
+const getParentMenuPath = (path: string) => {
+  const pathSegments = path.split("/").filter(Boolean);
+  // 只有当路径有两段或以上时才返回父菜单路径
+  // 例如: /licenseManagement/businessLicense -> /licenseManagement
+  if (pathSegments.length > 1) {
+    return `/${pathSegments[0]}`;
+  }
+  return "";
+};
+
+// 根据当前路由计算应该展开的父菜单
+const calculateOpenedMenus = () => {
+  const currentPath = (route.meta.activeMenu as string) || route.path;
+  const parentPath = getParentMenuPath(currentPath);
+
+  if (parentPath) {
+    // 如果手风琴模式开启,只保留当前父菜单
+    if (accordion.value) {
+      return [parentPath];
+    } else {
+      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
+      const newOpenedMenus = [...openedMenus.value];
+      if (!newOpenedMenus.includes(parentPath)) {
+        newOpenedMenus.push(parentPath);
+      }
+      return newOpenedMenus;
+    }
+  }
+  return openedMenus.value;
+};
+
+// 监听路由变化,保持父菜单展开
+watch(
+  () => [route.path, route.meta.activeMenu],
+  () => {
+    nextTick(() => {
+      const newOpenedMenus = calculateOpenedMenus();
+      // 只有当展开菜单发生变化时才更新
+      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
+        openedMenus.value = newOpenedMenus;
+      }
+    });
+  },
+  { immediate: true }
+);
 </script>
 
 <style scoped lang="scss">

+ 54 - 1
src/layouts/LayoutVertical/index.vue

@@ -9,8 +9,10 @@
         </div>
         <el-scrollbar>
           <el-menu
+            ref="menuRef"
             :router="false"
             :default-active="activeMenu"
+            :default-openeds="openedMenus"
             :collapse="isCollapse"
             :unique-opened="accordion"
             :collapse-transition="false"
@@ -31,7 +33,7 @@
 </template>
 
 <script setup lang="ts" name="layoutVertical">
-import { computed } from "vue";
+import { computed, ref, watch, nextTick } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -39,6 +41,7 @@ import Main from "@/layouts/components/Main/index.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
+import type { ElMenu } from "element-plus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -49,6 +52,56 @@ const accordion = computed(() => globalStore.accordion);
 const isCollapse = computed(() => globalStore.isCollapse);
 const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
+
+const menuRef = ref<InstanceType<typeof ElMenu>>();
+const openedMenus = ref<string[]>([]);
+
+// 计算应该展开的父菜单路径
+const getParentMenuPath = (path: string) => {
+  const pathSegments = path.split("/").filter(Boolean);
+  // 只有当路径有两段或以上时才返回父菜单路径
+  // 例如: /licenseManagement/businessLicense -> /licenseManagement
+  if (pathSegments.length > 1) {
+    return `/${pathSegments[0]}`;
+  }
+  return "";
+};
+
+// 根据当前路由计算应该展开的父菜单
+const calculateOpenedMenus = () => {
+  const currentPath = (route.meta.activeMenu as string) || route.path;
+  const parentPath = getParentMenuPath(currentPath);
+
+  if (parentPath) {
+    // 如果手风琴模式开启,只保留当前父菜单
+    if (accordion.value) {
+      return [parentPath];
+    } else {
+      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
+      const newOpenedMenus = [...openedMenus.value];
+      if (!newOpenedMenus.includes(parentPath)) {
+        newOpenedMenus.push(parentPath);
+      }
+      return newOpenedMenus;
+    }
+  }
+  return openedMenus.value;
+};
+
+// 监听路由变化,保持父菜单展开
+watch(
+  () => [route.path, route.meta.activeMenu],
+  () => {
+    nextTick(() => {
+      const newOpenedMenus = calculateOpenedMenus();
+      // 只有当展开菜单发生变化时才更新
+      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
+        openedMenus.value = newOpenedMenus;
+      }
+    });
+  },
+  { immediate: true }
+);
 </script>
 
 <style scoped lang="scss">

+ 105 - 0
src/views/licenseManagement/businessLicense.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="card content-box">
+    <div class="page-header">
+      <h1 class="store-title">重庆老火锅</h1>
+    </div>
+    <div class="divider" />
+    <div class="tip-text">如需更换请联系客服</div>
+    <div class="license-container">
+      <div class="license-display">
+        <el-image
+          v-if="licenseImage"
+          :src="licenseImage"
+          fit="contain"
+          class="license-image"
+          :preview-src-list="[licenseImage]"
+        />
+        <div v-else class="empty-image-box">
+          <el-icon class="empty-icon">
+            <Picture />
+          </el-icon>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessLicense">
+import { ref, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import { Picture } from "@element-plus/icons-vue";
+
+const licenseImage = ref<string>("");
+
+onMounted(async () => {
+  await initData();
+});
+
+const initData = async () => {
+  try {
+    // TODO: 调用API获取营业执照图片
+    // const response = await getBusinessLicense();
+    // if (response.code === 200) {
+    //   licenseImage.value = response.data.imageUrl;
+    // }
+  } catch (error) {
+    ElMessage.error("获取营业执照失败");
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.page-header {
+  margin-bottom: 20px;
+}
+.store-title {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+.divider {
+  width: 100%;
+  height: 1px;
+  margin-bottom: 20px;
+  background-color: var(--el-border-color-lighter);
+}
+.tip-text {
+  margin-bottom: 30px;
+  font-size: 14px;
+  color: var(--el-text-color-regular);
+}
+.license-container {
+  width: 100%;
+  min-height: 500px;
+  padding: 20px;
+  background-color: var(--el-bg-color-page);
+  border-radius: 8px;
+}
+.license-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  min-height: 500px;
+}
+.license-image {
+  max-width: 100%;
+  max-height: 600px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+}
+.empty-image-box {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 500px;
+  background-color: var(--el-fill-color-lighter);
+  border-radius: 8px;
+  .empty-icon {
+    font-size: 64px;
+    color: var(--el-text-color-placeholder);
+  }
+}
+</style>

+ 424 - 0
src/views/licenseManagement/contractManagement.vue

@@ -0,0 +1,424 @@
+<template>
+  <div class="card content-box">
+    <div class="page-header">
+      <h1 class="store-title">重庆老火锅</h1>
+    </div>
+    <div class="content-section">
+      <div class="tip-text">
+        如需续约合同或更改合同图片请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的合同图片将会覆盖之前的合同
+      </div>
+      <div class="action-buttons">
+        <el-button type="primary" @click="handleReplace"> 更换 </el-button>
+        <el-button type="primary" @click="handleViewChangeRecord"> 查看变更记录 </el-button>
+      </div>
+    </div>
+    <div class="contract-container">
+      <el-row :gutter="20">
+        <el-col v-for="(item, index) in contractList" :key="index" :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
+          <div class="contract-item">
+            <el-image
+              :src="item.url"
+              fit="cover"
+              class="contract-image"
+              :preview-src-list="contractList.map(img => img.url)"
+              :initial-index="index"
+            >
+              <template #error>
+                <div class="image-slot">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+          </div>
+        </el-col>
+        <template v-for="n in Math.max(0, 8 - contractList.length)" :key="`empty-${n}`">
+          <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
+            <div class="contract-item empty-item">
+              <el-icon class="empty-icon">
+                <Picture />
+              </el-icon>
+            </div>
+          </el-col>
+        </template>
+      </el-row>
+    </div>
+
+    <!-- 更换合同弹窗 -->
+    <el-dialog v-model="replaceDialogVisible" title="更换合同" width="800px" @close="handleReplaceDialogClose">
+      <div class="replace-upload-area">
+        <el-upload
+          ref="uploadRef"
+          v-model:file-list="fileList"
+          action="#"
+          list-type="picture-card"
+          :multiple="true"
+          :limit="20"
+          :http-request="handleHttpUpload"
+          :before-upload="beforeUpload"
+          :on-exceed="handleExceed"
+          :on-remove="handleRemove"
+          :on-success="handleUploadSuccess"
+        >
+          <el-icon><Plus /></el-icon>
+          <template #tip>
+            <div class="upload-tip">({{ fileList.length }}/20)</div>
+          </template>
+        </el-upload>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleCancelReplace"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmitReplace"> 去审核 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 取消确认弹窗 -->
+    <el-dialog v-model="cancelConfirmVisible" title="提示" width="400px" :close-on-click-modal="false">
+      <div class="confirm-text">确定要取消本次图片上传吗?已上传的图片将不保存</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelConfirmVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="handleConfirmCancel"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 变更记录弹窗 -->
+    <el-dialog v-model="changeRecordDialogVisible" title="变更记录" width="900px" :close-on-click-modal="false">
+      <div class="change-record-content">
+        <div class="record-date">
+          {{ currentRecordDate }}
+        </div>
+        <div class="record-items">
+          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item" :class="getStatusClass(item.status)">
+            <div class="record-status-text">
+              {{ getStatusText(item.status) }}
+            </div>
+          </div>
+        </div>
+        <div v-if="rejectionReason" class="rejection-reason">拒绝原因: {{ rejectionReason }}</div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="changeRecordDialogVisible = false"> 关闭 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="contractManagement">
+import { ref, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus, Picture } from "@element-plus/icons-vue";
+import { uploadImg } from "@/api/modules/upload";
+import type { UploadProps, UploadFile, UploadRequestOptions } from "element-plus";
+
+interface ContractItem {
+  id: string;
+  url: string;
+  name: string;
+}
+
+interface ChangeRecordItem {
+  id: string;
+  status: "pending" | "success" | "failed";
+  rejectionReason?: string;
+}
+
+const contractList = ref<ContractItem[]>([]);
+const replaceDialogVisible = ref(false);
+const cancelConfirmVisible = ref(false);
+const changeRecordDialogVisible = ref(false);
+const fileList = ref<UploadFile[]>([]);
+const uploadRef = ref();
+const currentRecordDate = ref("2025.08.01 10:29");
+const changeRecordList = ref<ChangeRecordItem[]>([]);
+const rejectionReason = ref("");
+
+onMounted(async () => {
+  await initData();
+});
+
+const initData = async () => {
+  try {
+    // TODO: 调用API获取合同图片列表
+    // const response = await getContractImages();
+    // if (response.code === 200) {
+    //   contractList.value = response.data;
+    // }
+  } catch (error) {
+    ElMessage.error("获取合同图片失败");
+  }
+};
+
+const handleReplace = () => {
+  fileList.value = [];
+  replaceDialogVisible.value = true;
+};
+
+const handleViewChangeRecord = async () => {
+  try {
+    // TODO: 调用API获取变更记录
+    // const response = await getChangeRecords();
+    // if (response.code === 200) {
+    //   changeRecordList.value = response.data.records;
+    //   currentRecordDate.value = response.data.date;
+    //   rejectionReason.value = response.data.rejectionReason || "";
+    // }
+    // 模拟数据
+    changeRecordList.value = [
+      { id: "1", status: "pending" },
+      { id: "2", status: "pending" },
+      { id: "3", status: "pending" },
+      { id: "4", status: "pending" },
+      { id: "5", status: "pending" }
+    ];
+    rejectionReason.value = "";
+    changeRecordDialogVisible.value = true;
+  } catch (error) {
+    ElMessage.error("获取变更记录失败");
+  }
+};
+
+const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
+  const imgSize = rawFile.size / 1024 / 1024 < 10;
+  const imgType = ["image/jpeg", "image/png", "image/gif"].includes(rawFile.type);
+  if (!imgType) {
+    ElMessage.warning("上传图片不符合所需的格式!");
+    return false;
+  }
+  if (!imgSize) {
+    ElMessage.warning("上传图片大小不能超过 10M!");
+    return false;
+  }
+  return true;
+};
+
+const handleHttpUpload = async (options: UploadRequestOptions) => {
+  const formData = new FormData();
+  formData.append("file", options.file);
+  try {
+    const { data } = await uploadImg(formData);
+    const fileUrl = data.fileUrl ? data.fileUrl : data[0];
+    options.onSuccess({ fileUrl }, options.file as any);
+  } catch (error) {
+    options.onError(error as any);
+  }
+};
+
+const handleExceed = () => {
+  ElMessage.warning("最多只能上传 20 张图片");
+};
+
+const handleRemove = (file: UploadFile) => {
+  const index = fileList.value.findIndex(item => item.uid === file.uid);
+  if (index > -1) {
+    fileList.value.splice(index, 1);
+  }
+};
+
+const handleUploadSuccess = (response: any, uploadFile: UploadFile) => {
+  if (response && response.fileUrl) {
+    uploadFile.url = response.fileUrl;
+  }
+};
+
+const handleCancelReplace = () => {
+  if (fileList.value.length > 0) {
+    cancelConfirmVisible.value = true;
+  } else {
+    replaceDialogVisible.value = false;
+  }
+};
+
+const handleConfirmCancel = () => {
+  fileList.value = [];
+  cancelConfirmVisible.value = false;
+  replaceDialogVisible.value = false;
+};
+
+const handleReplaceDialogClose = () => {
+  if (fileList.value.length > 0) {
+    cancelConfirmVisible.value = true;
+  }
+};
+
+const handleSubmitReplace = async () => {
+  if (fileList.value.length === 0) {
+    ElMessage.warning("请至少上传一张图片");
+    return;
+  }
+  const uploadedFiles = fileList.value.filter(file => file.url);
+  if (uploadedFiles.length === 0) {
+    ElMessage.warning("请先上传图片");
+    return;
+  }
+  try {
+    // TODO: 调用API提交审核
+    // const urls = uploadedFiles.map(file => file.url);
+    // await submitContractReview(urls);
+    ElMessage.success("提交审核成功");
+    replaceDialogVisible.value = false;
+    fileList.value = [];
+    await initData();
+  } catch (error) {
+    ElMessage.error("提交审核失败");
+  }
+};
+
+const getStatusClass = (status: string) => {
+  return {
+    "status-pending": status === "pending",
+    "status-success": status === "success",
+    "status-failed": status === "failed"
+  };
+};
+
+const getStatusText = (status: string) => {
+  const map: Record<string, string> = {
+    pending: "审核中",
+    success: "审核通过",
+    failed: "审核拒绝"
+  };
+  return map[status] || "未知";
+};
+</script>
+
+<style lang="scss" scoped>
+.page-header {
+  margin-bottom: 20px;
+}
+.store-title {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+.content-section {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 30px;
+}
+.tip-text {
+  flex: 1;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--el-text-color-regular);
+}
+.action-buttons {
+  display: flex;
+  flex-shrink: 0;
+  gap: 10px;
+}
+.contract-container {
+  width: 100%;
+  padding: 20px;
+  background-color: var(--el-bg-color-page);
+  border-radius: 8px;
+}
+.contract-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  aspect-ratio: 1;
+  margin-bottom: 20px;
+  overflow: hidden;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+  border-radius: 8px;
+  .contract-image {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+  .image-slot {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    font-size: 30px;
+    color: var(--el-text-color-placeholder);
+    background: var(--el-fill-color-light);
+  }
+  &.empty-item {
+    background-color: var(--el-fill-color-lighter);
+    border: 1px dashed var(--el-border-color);
+    .empty-icon {
+      font-size: 48px;
+      color: var(--el-text-color-placeholder);
+    }
+  }
+}
+.replace-upload-area {
+  min-height: 300px;
+  padding: 20px 0;
+  .upload-tip {
+    margin-top: 10px;
+    font-size: 14px;
+    color: var(--el-text-color-secondary);
+    text-align: center;
+  }
+}
+.confirm-text {
+  padding: 10px 0;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--el-text-color-primary);
+}
+.change-record-content {
+  padding: 20px 0;
+  .record-date {
+    margin-bottom: 20px;
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .record-items {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 15px;
+    margin-bottom: 20px;
+  }
+  .record-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 150px;
+    height: 100px;
+    border: 1px solid var(--el-border-color-light);
+    border-radius: 8px;
+    .record-status-text {
+      font-size: 14px;
+      font-weight: 500;
+    }
+    &.status-pending {
+      color: #e6a23c;
+      background-color: #fdf6ec;
+      border-color: #e6a23c;
+    }
+    &.status-success {
+      color: #67c23a;
+      background-color: #f0f9ff;
+      border-color: #67c23a;
+    }
+    &.status-failed {
+      color: #f56c6c;
+      background-color: #fef0f0;
+      border-color: #f56c6c;
+    }
+  }
+  .rejection-reason {
+    padding: 15px;
+    margin-top: 20px;
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+    background-color: var(--el-fill-color-lighter);
+    border-radius: 8px;
+  }
+}
+</style>

+ 0 - 93
src/views/licenseManagement/detail.vue

@@ -1,93 +0,0 @@
-<template>
-  <div class="card content-box">
-    <el-form :model="formData" label-width="140px">
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="店铺名称 :">
-            <span>{{ formData.storeName }}</span>
-          </el-form-item>
-          <el-form-item label="名称 :">
-            <span>{{ formData.name }}</span>
-          </el-form-item>
-          <el-form-item label="描述 :">
-            <span>{{ formData.description }}</span>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="状态:">
-            <span>{{ getStatusName(formData.status) }}</span>
-          </el-form-item>
-          <el-form-item label="拒绝原因:" v-if="formData.status === '2'">
-            <span>{{ formData.rejectionReason }}</span>
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row class="text-center" style="margin-top: 20px">
-        <el-col :span="24">
-          <el-button type="primary" @click="goBack"> 确定 </el-button>
-        </el-col>
-      </el-row>
-    </el-form>
-  </div>
-</template>
-
-<script setup lang="tsx" name="licenseManagementDetail">
-import { ref, onMounted } from "vue";
-import { useRoute, useRouter } from "vue-router";
-import { ElMessage } from "element-plus";
-import { getStaffConfigDeatail } from "@/api/modules/staffConfig";
-
-const route = useRoute();
-const router = useRouter();
-
-const formData = ref({});
-
-const id = ref((route.query.id as string) || "");
-
-const getStatusName = (status: string) => {
-  switch (status) {
-    case "0":
-      return "待审核";
-    case "1":
-      return "审核通过";
-    case "2":
-      return "审核拒绝";
-    default:
-      return "未知状态";
-  }
-};
-
-onMounted(async () => {
-  await initData();
-});
-
-const initData = async () => {
-  if (id.value) {
-    try {
-      const response = await getStaffConfigDeatail({ id: id.value });
-      if (response.code === 200) {
-        formData.value = response.data;
-      }
-    } catch (error) {
-      ElMessage.error("获取详情失败");
-    }
-  }
-};
-
-const goBack = () => {
-  router.go(-1);
-};
-</script>
-
-<style lang="scss" scoped>
-.el-form {
-  width: 100%;
-  .text-center {
-    text-align: center;
-  }
-}
-.el-col {
-  box-sizing: border-box;
-  padding-right: 10px;
-}
-</style>

+ 393 - 0
src/views/licenseManagement/foodBusinessLicense.vue

@@ -0,0 +1,393 @@
+<template>
+  <div class="card content-box">
+    <div class="page-header">
+      <h1 class="store-title">重庆老火锅</h1>
+    </div>
+    <div class="content-section">
+      <div class="tip-text">
+        如需变更请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的食品经营许可证将会覆盖之前的食品经营许可证
+      </div>
+      <div class="action-buttons">
+        <el-button type="primary" @click="handleReplace"> 更换 </el-button>
+        <el-button type="primary" @click="handleViewChangeRecord"> 查看变更记录 </el-button>
+      </div>
+    </div>
+    <div class="license-container">
+      <div class="license-display">
+        <el-image
+          v-if="licenseImage"
+          :src="licenseImage"
+          fit="contain"
+          class="license-image"
+          :preview-src-list="[licenseImage]"
+        />
+        <div v-else class="empty-image-box">
+          <el-icon class="empty-icon">
+            <Picture />
+          </el-icon>
+        </div>
+      </div>
+    </div>
+
+    <!-- 更换食品经营许可证弹窗 -->
+    <el-dialog v-model="replaceDialogVisible" title="更换食品经营许可证" width="600px" @close="handleReplaceDialogClose">
+      <div class="replace-upload-area">
+        <el-upload
+          ref="uploadRef"
+          action="#"
+          :show-file-list="false"
+          :http-request="handleHttpUpload"
+          :before-upload="beforeUpload"
+          drag
+          class="upload-box"
+        >
+          <el-icon class="el-icon--upload">
+            <upload-filled />
+          </el-icon>
+          <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+          <template #tip>
+            <div class="upload-tip">({{ uploadedCount }}/1)</div>
+          </template>
+        </el-upload>
+        <div v-if="newLicenseUrl" class="preview-section">
+          <div class="preview-label">预览</div>
+          <el-image :src="newLicenseUrl" fit="contain" class="preview-image" />
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleCancelReplace"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmitReplace"> 去审核 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 取消确认弹窗 -->
+    <el-dialog v-model="cancelConfirmVisible" title="提示" width="400px" :close-on-click-modal="false">
+      <div class="confirm-text">确定要取消本次图片上传吗?已上传的图片将不保存</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelConfirmVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="handleConfirmCancel"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 变更记录弹窗 -->
+    <el-dialog v-model="changeRecordDialogVisible" title="变更记录" width="900px" :close-on-click-modal="false">
+      <div class="change-record-content">
+        <div class="record-date">
+          {{ currentRecordDate }}
+        </div>
+        <div class="record-items">
+          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item" :class="getStatusClass(item.status)">
+            <div class="record-status-text">
+              {{ getStatusText(item.status) }}
+            </div>
+          </div>
+        </div>
+        <div v-if="rejectionReason" class="rejection-reason">拒绝原因: {{ rejectionReason }}</div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="changeRecordDialogVisible = false"> 关闭 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="foodBusinessLicense">
+import { ref, computed, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import { Picture, UploadFilled } from "@element-plus/icons-vue";
+import { uploadImg } from "@/api/modules/upload";
+import type { UploadProps, UploadRequestOptions } from "element-plus";
+
+interface ChangeRecordItem {
+  id: string;
+  status: "pending" | "success" | "failed";
+  rejectionReason?: string;
+}
+
+const licenseImage = ref<string>("");
+const replaceDialogVisible = ref(false);
+const cancelConfirmVisible = ref(false);
+const changeRecordDialogVisible = ref(false);
+const uploadRef = ref();
+const newLicenseUrl = ref("");
+const currentRecordDate = ref("2025.08.01 10:29");
+const changeRecordList = ref<ChangeRecordItem[]>([]);
+const rejectionReason = ref("");
+
+const uploadedCount = computed(() => (newLicenseUrl.value ? 1 : 0));
+
+onMounted(async () => {
+  await initData();
+});
+
+const initData = async () => {
+  try {
+    // TODO: 调用API获取食品经营许可证图片
+    // const response = await getFoodBusinessLicense();
+    // if (response.code === 200) {
+    //   licenseImage.value = response.data.imageUrl;
+    // }
+  } catch (error) {
+    ElMessage.error("获取食品经营许可证失败");
+  }
+};
+
+const handleReplace = () => {
+  newLicenseUrl.value = "";
+  replaceDialogVisible.value = true;
+};
+
+const handleViewChangeRecord = async () => {
+  try {
+    // TODO: 调用API获取变更记录
+    // const response = await getChangeRecords();
+    // if (response.code === 200) {
+    //   changeRecordList.value = response.data.records;
+    //   currentRecordDate.value = response.data.date;
+    //   rejectionReason.value = response.data.rejectionReason || "";
+    // }
+    // 模拟数据 - 根据图片,可能是单个记录
+    changeRecordList.value = [{ id: "1", status: "pending" }];
+    rejectionReason.value = "";
+    changeRecordDialogVisible.value = true;
+  } catch (error) {
+    ElMessage.error("获取变更记录失败");
+  }
+};
+
+const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
+  const imgSize = rawFile.size / 1024 / 1024 < 10;
+  const imgType = ["image/jpeg", "image/png", "image/gif"].includes(rawFile.type);
+  if (!imgType) {
+    ElMessage.warning("上传图片不符合所需的格式!");
+    return false;
+  }
+  if (!imgSize) {
+    ElMessage.warning("上传图片大小不能超过 10M!");
+    return false;
+  }
+  return true;
+};
+
+const handleHttpUpload = async (options: UploadRequestOptions) => {
+  const formData = new FormData();
+  formData.append("file", options.file);
+  try {
+    const { data } = await uploadImg(formData);
+    newLicenseUrl.value = data.fileUrl ? data.fileUrl : data[0];
+    ElMessage.success("上传成功");
+  } catch (error) {
+    ElMessage.error("上传失败");
+  }
+};
+
+const handleCancelReplace = () => {
+  if (newLicenseUrl.value) {
+    cancelConfirmVisible.value = true;
+  } else {
+    replaceDialogVisible.value = false;
+  }
+};
+
+const handleConfirmCancel = () => {
+  newLicenseUrl.value = "";
+  cancelConfirmVisible.value = false;
+  replaceDialogVisible.value = false;
+};
+
+const handleReplaceDialogClose = () => {
+  if (newLicenseUrl.value) {
+    cancelConfirmVisible.value = true;
+  }
+};
+
+const handleSubmitReplace = async () => {
+  if (!newLicenseUrl.value) {
+    ElMessage.warning("请先上传图片");
+    return;
+  }
+  try {
+    // TODO: 调用API提交审核
+    // await submitFoodLicenseReview(newLicenseUrl.value);
+    ElMessage.success("提交审核成功");
+    replaceDialogVisible.value = false;
+    newLicenseUrl.value = "";
+    await initData();
+  } catch (error) {
+    ElMessage.error("提交审核失败");
+  }
+};
+
+const getStatusClass = (status: string) => {
+  return {
+    "status-pending": status === "pending",
+    "status-success": status === "success",
+    "status-failed": status === "failed"
+  };
+};
+
+const getStatusText = (status: string) => {
+  const map: Record<string, string> = {
+    pending: "审核中",
+    success: "审核通过",
+    failed: "审核拒绝"
+  };
+  return map[status] || "未知";
+};
+</script>
+
+<style lang="scss" scoped>
+.page-header {
+  margin-bottom: 20px;
+}
+.store-title {
+  margin: 0;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+.content-section {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 30px;
+}
+.tip-text {
+  flex: 1;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--el-text-color-regular);
+}
+.action-buttons {
+  display: flex;
+  flex-shrink: 0;
+  gap: 10px;
+}
+.license-container {
+  width: 100%;
+  min-height: 500px;
+  padding: 20px;
+  background-color: var(--el-bg-color-page);
+  border-radius: 8px;
+}
+.license-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  min-height: 500px;
+}
+.license-image {
+  max-width: 100%;
+  max-height: 600px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+}
+.empty-image-box {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 500px;
+  background-color: var(--el-fill-color-lighter);
+  border-radius: 8px;
+  .empty-icon {
+    font-size: 64px;
+    color: var(--el-text-color-placeholder);
+  }
+}
+.replace-upload-area {
+  min-height: 300px;
+  padding: 20px 0;
+  .upload-box {
+    width: 100%;
+    min-height: 200px;
+  }
+  .upload-tip {
+    margin-top: 10px;
+    font-size: 14px;
+    color: var(--el-text-color-secondary);
+    text-align: center;
+  }
+  .preview-section {
+    padding: 20px;
+    margin-top: 30px;
+    background-color: var(--el-fill-color-lighter);
+    border-radius: 8px;
+    .preview-label {
+      margin-bottom: 15px;
+      font-size: 14px;
+      color: var(--el-text-color-secondary);
+    }
+    .preview-image {
+      width: 100%;
+      max-height: 300px;
+    }
+  }
+}
+.confirm-text {
+  padding: 10px 0;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--el-text-color-primary);
+}
+.change-record-content {
+  padding: 20px 0;
+  .record-date {
+    margin-bottom: 20px;
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .record-items {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 15px;
+    margin-bottom: 20px;
+  }
+  .record-item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 150px;
+    height: 100px;
+    background-color: var(--el-fill-color-lighter);
+    border: 1px solid var(--el-border-color-light);
+    border-radius: 8px;
+    .record-status-text {
+      font-size: 14px;
+      font-weight: 500;
+    }
+    &.status-pending {
+      color: #e6a23c;
+      background-color: #fdf6ec;
+      border-color: #e6a23c;
+    }
+    &.status-success {
+      color: #67c23a;
+      background-color: #f0f9ff;
+      border-color: #67c23a;
+    }
+    &.status-failed {
+      color: #f56c6c;
+      background-color: #fef0f0;
+      border-color: #f56c6c;
+    }
+  }
+  .rejection-reason {
+    padding: 15px;
+    margin-top: 20px;
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+    background-color: var(--el-fill-color-lighter);
+    border-radius: 8px;
+  }
+}
+</style>

+ 0 - 203
src/views/licenseManagement/index.vue

@@ -1,203 +0,0 @@
-<template>
-  <div class="table-box">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
-      <!-- 表格 header 按钮 -->
-      <template #tableHeader="scope">
-        <el-button type="primary" :icon="Download" @click="exportInfoExcel(scope)"> 导出 </el-button>
-      </template>
-      <template #expand="scope">
-        {{ scope.row }}
-      </template>
-      <!-- 表格操作 -->
-      <template #operation="scope">
-        <!-- 审批通过和拒绝按钮仅在状态为0时显示 -->
-        <template v-if="scope.row.status === '0'">
-          <el-button type="primary" link @click="changeTypes(scope.row, 'pass')"> 审核通过 </el-button>
-          <el-button type="primary" link @click="changeTypes(scope.row, '')"> 审核拒绝 </el-button>
-        </template>
-        <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
-      </template>
-    </ProTable>
-
-    <el-dialog v-model="dialogFormVisible" title="审核拒绝" width="500">
-      <el-form :model="form">
-        <el-form-item label="" label-width="0">
-          <el-input v-model="form.comment" autocomplete="off" type="textarea" maxlength="200" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="closeDialog"> 取消 </el-button>
-          <el-button type="primary" @click="handleSubmit"> 驳回 </el-button>
-        </div>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="tsx" name="licenseManagement">
-import { ref, reactive, onMounted, onActivated } from "vue";
-import { useRouter } from "vue-router";
-import { ElMessage } from "element-plus";
-import ProTable from "@/components/ProTable/index.vue";
-import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { Download } from "@element-plus/icons-vue";
-import { audit, exportExcelStaffConfig, getStaffConfigList } from "@/api/modules/staffConfig";
-
-const router = useRouter();
-const dialogFormVisible = ref(false);
-const form = reactive({
-  comment: ""
-});
-
-const rowData = ref<any>();
-
-const statusEnum = [
-  { value: "0", label: "待审核" },
-  { value: "1", label: "审核通过" },
-  { value: "2", label: "审核拒绝" }
-];
-// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
-const initParam = reactive({});
-
-// 定义 filterValues
-const filterValues = reactive({});
-
-const getStatusObj = (statusValue: string) => {
-  const statusObj = statusEnum.find(item => item.value === statusValue);
-  if (statusObj) {
-    filterValues.status = statusObj;
-  } else {
-    filterValues.status = "";
-  }
-  return statusObj;
-};
-
-// ProTable 实例
-const proTable = ref<ProTableInstance>();
-
-// 页面加载时触发查询
-onMounted(() => {
-  proTable.value?.getTableList();
-});
-
-// 从其他页面返回时触发查询
-onActivated(() => {
-  proTable.value?.getTableList();
-});
-
-// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
-// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
-const dataCallback = (data: any) => {
-  return {
-    list: data.records,
-    total: data.total
-  };
-};
-
-// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
-// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
-const getTableList = (params: any) => {
-  let newParams = JSON.parse(JSON.stringify(params));
-  return getStaffConfigList(newParams);
-};
-
-// 跳转详情页
-const toDetail = row => {
-  router.push(`/store/licenseManagementDetail?id=${row.id}`);
-};
-
-// 表格配置项
-const columns = reactive<ColumnProps<any>[]>([
-  { type: "index", fixed: "left", label: "序号", width: 130 },
-  { prop: "storeName", label: "所属店铺" },
-  { prop: "name", label: "名称" },
-  { prop: "description", label: "描述" },
-  {
-    prop: "status",
-    label: "状态",
-    render: scope => {
-      const statusObj = getStatusObj(scope.row.status);
-      return statusObj ? statusObj.label : "未知状态";
-    },
-    search: {
-      el: "select"
-    },
-    enum: statusEnum,
-    fieldNames: { label: "label", value: "value" }
-  },
-  { prop: "operation", label: "操作", fixed: "right", width: 330 }
-]);
-
-const changeTypes = (row: any, status: string) => {
-  rowData.value = row;
-  if (status === "pass") {
-    handleChangeStatus(row, "1");
-  } else {
-    form.comment = "";
-    dialogFormVisible.value = true;
-  }
-};
-
-const handleChangeStatus = async (row: any, status: string) => {
-  try {
-    let res = await audit({ id: row.id, status: status, rejectionReason: form.comment });
-    if (res.code === 200) {
-      proTable.value?.getTableList();
-      if (status === "2") closeDialog();
-      ElMessage.success("审核成功");
-    }
-  } catch (error) {
-    ElMessage.error("操作失败");
-  }
-};
-
-// 导出信息
-const exportInfoExcel = async scope => {
-  let res;
-  // 获取原始状态值(可能为数字、字符串或 undefined)
-  const rawStatus = proTable.value.searchParam.status;
-  // 转换为字符串(处理 undefined/null 为 "" 或保留原始字符串)
-  const statusParam = rawStatus !== undefined && rawStatus !== null ? String(rawStatus) : undefined;
-  // 将筛选条件作为参数传递给后台
-  res = await exportExcelStaffConfig({ status: statusParam });
-  if (res.code === 200) {
-    if (!res.data) {
-      ElMessage.error("暂无可下载数据");
-      return;
-    }
-    const exportFile = document.createElement("a");
-    exportFile.style.display = "none";
-    exportFile.download = `证照管理.xlsx`;
-    exportFile.href = `${res.data}?timestamp=${new Date().getTime()}`; // 添加时间戳防止缓存
-    document.body.appendChild(exportFile);
-    exportFile.click();
-    document.body.removeChild(exportFile);
-    ElMessage.success("下载成功");
-  }
-};
-
-// 弹窗提交
-const handleSubmit = () => {
-  if (!form.comment) {
-    ElMessage.error("请输入审批意见");
-    return;
-  }
-  handleChangeStatus(rowData.value, "2");
-};
-// 关闭弹窗;
-const closeDialog = () => {
-  dialogFormVisible.value = false;
-  form.comment = "";
-};
-</script>
-
-<style lang="scss" scoped>
-// 在组件样式中添加
-.date-range {
-  display: block; // 确保换行生效
-  padding: 0 8px; // 可选:增加内边距
-  word-wrap: break-word; // 长单词内换行
-  white-space: normal; // 允许自然换行
-}
-</style>