lxr hace 2 meses
padre
commit
431c3ddd65

+ 37 - 20
src/api/modules/newLoginApi.ts

@@ -184,20 +184,27 @@ export const addAppealNew = (params: FormData | Record<string, unknown>) => {
 };
 
 /** 商家端 commonComment/addComment:发表评论/回复(评价回复 sourceType=1,动态评论 sourceType=2) */
+// 发表评论 - 表单形式提交(字符串键值对,非 JSON 对象)
 export const addComment = (data: {
-  sourceType: number; // 1-评价的评论 2-动态等
-  sourceId: string | number; // 评价ID或动态ID
-  userId: string;
-  parentId: number; // 0-根评论
-  content: string;
-  commentType: number; // 2-商户评论
-  merchantId: string;
-  isAnonymous?: number;
+  commentContent: string;
+  businessType: number;
+  businessId: string | number;
+  storeId: string;
+  phoneId: string;
+  replyId: string;
+  commentStar: string;
+  multipartRequest: string;
 }) => {
-  return httpLogin.post(`/alienStore/commonComment/addComment`, {
-    ...data,
-    isAnonymous: data.isAnonymous ?? 0
-  });
+  const formData = new FormData();
+  formData.append("commentContent", data.commentContent);
+  formData.append("businessType", String(data.businessType));
+  formData.append("businessId", String(data.businessId));
+  formData.append("storeId", data.storeId);
+  formData.append("phoneId", data.phoneId);
+  formData.append("replyId", data.replyId);
+  formData.append("commentStar", data.commentStar);
+  formData.append("multipartRequest", data.multipartRequest);
+  return httpLogin.post(`/alienStore/storeComment/saveComment`, formData);
 };
 
 /**
@@ -208,15 +215,25 @@ export const addComment = (data: {
 export const saveComment = (params: any) => {
   const businessType = Number(params.businessType ?? 0);
   const isRating = businessType === 1;
+  // const payload = {
+  //   sourceType: isRating ? 1 : 2,
+  //   sourceId: isRating ? params.replyId : params.businessId,
+  //   userId: String(params.userId ?? params.phoneId ?? ""),
+  //   parentId: isRating ? 0 : Number(params.replyId || 0),
+  //   content: String(params.commentContent ?? params.content ?? ""),
+  //   commentType: 2,
+  //   merchantId: String(params.merchantId ?? params.storeId ?? params.businessId ?? ""),
+  //   isAnonymous: 0
+  // };
   const payload = {
-    sourceType: isRating ? 1 : 2,
-    sourceId: isRating ? params.replyId : params.businessId,
-    userId: String(params.userId ?? params.phoneId ?? ""),
-    parentId: isRating ? 0 : Number(params.replyId || 0),
-    content: String(params.commentContent ?? params.content ?? ""),
-    commentType: 2,
-    merchantId: String(params.merchantId ?? params.storeId ?? params.businessId ?? ""),
-    isAnonymous: 0
+    commentContent: String(params.commentContent ?? params.content ?? ""),
+    businessType: 2,
+    businessId: isRating ? params.replyId : params.businessId,
+    storeId: String(params.storeId ?? ""),
+    phoneId: String(params.userId ?? params.phoneId ?? ""),
+    replyId: String(params.replyId ?? ""),
+    commentStar: "",
+    multipartRequest: ""
   };
   return addComment(payload);
 };

+ 22 - 3
src/components/Upload/Imgs.vue

@@ -23,8 +23,9 @@
         </slot>
       </div>
       <template #file="{ file }">
+        <video v-if="isVideoFile(file) && file.url" :src="file.url" class="upload-image" muted preload="metadata" playsinline />
         <img
-          v-if="file.url && file.uid !== undefined && !imageLoadError.has(file.uid)"
+          v-else-if="file.url && file.uid !== undefined && !imageLoadError.has(file.uid)"
           :src="file.url"
           class="upload-image"
           @error="handleImageError"
@@ -32,7 +33,7 @@
         />
         <div v-else class="upload-image-placeholder">
           <el-icon><Picture /></el-icon>
-          <span>图片预览</span>
+          <span>{{ isVideoFile(file) ? "视频预览" : "图片预览" }}</span>
         </div>
         <div class="upload-handle" @click.stop>
           <div class="handle-icon" @click="handlePictureCardPreview(file)">
@@ -72,6 +73,7 @@ interface UploadFileProps {
   width?: string; // 组件宽度 ==> 非必传(默认为 150px)
   borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
   onSuccess?: (url: string) => void;
+  onVideoPreview?: (url: string) => void; // 点击视频「查看」时的回调,用于父级弹窗放大预览(不传则新标签页打开)
   showSuccessNotification?: boolean; // 是否显示上传成功通知 ==> 非必传(默认为 true)
 }
 
@@ -114,6 +116,14 @@ let hasShownSuccessNotification = false;
 // 记录图片加载失败的 UID
 const imageLoadError = ref<Set<string | number>>(new Set());
 
+/** 判断是否为视频文件(按 raw.type 或 url/name 后缀) */
+const isVideoFile = (file: UploadFile) => {
+  const type = file.raw?.type;
+  if (type && typeof type === "string" && type.startsWith("video/")) return true;
+  const url = file.url || file.name || "";
+  return /\.(mp4|webm|ogg|mov)(\?|$)/i.test(String(url));
+};
+
 /**
  * @description 文件上传之前判断
  * @param rawFile 选择的文件
@@ -271,7 +281,16 @@ const handleExceed = () => {
 const viewImageUrl = ref("");
 const imgViewVisible = ref(false);
 const handlePictureCardPreview: UploadProps["onPreview"] = file => {
-  viewImageUrl.value = file.url!;
+  if (!file.url) return;
+  if (isVideoFile(file)) {
+    if (props.onVideoPreview) {
+      props.onVideoPreview(file.url);
+    } else {
+      window.open(file.url, "_blank");
+    }
+    return;
+  }
+  viewImageUrl.value = file.url;
   imgViewVisible.value = true;
 };
 </script>

+ 1 - 1
src/stores/modules/auth.ts

@@ -86,7 +86,7 @@ export const useAuthStore = defineStore({
                 break;
               case "人员配置":
                 // 美食(1)和KTV(3)不显示人员配置,其他业务类型都显示
-                menu.meta.isHide = [1, 3].includes(businessSection);
+                menu.meta.isHide = [1, 3].includes(businessSection) || !storeId;
                 break;
               case "食品经营许可证":
                 // 特色美食(1) 和 酒吧(2):正常显示

+ 9 - 0
src/views/priceList/edit.vue

@@ -1088,4 +1088,13 @@ onMounted(() => {
     color: #f56c6c;
   }
 }
+.el-form-item.el-form-item--default.asterisk-left.el-form-item--label-right {
+  width: 100%;
+  :deep(.el-form-item__label) {
+    width: 100%;
+  }
+  :deep(.el-form-item__content) {
+    width: 100%;
+  }
+}
 </style>

+ 97 - 14
src/views/storeDecoration/personnelConfig/index.vue

@@ -124,14 +124,19 @@
             <div class="detail-label">背景:</div>
             <div class="detail-value">
               <div v-if="personnelDetail.backgroundUrl" class="background-images">
-                <img
-                  v-for="(url, index) in getBackgroundImageList(personnelDetail.backgroundUrl)"
-                  :key="index"
-                  :src="url"
-                  alt="背景图片"
-                  class="background-image"
-                  @click="previewBackgroundImage(url)"
-                />
+                <template v-for="(url, index) in getBackgroundImageList(personnelDetail.backgroundUrl)" :key="index">
+                  <div
+                    v-if="isVideoBackgroundUrl(url)"
+                    class="background-media-item background-media-item--video"
+                    @click="previewBackgroundImage(url)"
+                  >
+                    <video :src="url" class="background-image" muted preload="metadata" playsinline />
+                    <span class="background-media-video-badge">
+                      <el-icon :size="20"><VideoPlay /></el-icon>
+                    </span>
+                  </div>
+                  <img v-else :src="url" alt="背景图片" class="background-image" @click="previewBackgroundImage(url)" />
+                </template>
               </div>
               <span v-else>—</span>
             </div>
@@ -178,6 +183,21 @@
     <!-- 背景图片预览弹窗 -->
     <el-image-viewer v-if="previewImageVisible" :url-list="[previewImageUrl]" @close="previewImageVisible = false" />
 
+    <!-- 背景视频放大预览弹窗 -->
+    <el-dialog
+      v-model="previewVideoVisible"
+      title="视频预览"
+      width="70%"
+      top="5vh"
+      destroy-on-close
+      align-center
+      @close="previewVideoUrl = ''"
+    >
+      <div class="video-preview-wrap">
+        <video v-if="previewVideoUrl" :src="previewVideoUrl" controls autoplay class="video-preview-player" />
+      </div>
+    </el-dialog>
+
     <!-- 新建/编辑人员弹窗 -->
     <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
       <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
@@ -204,17 +224,18 @@
             ref="backgroundImagesUploadRef"
             v-model:file-list="formData.backgroundImages"
             :limit="9"
-            :file-size="20"
-            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+            :file-size="100"
+            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'] as any"
             :width="'150px'"
             :height="'150px'"
             :border-radius="'8px'"
             :api="handleCustomUpload"
             :show-success-notification="false"
             :on-success="handleBackgroundImageSuccess"
+            :on-video-preview="handleVideoPreviewInForm"
           >
             <template #tip>
-              <div class="upload-tip">上传图片 ({{ formData.backgroundImages.length }}/9)</div>
+              <div class="upload-tip">上传图片或视频 ({{ formData.backgroundImages.length }}/9),单个文件不超过 100M</div>
             </template>
           </UploadImgs>
         </el-form-item>
@@ -268,7 +289,7 @@
 import { ref, reactive, computed, onMounted, watch, nextTick } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import type { FormInstance, FormRules } from "element-plus";
-import { Picture, Plus, Delete, ArrowDown } from "@element-plus/icons-vue";
+import { Picture, Plus, Delete, ArrowDown, VideoPlay } from "@element-plus/icons-vue";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import UploadImg from "@/components/Upload/Img.vue";
@@ -321,6 +342,8 @@ const detailLoading = ref(false);
 const personnelDetail = ref<any>(null);
 const previewImageVisible = ref(false);
 const previewImageUrl = ref("");
+const previewVideoVisible = ref(false);
+const previewVideoUrl = ref("");
 
 // 标题相关
 const hasTitle = ref(false); // 是否已有标题
@@ -554,7 +577,7 @@ const getTableList = (params: any) => {
 };
 
 // 格式化时间
-// 获取背景图片列表
+// 获取背景图片/视频列表
 const getBackgroundImageList = (backgroundUrl: string | null | undefined): string[] => {
   if (!backgroundUrl) return [];
   if (typeof backgroundUrl === "string") {
@@ -563,12 +586,26 @@ const getBackgroundImageList = (backgroundUrl: string | null | undefined): strin
   return Array.isArray(backgroundUrl) ? backgroundUrl : [];
 };
 
-// 预览背景图片
+/** 判断背景 URL 是否为视频(用于详情展示与预览) */
+const isVideoBackgroundUrl = (url: string) => /\.(mp4|webm|ogg|mov)(\?|$)/i.test(url);
+
+// 预览背景图片/视频(图片用查看器,视频用放大弹窗)
 const previewBackgroundImage = (url: string) => {
+  if (isVideoBackgroundUrl(url)) {
+    previewVideoUrl.value = url;
+    previewVideoVisible.value = true;
+    return;
+  }
   previewImageUrl.value = url;
   previewImageVisible.value = true;
 };
 
