Quellcode durchsuchen

feat(licenseManagement): 新增娱乐经营许可证管理功能

- 在娱乐经营许可证页面添加到期时间展示
- 更新登录后权限跳转逻辑,支持娱乐经营许可证权限判断
- 扩展权限检查工具函数,新增娱乐经营许可证权限校验
- 调整菜单权限提示信息,支持多证照到期提醒
- 优化证照页面状态码判断逻辑,统一使用 == 判断
- 更新运营管理菜单配置,设置为可见状态
- 新增运营活动详情页面,支持活动信息展示与图片预览
congxuesong vor 2 Wochen
Ursprung
Commit
d457a99362

+ 1 - 1
src/assets/json/authMenuList.json

@@ -518,7 +518,7 @@
         "icon": "Operation",
         "title": "运营管理",
         "isLink": "",
-        "isHide": true,
+        "isHide": false,
         "isFull": false,
         "isAffix": false,
         "isKeepAlive": false

+ 43 - 26
src/utils/permission.ts

@@ -51,11 +51,12 @@ export async function usePermission(tip?: string) {
 
 /**
  * @description 检查菜单访问权限(新方法)
- * @returns {Object} 返回合同管理和食品经营许可证的权限状态
+ * @returns {Object} 返回合同管理、食品经营许可证、娱乐经营许可证的权限状态
  */
 export async function checkMenuAccessPermission(): Promise<{
   contractManagement: boolean;
   foodBusinessLicense: boolean;
+  entertainmentBusinessLicense: boolean;
 }> {
   try {
     // 调用单个API,返回两个字段
@@ -79,23 +80,33 @@ export async function checkMenuAccessPermission(): Promise<{
         foodPermission = value == "0";
       }
 
+      // 解析娱乐经营许可证权限
+      let entertainmentPermission = false;
+      if (data.entertainmentLicenceExpirationTime !== undefined) {
+        const value = data.entertainmentLicenceExpirationTime;
+        entertainmentPermission = value == "0";
+      }
+
       return {
         contractManagement: contractPermission,
-        foodBusinessLicense: foodPermission
+        foodBusinessLicense: foodPermission,
+        entertainmentBusinessLicense: entertainmentPermission
       };
     }
 
-    // 如果API调用失败或返回格式不正确,默认返回两个都为false(不做限制)
+    // 如果API调用失败或返回格式不正确,默认返回都为 false(不做限制)
     return {
       contractManagement: false,
-      foodBusinessLicense: false
+      foodBusinessLicense: false,
+      entertainmentBusinessLicense: false
     };
   } catch (error) {
     console.error("检查菜单权限失败:", error);
-    // 如果API调用失败,默认返回两个都为false(不做限制)
+    // 如果API调用失败,默认返回都为 false(不做限制)
     return {
       contractManagement: false,
-      foodBusinessLicense: false
+      foodBusinessLicense: false,
+      entertainmentBusinessLicense: false
     };
   }
 }
