Browse Source

feat(template): 优化模板封面图生成和显示逻辑

- 实现 getTemplateDisplayImage 方法,优先使用 template_preview_image,其次 template_cover_image
- 更新模板详情查看功能,使用新的封面图获取逻辑
- 重构模板操作按钮显示条件,统一管理查看、编辑、删除权限
- 实现模板封面图合并上传功能,支持多画布预览图合并为长图
- 添加模型和场景类型画布的占位图处理
- 调整模板操作按钮样式,减少内边距和移除右侧边距
panqiuyao 17 hours ago
parent
commit
b314312896
2 changed files with 75 additions and 18 deletions
  1. 18 14
      frontend/src/views/Photography/detail.vue
  2. 57 4
      frontend/src/views/Tpl/Edit/index.vue

+ 18 - 14
frontend/src/views/Photography/detail.vue

@@ -201,7 +201,7 @@
                          v-log="{ describe: { action: '点击下载EXECL模版', template_name: template.template_name } }">
                       下载EXECL模版
                     </div>
-                    <el-image :src="template.template_preview_image" fit="contain" class="cur-p"
+                    <el-image :src="getTemplateDisplayImage(template)" fit="contain" class="cur-p"
                       style="width: 100%; display: block;" />
                     <div class="select-warp" :class="form.selectTemplate?.id == template.id ? 'active' : ''">
                       <el-icon color="#FFFFFF">
@@ -210,17 +210,17 @@
                     </div>
                     <div class="template-info">
                       <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
-                      <div class="template-view" v-if="isDetailServiceSelected && template.template_type == 0"
-                        @click.stop="viewTemplate(template)"
-                        v-log="{ describe: { action: '点击查看模板详情', template_name: template.template_name } }">
-                        查看
-                      </div>
-                      <div class="template-actions flex" v-if="isDetailServiceSelected && template.template_type == 1">
-                        <div class="template-view" @click.stop="editTemplate(template)"
+                      <div class="template-actions flex" v-if="isDetailServiceSelected">
+                        <div class="template-view"
+                             @click.stop="viewTemplate(template)"
+                             v-log="{ describe: { action: '点击查看模板详情', template_name: template.template_name } }">
+                          查看
+                        </div>
+                        <div class="template-view" v-if="template.template_type == 1" @click.stop="editTemplate(template)"
                           v-log="{ describe: { action: '点击编辑模板详情', template_name: template.template_name } }">
                           编辑
                         </div>
-                        <div class="template-view template-view-delete" @click.stop="deleteTemplate(template)"
+                        <div class="template-view template-view-delete"  v-if="template.template_type == 1" @click.stop="deleteTemplate(template)"
                           v-log="{ describe: { action: '点击删除自定义模板', template_name: template.template_name } }">
                           删除
                         </div>
@@ -1103,12 +1103,17 @@ watch(isDetailServiceSelected, (selected) => {
     clearTemplateSelection()
   }
 });
+// 获取模板展示图(优先 template_preview_image,其次 template_cover_image)
+const getTemplateDisplayImage = (template: any) => {
+  if (!template) return ''
+  return template.template_preview_image || template.template_cover_image || ''
+}
+
 // 查看模板详情
 const viewTemplate = (template) => {
-  // 展示大图
+  // 展示大图,优先 template_preview_image,其次 template_cover_image
   dialogVisible.value = true
-  dialogImageUrl.value = template.template_preview_image
-
+  dialogImageUrl.value = getTemplateDisplayImage(template)
 };
 // 编辑自定义模版
 const editTemplate = async (template: any) => {
@@ -2882,9 +2887,8 @@ const selectFolder = () => {
         color: #3366FF;
         height: 30px;
         line-height: 30px;
-        padding: 0 10px;
+        padding: 0 5px;
         border-radius: 4px;
-        margin-right: 10px;
         font-size: 14px;
       }
     }

+ 57 - 4
frontend/src/views/Tpl/Edit/index.vue

@@ -38,7 +38,7 @@ import headerBar from "@/components/header-bar/index.vue";
 import marketingEdit from '@/views/components/marketingEdit/index.vue'
 import useClientStore from '@/stores/modules/client'
 
-import { saveCustomerTemplate } from '@/apis/other'
+import { saveCustomerTemplate, uploadBaseImg } from '@/apis/other'
 
 
 const route = useRoute();
@@ -154,9 +154,62 @@ const doSave = async (payload: any) => {
       // 新增时必填模板名称
       requestData.template_name = templateName.value?.trim() || ''
     }
-    // 封面图:取第一张画布的 preview
-    if (payload && payload.length > 0 && payload[0].preview) {
-      requestData.template_cover_image = payload[0].preview
+    // 生成并上传模板封面:把每个画布的 preview(若有)合并为一张长图并上传,取回 URL
+    async function composeAndUpload(previewList: string[]) {
+      try {
+        const infos = await Promise.all(previewList.map(src => new Promise((res) => {
+          if (!src) return res(null)
+          const img = new Image()
+          img.crossOrigin = 'anonymous'
+          img.onload = () => res({ img, w: img.width, h: img.height })
+          img.onerror = () => res(null)
+          img.src = src
+        })))
+        const valid = infos.filter(Boolean)
+        if (!valid.length) return null
+        const maxW = Math.max(...valid.map(i => i.w))
+        const totalH = valid.reduce((s, i) => s + i.h, 0)
+        const canvas = document.createElement('canvas')
+        canvas.width = maxW
+        canvas.height = totalH
+        const ctx = canvas.getContext('2d')
+        if (!ctx) return null
+        ctx.fillStyle = '#fff'
+        ctx.fillRect(0, 0, canvas.width, canvas.height)
+        let top = 0
+        for (const v of valid) {
+          const left = Math.round((maxW - v.w) / 2)
+          ctx.drawImage(v.img, left, top, v.w, v.h)
+          top += v.h
+        }
+        const dataUrl = canvas.toDataURL('image/jpeg', 0.9)
+        const res = await uploadBaseImg({ image: dataUrl })
+        return res?.data?.url || res?.url || null
+      } catch (e) {
+        console.warn('composeAndUpload error', e)
+        return null
+      }
+    }
+
+    // collect previews from payload, use placeholders for model/scene canvases
+    const MODEL_PLACEHOLDER = 'https://ossimg.valimart.net/uploads/vali_ai/20251230/176707412156399.png'
+    const SCENE_PLACEHOLDER = 'https://ossimg.valimart.net/uploads/vali_ai/20251230/176707413531824.png'
+    const previews = (payload || []).map((p: any) => {
+      if (!p) return null
+      if (p.canvas_type === 'model') return MODEL_PLACEHOLDER
+      if (p.canvas_type === 'scene') return SCENE_PLACEHOLDER
+      return p.preview || null
+    }).filter(Boolean)
+    if (previews.length) {
+      const uploadedUrl = await composeAndUpload(previews)
+      if (uploadedUrl) requestData.template_preview_image = uploadedUrl
+      // also set cover to first preview if not set
+      if (!requestData.template_cover_image && previews[0]) requestData.template_cover_image = previews[0]
+    } else {
+      // fallback: if no previews, keep existing logic of using first canvas preview if available
+      if (payload && payload.length > 0 && payload[0].preview) {
+        requestData.template_cover_image = payload[0].preview
+      }
     }
     requestData.customer_template_images = goodsImages.value
     requestData.template_image_order = templateImageOrder.value || undefined