+// 编辑/新增表单内点击视频「查看」时,复用同一套视频放大弹窗
+const handleVideoPreviewInForm = (url: string) => {
+  previewVideoUrl.value = url;
+  previewVideoVisible.value = true;
+};
+
 const formatTime = (time: string | null | undefined) => {
   if (!time) return "";
   try {
@@ -2386,9 +2423,55 @@ onMounted(async () => {
               transform: scale(1.05);
             }
           }
+          .background-media-item {
+            position: relative;
+            width: 120px;
+            height: 120px;
+            overflow: hidden;
+            cursor: pointer;
+            border-radius: 8px;
+            transition: transform 0.2s;
+            &:hover {
+              box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
+              transform: scale(1.05);
+              .background-media-video-badge {
+                opacity: 1;
+              }
+            }
+            .background-image {
+              width: 100%;
+              height: 100%;
+            }
+            .background-media-video-badge {
+              position: absolute;
+              bottom: 6px;
+              left: 6px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              width: 28px;
+              height: 28px;
+              color: #ffffff;
+              background: rgb(0 0 0 / 55%);
+              border-radius: 6px;
+              opacity: 0.9;
+            }
+          }
         }
       }
     }
   }
 }
+.video-preview-wrap {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 200px;
+  background: #000000;
+  .video-preview-player {
+    width: 100%;
+    max-width: 100%;
+    max-height: 75vh;
+  }
+}
 </style>