@@ -103,31 +114,34 @@ export async function checkMenuAccessPermission(): Promise<{
 /**
  * @description 检查菜单项是否可以点击
  * @param {string} path 菜单路径
- * @returns {Promise<Object>} 返回包含canClick、contractManagement、foodBusinessLicense的对象
+ * @returns {Promise<Object>} 返回包含 canClick、contractManagement、foodBusinessLicense、entertainmentBusinessLicense 的对象
  */
 export async function checkMenuClickPermission(path?: string): Promise<{
   canClick: boolean;
   contractManagement: boolean;
   foodBusinessLicense: boolean;
+  entertainmentBusinessLicense: boolean;
 }> {
   // 页面路径常量
   const CONTRACT_MANAGEMENT_PATH = "/licenseManagement/contractManagement"; // 合同管理
   const FOOD_BUSINESS_LICENSE_PATH = "/licenseManagement/foodBusinessLicense"; // 食品经营许可证
+  const ENTERTAINMENT_LICENSE_PATH = "/licenseManagement/entertainmentLicense"; // 娱乐经营许可证
 
   // 调用权限检查方法,获取两个权限状态
   const permissions = await checkMenuAccessPermission();
-  const { contractManagement, foodBusinessLicense } = permissions;
+  const { contractManagement, foodBusinessLicense, entertainmentBusinessLicense } = permissions;
 
-  // 如果两者都为false,不做限制,所有页面都可以点击
-  if (!contractManagement && !foodBusinessLicense) {
+  // 如果全部为 false,不做限制,所有页面都可以点击
+  if (!contractManagement && !foodBusinessLicense && !entertainmentBusinessLicense) {
     return {
       canClick: true,
       contractManagement: false,
-      foodBusinessLicense: false
+      foodBusinessLicense: false,
+      entertainmentBusinessLicense: false
     };
   }
 
-  // 如果至少有一个为true,需要检查权限
+  // 如果至少有一个为 true,需要检查权限
   // 构建允许访问的路径列表
   const allowedPaths: string[] = [];
   if (contractManagement) {
@@ -136,6 +150,9 @@ export async function checkMenuClickPermission(path?: string): Promise<{
   if (foodBusinessLicense) {
     allowedPaths.push(FOOD_BUSINESS_LICENSE_PATH);
   }
+  if (entertainmentBusinessLicense) {
+    allowedPaths.push(ENTERTAINMENT_LICENSE_PATH);
+  }
 
   const canClick = true;
   // 检查当前路径是否在允许访问的列表中
@@ -144,21 +161,20 @@ export async function checkMenuClickPermission(path?: string): Promise<{
 
     // 如果不可点击,根据权限状态显示相应的提示信息
     if (!canClick) {
-      let message = "";
-
-      // 如果两者都为true,显示两行提示
-      if (contractManagement && foodBusinessLicense) {
-        message = "合同已到期,请上传最新合同。\n经营许可证已到期,请上传最新许可证。";
-      } else if (contractManagement) {
-        // 只有合同管理为true
-        message = "合同已到期,请上传最新合同。";
-      } else if (foodBusinessLicense) {
-        // 只有食品经营许可证为true
-        message = "经营许可证已到期,请上传最新许可证。";
+      const messages: string[] = [];
+
+      if (contractManagement) {
+        messages.push("合同已到期,请上传最新合同。");
+      }
+      if (foodBusinessLicense) {
+        messages.push("食品经营许可证已到期,请上传最新许可证。");
+      }
+      if (entertainmentBusinessLicense) {
+        messages.push("娱乐经营许可证已到期,请上传最新许可证。");
       }
 
-      if (message) {
-        ElMessage.warning(message);
+      if (messages.length) {
+        ElMessage.warning(messages.join("\n"));
       }
     }
   }
@@ -166,6 +182,7 @@ export async function checkMenuClickPermission(path?: string): Promise<{
   return {
     canClick,
     contractManagement,
-    foodBusinessLicense
+    foodBusinessLicense,
+    entertainmentBusinessLicense
   };
 }

+ 1 - 1
src/views/licenseManagement/businessLicense.vue

@@ -40,7 +40,7 @@ const initData = async () => {
     id: id
   };
   const res: any = await getBusinessLicense(params);
-  if (res.code === 200) {
+  if (res.code == 200) {
     licenseImage.value = res.data[0].imgUrl;
   }
 };

+ 1 - 1
src/views/licenseManagement/contractManagement.vue

@@ -201,7 +201,7 @@ const initData = async () => {
     id: id
   };
   const res: any = await getContractImages(params);
-  if (res.code === 200) {
+  if (res.code == 200) {
     contractList.value = res.data;
   }
 };

+ 4 - 5
src/views/licenseManagement/entertainmentLicense.vue

@@ -1,9 +1,7 @@
 <template>
   <div class="card content-box">
     <div class="content-section">
-      <div class="tip-text">
-        如需变更请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的娱乐经营许可证将会覆盖之前的娱乐经营许可证
-      </div>
+      <div class="tip-text">娱乐经营许可证到期时间:{{ entertainmentLicenseExpireTime }}</div>
       <div class="action-buttons">
         <el-button type="primary" @click="handleReplace"> 更换 </el-button>
         <el-button type="primary" @click="handleViewChangeRecord"> 查看变更记录 </el-button>
@@ -134,7 +132,7 @@ const statusMap: Record<number, { name: string; class: string }> = {
 };
 
 const id = localGet("createdId");
-
+const entertainmentLicenseExpireTime = ref<string>("");
 const licenseImage = ref<string>("");
 const replaceDialogVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
@@ -180,8 +178,9 @@ const initData = async () => {
     id: id
   };
   const res: any = await getEntertainmentBusinessLicense(params);
-  if (res.code === 200) {
+  if (res.code == 200) {
     licenseImage.value = res.data[0].imgUrl;
+    entertainmentLicenseExpireTime.value = res.data[0].expireTime || 11;
   }
 };
 

+ 1 - 1
src/views/licenseManagement/foodBusinessLicense.vue

@@ -180,7 +180,7 @@ const initData = async () => {
     id: id
   };
   const res: any = await getFoodBusinessLicense(params);
-  if (res.code === 200) {
+  if (res.code == 200) {
     licenseImage.value = res.data[0].imgUrl;
   }
 };

+ 0 - 519
src/views/licenseManagement/window.vue

@@ -1,519 +0,0 @@
-<template>
-  <!-- 更换合同弹窗 -->
-  <el-dialog
-    v-model="replaceDialogVisible"
-    title="更换合同"
-    width="860px"
-    :before-close="handleReplaceDialogClose"
-    :close-on-click-modal="false"
-    :close-on-press-escape="false"
-  >
-    <el-scrollbar height="400px" class="replace-upload-scrollbar">
-      <div class="replace-upload-area" :class="{ 'upload-full': uploadedImageCount >= uploadMaxCount }">
-        <el-upload
-          v-model:file-list="fileList"
-          list-type="picture-card"
-          :accept="'.jpg,.png'"
-          :limit="uploadMaxCount"
-          :auto-upload="false"
-          :disabled="hasUnuploadedImages"
-          multiple
-          :on-change="handleUploadChange"
-          :on-exceed="handleUploadExceed"
-          :on-preview="handlePictureCardPreview"
-          :before-remove="handleBeforeRemove"
-          :on-remove="handleRemove"
-          :show-file-list="true"
-        >
-          <template #trigger>
-            <div v-if="uploadedImageCount < uploadMaxCount" class="upload-trigger-card el-upload--picture-card">
-              <el-icon>
-                <Plus />
-              </el-icon>
-              <div class="upload-tip">({{ uploadedImageCount }}/{{ uploadMaxCount }})</div>
-            </div>
-          </template>
-        </el-upload>
-      </div>
-    </el-scrollbar>
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button @click="handleCancelReplace" :disabled="hasUnuploadedImages"> 取消 </el-button>
-        <el-button type="primary" @click="handleSubmitReplace" :disabled="hasUnuploadedImages"> 去审核 </el-button>
-      </div>
-    </template>
-  </el-dialog>
-
-  <!-- 图片预览 -->
-  <el-image-viewer
-    v-if="imageViewerVisible"
-    :url-list="imageViewerUrlList"
-    :initial-index="imageViewerInitialIndex"
-    @close="imageViewerVisible = false"
-  />
-</template>
-
-<script setup lang="ts" name="contractManagementwindow">
-import { ref, computed } from "vue";
-import { ElMessage, ElMessageBox } from "element-plus";
-import { Plus } from "@element-plus/icons-vue";
-import type { UploadProps, UploadFile } from "element-plus";
-import { localGet } from "@/utils";
-import { uploadContractImage, submitContractReview, getStoreContractStatus } from "@/api/modules/licenseManagement";
-
-const replaceDialogVisible = ref(false);
-const fileList = ref<UploadFile[]>([]);
-
-// ==================== 图片上传相关变量 ====================
-const uploadMaxCount = 20;
-const uploading = ref(false);
-const pendingUploadFiles = ref<UploadFile[]>([]);
-const imageUrlList = ref<string[]>([]); // 存储图片URL列表
-
-// 图片预览相关
-const imageViewerVisible = ref(false);
-const imageViewerUrlList = ref<string[]>([]);
-const imageViewerInitialIndex = ref(0);
-
-// 计算属性:获取已成功上传的图片数量
-const uploadedImageCount = computed(() => {
-  return fileList.value.filter((file: any) => file.status === "success" && file.url).length;
-});
-
-// 计算属性:检查是否有未上传完成的图片
-const hasUnuploadedImages = computed(() => {
-  // 检查是否有正在上传的文件
-  if (uploading.value || pendingUploadFiles.value.length > 0) {
-    return true;
-  }
-  // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
-  if (fileList.value && fileList.value.length > 0) {
-    return fileList.value.some((file: any) => {
-      return file.status === "ready" || file.status === "uploading";
-    });
-  }
-  return false;
-});
-
-const handleReplace = async () => {
-  fileList.value = [];
-  imageUrlList.value = [];
-  pendingUploadFiles.value = [];
-  uploading.value = false;
-  const params = {
-    id: localGet("createdId")
-  };
-  const res: any = await getStoreContractStatus(params);
-  if (res.data.renewContractStatus === 2) {
-    ElMessage.warning("合同审核中,请耐心等待");
-  } else {
-    replaceDialogVisible.value = true;
-  }
-};
-
-/**
- * 检查文件是否在排队中(未上传)
- * @param file 文件对象
- * @returns 是否在排队中
- */
-const isFilePending = (file: any): boolean => {
-  // 只检查 ready 状态(排队中),不包括 uploading(正在上传)
-  if (file.status === "ready") {
-    return true;
-  }
-  // 检查是否在待上传队列中
-  if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
-    return true;
-  }
-  return false;
-};
-
-/**
- * 图片上传 - 删除前确认
- * @param uploadFile 要删除的文件对象
- * @param uploadFiles 当前文件列表
- * @returns Promise<boolean>,true 允许删除,false 阻止删除
- */
-const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
-  // 如果文件在排队中(未上传),禁止删除
-  if (isFilePending(uploadFile)) {
-    ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
-    return false;
-  }
-  try {
-    await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
-      confirmButtonText: "确定",
-      cancelButtonText: "取消",
-      type: "warning"
-    });
-    // 用户确认删除,返回 true 允许删除
-    return true;
-  } catch {
-    // 用户取消删除,返回 false 阻止删除
-    return false;
-  }
-};
-
-/**
- * 图片上传 - 移除图片回调(删除成功后调用)
- * @param uploadFile 已删除的文件对象
- * @param uploadFiles 删除后的文件列表
- */
-const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
-  // 从被删除的文件对象中获取 url
-  const file = uploadFile as any;
-  const imageUrl = file.url;
-
-  if (imageUrl) {
-    // 从 imageUrl 数组中删除对应的 URL
-    const urlIndex = imageUrlList.value.indexOf(imageUrl);
-    if (urlIndex > -1) {
-      imageUrlList.value.splice(urlIndex, 1);
-    }
-  }
-
-  if (file.url && file.url.startsWith("blob:")) {
-    URL.revokeObjectURL(file.url);
-  }
-  // 同步文件列表
-  fileList.value = [...uploadFiles];
-  // 删除成功后提示
-  ElMessage.success("图片已删除");
-};
-
-/**
- * 上传文件超出限制提示
- */
-const handleUploadExceed: UploadProps["onExceed"] = () => {
-  ElMessage.warning(`最多只能上传${uploadMaxCount}张图片`);
-};
-
-/**
- * el-upload 文件变更(选中或移除)
- */
-const handleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
-  // 检查文件类型,只允许 jpg 和 png
-  if (uploadFile.raw) {
-    const fileType = uploadFile.raw.type.toLowerCase();
-    const fileName = uploadFile.name.toLowerCase();
-    const validTypes = ["image/jpeg", "image/jpg", "image/png"];
-    const validExtensions = [".jpg", ".jpeg", ".png"];
-
-    // 检查 MIME 类型或文件扩展名
-    const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
-
-    if (!isValidType) {
-      // 从文件列表中移除不符合类型的文件
-      const index = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
-      if (index > -1) {
-        fileList.value.splice(index, 1);
-      }
-      // 从 uploadFiles 中移除
-      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
-      if (uploadIndex > -1) {
-        uploadFiles.splice(uploadIndex, 1);
-      }
-      // 如果文件有 blob URL,释放它
-      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
-        URL.revokeObjectURL(uploadFile.url);
-      }
-      ElMessage.warning("只支持上传 JPG 和 PNG 格式的图片");
-      return;
-    }
-  }
-
-  // 同步文件列表到表单数据(只添加通过验证的文件)
-  const existingIndex = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
-  if (existingIndex === -1) {
-    fileList.value.push(uploadFile);
-  }
-
-  const readyFiles = fileList.value.filter(file => file.status === "ready");
-  if (readyFiles.length) {
-    readyFiles.forEach(file => {
-      if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
-        pendingUploadFiles.value.push(file);
-      }
-    });
-  }
-  processUploadQueue();
-};
-
-/**
- * 处理上传队列 - 逐个上传文件
- */
-const processUploadQueue = async () => {
-  if (uploading.value || pendingUploadFiles.value.length === 0) {
-    return;
-  }
-  // 每次只取一个文件进行上传
-  const file = pendingUploadFiles.value.shift();
-  if (file) {
-    await uploadSingleFile(file);
-    // 继续处理队列中的下一个文件
-    processUploadQueue();
-  }
-};
-
-/**
- * 单文件上传图片
- * @param file 待上传的文件
- */
-const uploadSingleFile = async (file: UploadFile) => {
-  if (!file.raw) {
-    return;
-  }
-  const rawFile = file.raw as File;
-  const formData = new FormData();
-  formData.append("file", rawFile);
-  formData.append("user", "text");
-  file.status = "uploading";
-  file.percentage = 0;
-  uploading.value = true;
-
-  try {
-    // 上传过程中先保持进度为 0,避免接口异常时进度条误显示 100%
-    const result: any = await uploadContractImage(formData);
-    if (result?.code === 200 && result.data) {
-      // 处理单个文件的上传结果
-      let imageUrl = result.data[0];
-      if (!imageUrl) {
-        throw new Error("上传成功但未获取到图片URL");
-      }
-
-      file.status = "success";
-      file.percentage = 100;
-      // 保存图片URL到文件对象
-      file.url = imageUrl;
-      file.response = { url: imageUrl };
-
-      // 保存图片URL
-      if (!Array.isArray(imageUrlList.value)) {
-        imageUrlList.value = [];
-      }
-      if (!imageUrlList.value.includes(imageUrl)) {
-        imageUrlList.value.push(imageUrl);
-      }
-    } else {
-      throw new Error(result?.msg || "图片上传失败");
-    }
-  } catch (error: any) {
-    // 上传失败时保持进度条为 0
-    file.percentage = 0;
-    file.status = "fail";
-    if (file.url && file.url.startsWith("blob:")) {
-      URL.revokeObjectURL(file.url);
-    }
-    // 从文件列表中移除失败的文件
-    const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
-    if (index > -1) {
-      fileList.value.splice(index, 1);
-    }
-  } finally {
-    uploading.value = false;
-    // 触发视图更新
-    fileList.value = [...fileList.value];
-  }
-};
-
-/**
- * 图片预览 - 使用 el-image-viewer 预览功能
- * @param file 上传文件对象
- */
-const handlePictureCardPreview = (file: any) => {
-  // 如果文件在排队中(未上传),禁止预览
-  if (isFilePending(file)) {
-    ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
-    return;
-  }
-  // 如果文件正在上传中,允许预览(使用本地预览)
-  if (file.status === "uploading" && file.url) {
-    imageViewerUrlList.value = [file.url];
-    imageViewerInitialIndex.value = 0;
-    imageViewerVisible.value = true;
-    return;
-  }
-  // 获取所有图片的 URL 列表(只包含已上传成功的图片)
-  const urlList = fileList.value
-    .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
-    .map((item: any) => item.url || item.response?.data);
-  // 找到当前点击的图片索引
-  const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
-  if (currentIndex < 0) {
-    ElMessage.warning("图片尚未上传完成,无法预览");
-    return;
-  }
-  imageViewerUrlList.value = urlList;
-  imageViewerInitialIndex.value = currentIndex;
-  imageViewerVisible.value = true;
-};
-
-const handleCancelReplace = async () => {
-  // 如果有图片正在上传,阻止关闭
-  if (hasUnuploadedImages.value) {
-    ElMessage.warning("请等待图片上传完成后再关闭");
-    return;
-  }
-  if (fileList.value.length > 0) {
-    try {
-      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      });
-      // 用户确认取消
-      fileList.value = [];
-      imageUrlList.value = [];
-      pendingUploadFiles.value = [];
-      uploading.value = false;
-      replaceDialogVisible.value = false;
-    } catch {
-      // 用户取消操作,不做任何处理
-    }
-  } else {
-    replaceDialogVisible.value = false;
-  }
-};
-
-const handleReplaceDialogClose = async (done: () => void) => {
-  // 如果有图片正在上传,阻止关闭
-  if (hasUnuploadedImages.value) {
-    ElMessage.warning("请等待图片上传完成后再关闭");
-    return; // 不调用 done(),阻止关闭弹窗
-  }
-  if (fileList.value.length > 0) {
-    try {
-      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      });
-      // 用户确认取消,清空数据并关闭弹窗
-      fileList.value = [];
-      imageUrlList.value = [];
-      pendingUploadFiles.value = [];
-      uploading.value = false;
-      done(); // 调用 done() 允许关闭弹窗
-    } catch {
-      // 用户取消操作,不调用 done(),阻止关闭弹窗
-    }
-  } else {
-    // 没有文件,直接关闭
-    done();
-  }
-};
-
-const handleSubmitReplace = async () => {
-  // 检查是否有未上传完成的图片
-  if (hasUnuploadedImages.value) {
-    ElMessage.warning("请等待图片上传完成后再提交");
-    return;
-  }
-  if (fileList.value.length === 0) {
-    ElMessage.warning("请至少上传一张图片");
-    return;
-  }
-  const uploadedFiles = fileList.value.filter(file => file.status === "success");
-  if (uploadedFiles.length === 0) {
-    ElMessage.warning("请先上传图片");
-    return;
-  }
-  try {
-    // 根据文件列表顺序,生成带排序的图片数据(排序从0开始)
-    const imageDataWithSort = uploadedFiles.map((file, index) => ({
-      imgUrl: file.url,
-      imgSort: index,
-      storeId: localGet("createdId")
-    }));
-    await submitContractReview(imageDataWithSort);
-    ElMessage.success("提交审核成功");
-    replaceDialogVisible.value = false;
-    fileList.value = [];
-    imageUrlList.value = [];
-    pendingUploadFiles.value = [];
-    uploading.value = false;
-  } catch (error) {
-    ElMessage.error("提交审核失败");
-  }
-};
-</script>
-
-<style lang="scss" scoped>
-.replace-upload-scrollbar {
-  :deep(.el-scrollbar__wrap) {
-    overflow-x: hidden;
-  }
-}
-.replace-upload-area {
-  min-height: 300px;
-  padding: 20px;
-  :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
-    display: inline-flex !important;
-    opacity: 1 !important;
-  }
-  :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
-    display: inline-flex !important;
-    opacity: 1 !important;
-  }
-  :deep(.el-upload-list--picture-card .el-icon--close-tip) {
-    display: none !important;
-  }
-  &.upload-full {
-    :deep(.el-upload--picture-card) {
-      display: none !important;
-    }
-  }
-}
-.dialog-footer {
-  display: flex;
-  gap: 10px;
-  justify-content: center;
-}
-
-/* el-upload 图片预览铺满容器 */
-:deep(.el-upload-list--picture-card) {
-  .el-upload-list__item {
-    overflow: hidden;
-    .el-upload-list__item-thumbnail {
-      width: 100%;
-      height: 100%;
-      object-fit: fill;
-    }
-  }
-
-  /* 排队中(未上传)的图片禁用样式 */
-  .el-upload-list__item[data-status="ready"],
-  .el-upload-list__item.is-ready {
-    position: relative;
-    pointer-events: none;
-    cursor: not-allowed;
-    opacity: 0.6;
-    &::after {
-      position: absolute;
-      inset: 0;
-      z-index: 1;
-      content: "";
-      background-color: rgb(0 0 0 / 30%);
-    }
-    .el-upload-list__item-actions {
-      pointer-events: none;
-      opacity: 0.5;
-    }
-  }
-}
-.upload-trigger-card {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  font-size: 28px;
-  color: #8c939d;
-  .upload-tip {
-    margin-top: 8px;
-    font-size: 14px;
-    color: #8c939d;
-  }
-}
-</style>

+ 7 - 4
src/views/login/index.vue

@@ -939,14 +939,17 @@ const handleLogin = async () => {
         // 4.等待路由完全初始化后再跳转
         // 使用 nextTick 确保路由已完全添加
         await nextTick();
-        const { contractManagement, foodBusinessLicense } = await checkMenuClickPermission();
-        // 可以使用所有三个值
-        if ((contractManagement && foodBusinessLicense) || contractManagement) {
-          // 合同管理权限
+        const { contractManagement, foodBusinessLicense, entertainmentBusinessLicense } = await checkMenuClickPermission();
+        // 登录后根据不同证照权限跳转
+        if ((contractManagement && foodBusinessLicense && entertainmentBusinessLicense) || contractManagement) {
+          // 合同管理权限优先
           router.replace("/licenseManagement/contractManagement");
         } else if (foodBusinessLicense) {
           // 食品经营许可证权限
           router.replace("/licenseManagement/foodBusinessLicense");
+        } else if (entertainmentBusinessLicense) {
+          // 娱乐经营许可证权限
+          router.replace("/licenseManagement/entertainmentLicense");
         } else {
           // 5.跳转到首页,使用 replace 避免历史记录问题
           await router.replace(HOME_URL);

+ 462 - 0
src/views/operationManagement/activityDetail.vue

@@ -0,0 +1,462 @@
+<template>
+  <!-- 运营活动管理 - 详情页面 -->
+  <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
+    <div class="header">
+      <el-button @click="goBack"> 返回 </el-button>
+      <h2 class="title">运营活动详情</h2>
+    </div>
+    <div class="content">
+      <!-- 左侧内容区域 -->
+      <div class="contentLeft">
+        <!-- 基础信息模块 -->
+        <div class="model">
+          <h3 style="font-weight: bold">基础信息:</h3>
+          <!-- 活动名称 -->
+          <div class="detail-item">
+            <div class="detail-label">活动名称</div>
+            <div class="detail-value">
+              {{ activityModel.activityName || "--" }}
+            </div>
+          </div>
+          <!-- 活动时间 -->
+          <div class="detail-item">
+            <div class="detail-label">活动时间</div>
+            <div class="detail-value">
+              <span v-if="activityModel.startTime && activityModel.endTime">
+                {{ formatDate(activityModel.startTime) }} - {{ formatDate(activityModel.endTime) }}
+              </span>
+              <span v-else>--</span>
+            </div>
+          </div>
+          <!-- 用户可参与次数 -->
+          <div class="detail-item">
+            <div class="detail-label">用户可参与次数</div>
+            <div class="detail-value">
+              {{ activityModel.participationLimit || "--" }}
+            </div>
+          </div>
+          <!-- 活动规则 -->
+          <div class="detail-item">
+            <div class="detail-label">活动规则</div>
+            <div class="detail-value">
+              {{ getRuleDisplayText(activityModel.activityRule) || "--" }}
+            </div>
+          </div>
+          <!-- 优惠券名称 -->
+          <div class="detail-item">
+            <div class="detail-label">优惠券</div>
+            <div class="detail-value">
+              {{ activityModel.couponName || "--" }}
+            </div>
+          </div>
+          <!-- 优惠券发放数量 -->
+          <div class="detail-item">
+            <div class="detail-label">优惠券发放数量</div>
+            <div class="detail-value">
+              {{ activityModel.couponQuantity || "--" }}
+            </div>
+          </div>
+          <!-- 状态 -->
+          <div class="detail-item">
+            <div class="detail-label">状态</div>
+            <div class="detail-value">
+              {{ getStatusLabel(activityModel.status) }}
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- 右侧内容区域 -->
+      <div class="contentRight">
+        <!-- 活动宣传图模块 -->
+        <div class="model">
+          <div class="promotion-images-container">
+            <!-- 活动标题图 -->
+            <div class="promotion-image-item">
+              <h3 style="font-weight: bold">活动标题图:</h3>
+              <div class="image-container">
+                <div v-if="getImageUrl(activityModel.activityTitleImgUrl)" class="image-item">
+                  <el-image
+                    :src="getImageUrl(activityModel.activityTitleImgUrl)"
+                    fit=""
+                    class="promotion-image"
+                    :preview-src-list="getPreviewImageList()"
+                    :initial-index="0"
+                  >
+                    <template #error>
+                      <div class="image-slot">
+                        <el-icon><Picture /></el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                </div>
+                <div v-else class="empty-text">--</div>
+              </div>
+            </div>
+            <!-- 活动详情图 -->
+            <div class="promotion-image-item">
+              <h3 style="font-weight: bold">活动详情图:</h3>
+              <div class="image-container">
+                <div v-if="getImageUrl(activityModel.activityDetailImgUrl)" class="image-item">
+                  <el-image
+                    :src="getImageUrl(activityModel.activityDetailImgUrl)"
+                    fit=""
+                    class="promotion-image"
+                    :preview-src-list="getPreviewImageList()"
+                    :initial-index="getImageUrl(activityModel.activityTitleImgUrl) ? 1 : 0"
+                  >
+                    <template #error>
+                      <div class="image-slot">
+                        <el-icon><Picture /></el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                </div>
+                <div v-else class="empty-text">--</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="activityDetail">
+/**
+ * 运营活动管理 - 详情页面
+ * 功能:显示运营活动的详细信息
+ */
+import { ref, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { ElMessage } from "element-plus";
+import { Picture } from "@element-plus/icons-vue";
+import { getActivityDetail } from "@/api/modules/operationManagement";
+
+// ==================== 响应式数据定义 ====================
+
+// 路由相关
+const router = useRouter();
+const route = useRoute();
+
+// 页面ID参数
+const id = ref<string>("");
+
+// ==================== 活动信息数据模型 ====================
+const activityModel = ref<any>({
+  // 活动名称
+  activityName: "",
+  // 活动开始时间
+  startTime: "",
+  // 活动结束时间
+  endTime: "",
+  // 用户可参与次数
+  participationLimit: 0,
+  // 活动规则
+  activityRule: "",
+  // 优惠券ID
+  couponId: "",
+  // 优惠券名称
+  couponName: "",
+  // 优惠券发放数量
+  couponQuantity: 0,
+  // 活动标题图片
+  activityTitleImgUrl: null,
+  // 活动详情图片
+  activityDetailImgUrl: null,
+  // 状态
+  status: 0
+});
+
+// ==================== 生命周期钩子 ====================
+
+/**
+ * 组件挂载时初始化
+ * 从路由参数中获取ID并加载详情数据
+ */
+onMounted(async () => {
+  id.value = (route.query.id as string) || "";
+  if (id.value) {
+    await loadDetailData();
+  } else {
+    ElMessage.warning("缺少活动ID参数");
+  }
+});
+
+// ==================== 事件处理函数 ====================
+
+/**
+ * 返回上一页
+ */
+const goBack = () => {
+  router.go(-1);
+};
+
+// ==================== 数据加载函数 ====================
+
+/**
+ * 加载详情数据
+ */
+const loadDetailData = async () => {
+  try {
+    const res: any = await getActivityDetail({ id: id.value });
+    if (res && res.code == 200) {
+      // 合并主数据
+      activityModel.value = { ...activityModel.value, ...res.data };
+    } else {
+      ElMessage.error("加载详情数据失败");
+    }
+  } catch (error) {
+    console.error("加载详情数据出错:", error);
+    ElMessage.error("加载详情数据出错");
+  }
+};
+
+// ==================== 工具函数 ====================
+
+/**
+ * 格式化日期
+ * @param date 日期字符串 (YYYY-MM-DD)
+ * @returns 格式化后的日期字符串 (YYYY/MM/DD)
+ */
+const formatDate = (date: string) => {
+  if (!date) return "--";
+  return date.replace(/-/g, "/");
+};
+
+/**
+ * 获取状态标签
+ */
+const getStatusLabel = (status: number) => {
+  const statusMap: Record<number, string> = {
+    1: "待审核",
+    2: "未开始",
+    3: "审核驳回",
+    5: "进行中",
+    6: "已下架",
+    7: "已结束"
+  };
+  return statusMap[status] || "--";
+};
+
+/**
+ * 获取活动规则显示文本
+ */
+const getRuleDisplayText = (ruleValue: any) => {
+  if (!ruleValue) return "--";
+
+  // 如果是字符串,尝试解析为数组
+  let ruleArray: any[] = [];
+  if (typeof ruleValue === "string") {
+    try {
+      ruleArray = JSON.parse(ruleValue);
+    } catch {
+      return ruleValue.replace(/,/g, " / ");
+    }
+  } else if (Array.isArray(ruleValue)) {
+    ruleArray = ruleValue;
+  } else {
+    return ruleValue.replace(/,/g, " / ");
+  }
+
+  // 将数组转换为显示文本,用 " / " 连接
+  if (ruleArray && ruleArray.length > 0) {
+    return ruleArray.filter(item => item).join(" / ");
+  }
+
+  return "--";
+};
+
+/**
+ * 获取图片URL(支持对象和字符串格式)
+ */
+const getImageUrl = (img: any): string => {
+  if (!img) return "";
+  if (typeof img === "string") return img;
+  if (img.url) return img.url;
+  return "";
+};
+
+/**
+ * 获取预览图片列表
+ */
+const getPreviewImageList = () => {
+  const list: string[] = [];
+  const titleUrl = getImageUrl(activityModel.value.activityTitleImgUrl);
+  const detailUrl = getImageUrl(activityModel.value.activityDetailImgUrl);
+  if (titleUrl) {
+    list.push(titleUrl);
+  }
+  if (detailUrl) {
+    list.push(detailUrl);
+  }
+  return list;
+};
+</script>
+
+<style scoped lang="scss">
+/* 页面容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+
+/* 头部区域 */
+.header {
+  display: flex;
+  align-items: center;
+  padding: 20px 24px;
+  background-color: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  box-shadow: 0 2px 4px rgb(0 0 0 / 2%);
+}
+.title {
+  flex: 1;
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+}
+
+/* 内容区域布局 */
+.content {
+  display: flex;
+  flex: 1;
+  column-gap: 24px;
+  width: 100%;
+  max-width: 800px;
+  padding: 24px;
+  margin: 0 auto;
+  background-color: #ffffff;
+  .contentLeft {
+    width: 50%;
+    padding-right: 12px;
+  }
+  .contentRight {
+    width: 50%;
+    padding-left: 12px;
+  }
+}
+
+/* 模块容器 */
+.model {
+  margin-bottom: 50px;
+  h3 {
+    width: 100%;
+    padding-bottom: 12px;
+    margin: 0 0 20px;
+    font-size: 16px;
+    color: #303133;
+    border-bottom: 2px solid #e4e7ed;
+  }
+}
+
+/* 详情项样式 */
+.detail-item {
+  display: flex;
+  align-items: flex-start;
+  min-height: 32px;
+  margin-bottom: 24px;
+}
+.detail-label {
+  flex-shrink: 0;
+  width: 140px;
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 32px;
+  color: #606266;
+}
+.detail-value {
+  flex: 1;
+  font-size: 14px;
+  line-height: 32px;
+  color: #303133;
+  word-break: break-word;
+}
+
+/* 活动宣传图容器 */
+.promotion-images-container {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  align-items: flex-start;
+  width: 100%;
+}
+.promotion-image-item {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  align-items: flex-start;
+  width: 100%;
+  .image-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: #409eff;
+  }
+}
+
+/* 图片容器样式 */
+.image-container {
+  width: 100%;
+  min-height: 100px;
+}
+.image-item {
+  width: 100%;
+}
+.promotion-image {
+  width: 100%;
+  border-radius: 6px;
+  box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+}
+.empty-text {
+  padding: 20px 0;
+  font-size: 14px;
+  color: #909399;
+}
+
+/* 活动标题图 - 21:9横向样式 */
+.promotion-image-item:first-child {
+  .image-container {
+    width: 100%;
+  }
+  .image-item {
+    width: 100%;
+    aspect-ratio: 21 / 9;
+  }
+  .promotion-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+}
+
+/* 活动详情图 - 竖版样式 */
+.promotion-image-item:last-child {
+  .image-container {
+    width: 100%;
+    max-width: 300px;
+  }
+  .image-item {
+    width: 100%;
+    aspect-ratio: 3 / 4;
+  }
+  .promotion-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+}
+.image-slot {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  min-height: 200px;
+  font-size: 30px;
+  color: var(--el-text-color-placeholder);
+  background: var(--el-fill-color-light);
+}
+</style>