Explorar o código

feat(generate): 新增获取商品图片 JSON 接口及相关功能

- 在 generate.js 中新增 getGoodsImageJson 方法,用于根据货号获取商品图片 JSON。
- 更新 GenerateController,添加 getGoodsImageJson 方法以处理请求。
- 在前端视图中实现货号选择弹窗,支持用户选择货号并生成商品模板。
- 更新相关样式以支持新功能的展示和交互。
- 优化模板编辑页面,支持保存和删除自定义模板功能。
kongwenhao hai 1 día
pai
achega
de5e9e0d63

+ 8 - 0
electron/api/generate.js

@@ -32,4 +32,12 @@ module.exports = {
       data: data
     })
   },
+
+  // 根据货号获取商品图片 JSON(生成商品模板用)
+  getGoodsImageJson(data){
+    return get({
+      url: '/get_goods_image_json',
+      data: data,
+    })
+  },
 }

+ 15 - 1
electron/controller/generate.js

@@ -5,7 +5,7 @@ const Log = require('ee-core/log');
 const Services = require('ee-core/services');
 const path = require('path');
 const fs = require('fs');
-const { generatePhotoDetail, getLogoList, addLogo,deleteLogo } = require('../api/generate');
+const { generatePhotoDetail, getLogoList, addLogo, deleteLogo, getGoodsImageJson } = require('../api/generate');
 
 const errData = {
   msg :'请求失败,请联系管理员',
@@ -77,6 +77,20 @@ class GenerateController extends Controller {
     }
   }
 
+  /**
+   * 根据货号获取商品图片 JSON(生成商品模板用)
+   */
+  async getGoodsImageJson(args) {
+    try {
+      const result = await getGoodsImageJson(args);
+      if (result.data) return result.data;
+      return errData;
+    } catch (error) {
+      Log.error('获取商品图片 JSON 失败:', error);
+      return errData;
+    }
+  }
+
 
 }
 

+ 12 - 0
frontend/src/apis/other.ts

@@ -35,3 +35,15 @@ export function uploadImg(data,loading=true) {
         loading: loading,
     })
 }
+
+
+
+// 保存客户模版
+export async function saveCustomerTemplate(params:any){
+    return POST('/api/ai_image/auto_photo/save_customer_template', params)
+}
+
+// 删除自定义模板
+export async function deleteCustomerTemplate(params: { id: number | string }) {
+    return POST('/api/ai_image/auto_photo/delete_template', params)
+}

+ 1 - 0
frontend/src/utils/ipc.ts

@@ -50,6 +50,7 @@ const icpList = {
         addLogo: 'controller.generate.addLogo',
         getLogoList: 'controller.generate.getLogoList',
         deleteLogo: 'controller.generate.deleteLogo',
+        getGoodsImageJson: 'controller.generate.getGoodsImageJson',
     },
     ota:{
         updateVersion: 'controller.ota.updateVersion'

+ 546 - 7
frontend/src/views/Photography/detail.vue

@@ -83,15 +83,19 @@
         </div>
 
         <!-- 未开发的功能 -->
-        <div class="service-tab disabled" title="功能开发中">
+        <div class="service-tab " title="新增自定义详情页模板"
+         @click="addCustomTemplate"
+          v-log="{ describe: { action: '点击新增自定义详情页模板', service: '新增自定义详情页模板' } }"
+        >
           <div class="tab-content">
             <div class="tab-img flex">
               <img src="@/assets/images/detail/xqmb.svg" alt="详情页模板自定义" class="tab-icon" />
             </div>
-            <span class="tab-name">详情页模板自定义</span>
+            <span class="tab-name">新增自定义详情页模板</span>
           </div>
         </div>
 
+
         <div
           class="service-tab external-tool"
           title="白底图批量导出"
@@ -229,7 +233,33 @@
                     </div>
                     <div class="template-info">
                       <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
-                      <div class="template-view" v-if="isDetailServiceSelected" @click.stop="viewTemplate(template)" v-log="{ describe: { action: '点击查看模板详情', template_name: template.template_name } }">查看</div>
+                      <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)"
+                          v-log="{ describe: { action: '点击编辑模板详情', template_name: template.template_name } }"
+                        >
+                          编辑
+                        </div>
+                        <div
+                          class="template-view template-view-delete"
+                          @click.stop="deleteTemplate(template)"
+                          v-log="{ describe: { action: '点击删除自定义模板', template_name: template.template_name } }"
+                        >
+                          删除
+                        </div>
+                      </div>
                     </div>
                   </div>
                 </div>
@@ -510,6 +540,81 @@
     @cancel="scenePromptDialogVisible = false"
   />
 
+  <!-- 货号选择弹窗 -->
+  <el-dialog
+    v-model="goodsSelectDialogVisible"
+    title="请选择一个货号作为自定义商品的模板"
+    width="60%"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    custom-class="goods-select-dialog"
+  >
+    <div class="goods-select-content">
+  <el-radio-group v-model="selectedGoodsArtNo" class="goods-radio-group"> 
+        <div
+          v-for="item in filteredGoodsList"
+          :key="item.goods_art_no"
+          class="goods-item"
+          :class="{ 'selected': selectedGoodsArtNo === item.goods_art_no }"
+          @click="selectedGoodsArtNo = item.goods_art_no"
+        >
+      
+            <div class="goods-item-header">
+              <div class="goods-item-left">
+                <el-radio
+                  :label="item.goods_art_no"
+                  class="goods-radio"
+                  @click.stop
+          > </el-radio>
+              
+              </div>
+            </div>
+            <div class="goods-item-images">
+              <div
+                v-for="(image, index) in item.items"
+                :key="image.action_id || image.action_name"
+                class="goods-item_image"
+              >
+                <span class="tag" v-if="!image.PhotoRecord?.image_path">{{ image.action_name }}</span>
+                <el-image
+                  v-if="image.PhotoRecord?.image_path"
+                  :src="getFilePath(image.PhotoRecord.image_path)"
+                  :preview-src-list="getPreviewImageList(item)"
+                  :initial-index="getPreviewIndex(item, index)"
+                  class="preview-image"
+                  fit="contain"
+                  :preview-teleported="true"
+                  lazy
+                >
+                  <template #error>
+                    <div class="image-slot">
+                      <span class="tag">{{ image.action_name }}</span>
+                    </div>
+                  </template>
+                </el-image>
+                <div v-else class="image-placeholder">
+                  <span class="tag">{{ image.action_name }}</span>
+                </div>
+              </div>
+            </div>
+         
+        </div>
+      </el-radio-group>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="cancelGoodsSelection">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="goodsGenerateLoading"
+          :disabled="goodsGenerateLoading"
+          @click="confirmGoodsSelection"
+        >
+          {{ goodsGenerateLoading ? '正在生成自定义商品模版' : '确定' }}
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
 
 </template>
 
@@ -541,13 +646,24 @@ import ScenePromptDialog from '@/components/ScenePromptDialog/index.vue'
 import { Close, Warning } from '@element-plus/icons-vue'
 import LoadingDialog from '@/views/Photography/components/LoadingDialog.vue'
 
-
 import configInfo from "@/stores/modules/config";
+import { deleteCustomerTemplate } from '@/apis/other'
 
 const useConfigInfoStore = configInfo();
 
 const EXTERNAL_TOOL_EXECUTABLE = '智慧映拍照机辅助工具箱.exe'
 
+import usePhotography from './mixin/usePhotography'
+
+const {
+  loading,
+  goodsList,
+  getPhotoRecords,
+  getTime,
+  getFilePath,
+} = usePhotography()
+
+
 const launchExternalTool = async (pagePath: string, successMessage: string) => {
   if (!clientStore?.ipc || !clientStore.isClient) {
     ElMessage.error('当前环境暂不支持外部工具');
@@ -579,6 +695,180 @@ const launchExternalTool = async (pagePath: string, successMessage: string) => {
 const handleWhiteBgExportClick = () => launchExternalTool('/copy_800_tool', '白底图批量导出工具已启动')
 const handleProductAlbumClick = () => launchExternalTool('/product_list', '产品图册生成工具已启动')
 
+// 货号选择弹窗相关状态
+const goodsSelectDialogVisible = ref(false)
+const filteredGoodsList = ref<any[]>([])
+const selectedGoodsArtNo = ref<string>('')
+const goodsGenerateLoading = ref(false)
+
+const addCustomTemplate = async() => {
+  // 调用获取数据
+  getPhotoRecords()
+  
+  // 处理数据的函数
+  const processData = () => {
+    // 从 goodsList 中筛选出 goods_art_no 在 goods_art_nos.value 中的对象
+    const filteredGoods = goodsList.value.filter((item: any) =>
+      goods_art_nos.value.includes(item.goods_art_no)
+    )
+
+    console.log('goods_art_nos', goods_art_nos.value)
+    console.log('filteredGoods', filteredGoods)
+
+    if (filteredGoods.length > 0) {
+      // 若有匹配项,则只展示匹配到的货号
+      filteredGoodsList.value = filteredGoods
+      selectedGoodsArtNo.value = filteredGoods[0].goods_art_no
+      goodsSelectDialogVisible.value = true
+    } else if (goodsList.value.length > 0) {
+      // 若没有匹配到货号,则展示全部货号列表
+      filteredGoodsList.value = goodsList.value
+      selectedGoodsArtNo.value = goodsList.value[0].goods_art_no
+      goodsSelectDialogVisible.value = true
+    } else {
+      // 列表本身为空时再提示
+      ElMessage.warning('未找到匹配的货号数据')
+    }
+  }
+  
+  // 使用 watch 监听 loading 状态,当从 true 变为 false 时,说明数据加载完成
+  const stopWatcher = watch(
+    () => loading.value,
+    (isLoading, wasLoading) => {
+      // 当 loading 从 true 变为 false 时,说明数据已经加载完成
+      if (wasLoading && !isLoading) {
+        processData()
+        // 停止监听,避免重复执行
+        stopWatcher()
+      }
+    }
+  )
+  
+  // 如果当前 loading 已经是 false,可能数据已经存在或加载很快完成
+  // 使用 nextTick 等待一个 tick,如果 loading 仍然是 false,直接处理
+  if (!loading.value) {
+    await nextTick()
+    // 如果 loading 仍然是 false,可能数据已经存在,直接处理
+    if (!loading.value) {
+      processData()
+      stopWatcher() // 停止监听
+    }
+  }
+}
+
+// 根据选中的货号调用后端生成商品模板
+const generateGoodsTemplate = (goodsArtNo: string) => {
+  return new Promise<void>((resolve, reject) => {
+    try {
+      console.log('调用 get_goods_image_json, goods_art_no:', goodsArtNo)
+
+      // 按后端要求带上 token(参考其他生成类接口)
+      const tokenStore = tokenInfo();
+      const token = (tokenStore as any)?.getToken || '';
+
+      // 先清理监听,避免重复回调
+      clientStore.ipc.removeAllListeners(icpList.generate.getGoodsImageJson)
+
+      clientStore.ipc.send(icpList.generate.getGoodsImageJson, {
+        goods_art_no: goodsArtNo,
+        token,
+      })
+
+      clientStore.ipc.on(icpList.generate.getGoodsImageJson, (event, result) => {
+        clientStore.ipc.removeAllListeners(icpList.generate.getGoodsImageJson)
+        console.log('get_goods_image_json result:', result)
+        if (result && result.code === 0) {
+          resolve(result.data)
+        } else {
+          const msg = result?.msg || '商品模板生成失败'
+          ElMessage.error(msg)
+          reject(new Error(msg))
+        }
+      })
+    } catch (error: any) {
+      ElMessage.error(error?.message || '商品模板生成异常')
+      reject(error)
+    }
+  })
+}
+
+// 确认选择货号
+const confirmGoodsSelection = async () => {
+  if (!selectedGoodsArtNo.value) {
+    ElMessage.warning('请选择一个货号')
+    return
+  }
+
+  console.log('选中的货号:', selectedGoodsArtNo.value)
+  goodsGenerateLoading.value = true
+
+  try {
+    const data = await generateGoodsTemplate(selectedGoodsArtNo.value)
+    console.log('商品模版数据', data)
+
+    // 组装 goods_images 数组:[{ key: 模板顺序项, value: 对应图片地址 }, ...]
+    const goodsImages: Array<{ key: string; value: string }> = []
+    const orderArr = (data?.template_image_order || '')
+      .split(',')
+      .map((s: string) => s.trim())
+      .filter((s: string) => s)
+    const imagesArr = Array.isArray(data?.customer_template_images)
+      ? data.customer_template_images
+      : []
+
+    const len = Math.min(orderArr.length, imagesArr.length)
+    for (let i = 0; i < len; i++) {
+      goodsImages.push({
+        key: orderArr[i],
+        value: imagesArr[i],
+      })
+    }
+
+    // 生成商品模板成功后,跳转到新增模板页,并携带当前选中的货号
+    router.push({
+      name: 'addTpl',
+      query: {
+        // 统一用 JSON 字符串传递图片与顺序信息
+        customer_template_images: encodeURIComponent(JSON.stringify(goodsImages)),
+        template_image_order: data?.template_image_order || '',
+      },
+    })
+  } finally {
+    // 无论成功或失败,都关闭弹窗,保留当前选择
+    goodsSelectDialogVisible.value = false
+    goodsGenerateLoading.value = false
+  }
+}
+
+// 取消选择
+const cancelGoodsSelection = () => {
+  goodsSelectDialogVisible.value = false
+  selectedGoodsArtNo.value = ''
+  filteredGoodsList.value = []
+}
+
+// 获取预览图片列表(只包含有图片路径的,保持原始顺序)
+const getPreviewImageList = (item: any) => {
+  if (!item || !item.items) return []
+  return item.items
+    .filter((img: any) => img.PhotoRecord?.image_path)
+    .map((img: any) => getFilePath(img.PhotoRecord.image_path))
+}
+
+// 获取当前图片在预览列表中的索引
+const getPreviewIndex = (item: any, currentIndex: number) => {
+  if (!item || !item.items) return 0
+  // 计算当前图片在过滤后的预览列表中的索引
+  let previewIndex = 0
+  for (let i = 0; i <= currentIndex; i++) {
+    if (item.items[i]?.PhotoRecord?.image_path) {
+      if (i === currentIndex) break
+      previewIndex++
+    }
+  }
+  return previewIndex
+}
+
 
 
 import { useCheckInfo } from '@/composables/userCheck';
@@ -618,6 +908,42 @@ const progressSteps = ref<Array<{
   error: number
 }>>([])
 
+// 删除自定义模板
+const deleteTemplate = async (template: any) => {
+  if (!template?.id) return
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除自定义模板「${template.template_name || ''}」吗?`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      },
+    )
+
+    const res = await deleteCustomerTemplate({ id: template.id })
+    const data = res?.data
+
+    if (data?.code === 0) {
+      ElMessage.success('删除成功')
+      // 从当前模板列表中移除
+      const idx = templates.value.findIndex((item: any) => item.id === template.id)
+      if (idx !== -1) {
+        templates.value.splice(idx, 1)
+      }
+    } else {
+      ElMessage.error(data?.msg || '删除失败')
+    }
+  } catch (e: any) {
+    if (e !== 'cancel' && e !== 'close') {
+      console.error('删除模板失败:', e)
+      ElMessage.error(e?.message || '删除失败')
+    }
+  }
+}
+
 // 更新进度步骤
 const updateProgressStep = (msgType: string, stepData: any) => {
   console.log('updateProgressStep called:', msgType, stepData)
@@ -745,12 +1071,10 @@ const hasTemplates = computed(() => templates.value.length > 0)
 const canUsePublishSection = computed(() => isDetailServiceSelected.value && hasTemplates.value)
 onMounted(() => {
   // 页面访问埋点
-
   const goods_art_data = route.query.goods_art_nos
   goods_art_nos.value = Array.isArray(goods_art_data) ? goods_art_data : [goods_art_data]
   getCompanyTemplates()
   getLogolist()
-
   loadDetailCache()
 
   // 初始化目录打开状态标记
@@ -859,10 +1183,32 @@ const viewTemplate = (template) => {
   dialogImageUrl.value = template.template_preview_image
 
 };
+// 编辑自定义模版
+const editTemplate = (template: any) => {
+  if (!template) return
+
+  // 将画布 JSON、名称、商品图片等信息通过 query 传给编辑页
+  const customerTemplateJson = template.customer_template_json || []
+  const customerTemplateImages = template.customer_template_images || []
+  const templateImageOrder = template.template_image_order || ''
+  const templateName = template.template_name || ''
+
+  router.push({
+    name: 'editTpl',
+    params: { id: template.id },
+    query: {
+      customer_template_json: encodeURIComponent(JSON.stringify(customerTemplateJson)),
+      template_name: templateName,
+      customer_template_images: encodeURIComponent(JSON.stringify(customerTemplateImages)),
+      template_image_order: templateImageOrder,
+    },
+  })
+};
+
 // 获取模版列表
 const getCompanyTemplates = async () => {
   const { data } = await getCompanyTemplatesApi()
-  console.log(data);
+  console.log('getCompanyTemplatesApi' ,data);
   templates.value = data.list || []
   // 获取电商平台列表 - 支持新的数据结构
   if (data.online_store_temp_list) {
@@ -2774,3 +3120,196 @@ const selectFolder = () => {
   }
 }
 </style>
+
+<style lang="scss" scoped>
+// 货号选择弹窗样式(非 scoped,因为弹窗挂载在 body 上)
+.goods-select-dialog {
+  max-width: 60vw;
+  .el-dialog__body {
+    padding: 20px;
+    max-height: 70vh;
+    overflow-y: auto;
+    height: 60vh;
+  }
+
+  .goods-select-content {
+    .goods-radio-group {
+      width: 100%;
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+    }
+
+    .goods-item {
+      background: #FFFFFF;
+      box-shadow: 0px 2px 4px 0px rgba(23,33,71,0.1);
+      border-radius: 10px;
+      border: 1px solid #D9DEE6;
+      margin-bottom: 20px;
+      cursor: pointer;
+      transition: all 0.3s;
+
+   
+
+      .goods-radio {
+        width: 100%;
+        margin: 0;
+        padding: 0;
+        position: relative;
+
+        .el-radio__input {
+          position: absolute;
+          top: 14px;
+          left: 18px;
+          z-index: 10;
+          transform: scale(1);
+        }
+
+        .el-radio__inner {
+          border-width: 2px;
+        }
+
+        &.is-checked {
+          .el-radio__inner {
+            background-color: #409eff;
+            border-color: #409eff;
+          }
+        }
+
+        .el-radio__label {
+          width: 100%;
+          padding-left: 40px;
+          display: none;
+        }
+      }
+
+      .goods-item-header {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+        height: 40px;
+        padding: 0 16px;
+        background: linear-gradient(90deg, #F4ECFF 0%, #DFEDFF 100%);
+        border-radius: 10px 10px 0px 0px;
+
+        .goods-item-left {
+          display: flex;
+          align-items: center;
+          gap: 16px;
+
+          .goods-art-no {
+            font-size: 16px;
+            font-weight: 500;
+            color: #333;
+          }
+
+          .goods-item-meta {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            font-size: 12px;
+            color: #666;
+
+            img {
+              height: 14px;
+              margin-right: 2px;
+            }
+
+            .action-time {
+              color: #666;
+            }
+
+            .image-count {
+              color: #666;
+            }
+          }
+        }
+      }
+
+      .goods-item-images {
+        display: grid;
+        grid-template-columns: repeat(5, 1fr);
+        gap: 10px;
+        padding: 15px;
+        border-top: 1px solid #f0f0f0;
+        overflow-x: auto;
+
+        @media (min-width: 1200px) {
+          grid-template-columns: repeat(5, 1fr);
+        }
+
+        @media (max-width: 768px) {
+          grid-template-columns: repeat(3, 1fr);
+        }
+      }
+
+      .goods-item_image {
+        position: relative;
+        width: 100%;
+        aspect-ratio: 1;
+        background: #F7F7F7;
+        border-radius: 10px;
+        overflow: hidden;
+        cursor: pointer;
+        border: 1px solid #D9DEE6;
+        transition: all 0.3s;
+
+        .tag {
+          color: #bbb;
+          position: absolute;
+          left: 0;
+          right: 0;
+          top: 50%;
+          margin-top: -10px;
+          line-height: 20px;
+          text-align: center;
+          font-size: 12px;
+          z-index: 1;
+          pointer-events: none;
+        }
+
+        .preview-image {
+          width: 100%;
+          height: 100%;
+
+          .el-image__inner {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+
+        .image-placeholder {
+          width: 100%;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background: #F7F7F7;
+        }
+
+        .image-slot {
+          width: 100%;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background: #F7F7F7;
+        }
+
+        &:hover {
+          border-color: #409eff;
+          transform: scale(1.02);
+          box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+        }
+      }
+    }
+  }
+
+  .dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+  }
+}
+</style>

+ 1 - 0
frontend/src/views/Photography/mixin/usePhotography.ts

@@ -98,6 +98,7 @@ export default function usePhotography() {
     async function getPhotoRecords(params?: {}) {
       if (loading.value) return;
       loading.value = true;
+      console.log('params' , params)
       clientStore.ipc.send(icpList.takePhoto.getPhotoRecords, {
         ...params,
         page: 1,

+ 191 - 65
frontend/src/views/Tpl/Edit/index.vue

@@ -2,82 +2,208 @@
   <headerBar title="编辑模板" />
 
   <marketingEdit
-      v-model:data="data"
-      v-model:index="index"
-      :goods_text="[
-          {
-            key: '设计理念',
-            value: '经典凹出兼具动感同时带来轻盈\n步调轻软,松弛自在蔓延\n立体质感让朝气肆意绽放'
-          },
-          {
-            key: '标题',
-            value: '休闲运动'
-          },
-          {
-            key: '帮面',
-            value: '网布+合成革'
-          },
-          {
-            key: '鞋底',
-            value: '橡胶底'
-          },
-          {
-            key: '颜色',
-            value: '黑色'
-          }
-      ]"
-      :goods_images="[
-          {
-            key: '俯视',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png'
-          },
-          {
-            key: '侧视',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png'
-          },
-          {
-            key: '后跟',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\3.png'
-          },
-          {
-            key: '鞋底',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\4.png'
-          },
-          {
-            key: '内里',
-            value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\5.png'
-          }
-      ]"
-      @save="save"
+    v-model:data="data"
+    v-model:index="index"
+    :goods_text="defaultGoodsText"
+    :goods_images="goodsImages"
+    @save="save"
   />
 
-
+  <el-dialog
+      v-model="showNameDialog"
+      title="输入模板名称"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      width="400px"
+  >
+    <el-input
+        v-model="templateName"
+        placeholder="请输入模板名称"
+        maxlength="50"
+        show-word-limit
+    />
+    <template #footer>
+      <el-button @click="showNameDialog = false">取消</el-button>
+      <el-button type="primary" @click="confirmName">确定</el-button>
+    </template>
+  </el-dialog>
 
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { computed, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
 import headerBar from "@/components/header-bar/index.vue";
 import marketingEdit from '@/views/components/marketingEdit'
+import useClientStore from '@/stores/modules/client'
+
+import { saveCustomerTemplate } from '@/apis/other'
+
+
+const route = useRoute();
+const router = useRouter();
+const clientStore = useClientStore();
+const isEdit = !!route.params.id;
+
 const data  = ref([])
 const index = ref(0)
+const templateName = ref('')
+const showNameDialog = ref(false) // 由"保存"触发,而不是进入页面就弹
+const pendingPayload = ref<any | null>(null)
+
+// 编辑模式:从路由 query 中还原模板数据和名称
+if (isEdit) {
+  const query: any = route.query || {}
+
+  // 还原画布 JSON
+  const rawJson = query.customer_template_json as string | undefined
+  if (rawJson) {
+    try {
+      const decoded = decodeURIComponent(rawJson)
+      const parsed = JSON.parse(decoded)
+      if (Array.isArray(parsed)) {
+        data.value = parsed
+      } else {
+        data.value = parsed
+      }
+    } catch (e) {
+      console.error('解析 customer_template_json 失败:', e)
+    }
+  }
+
+  // 名称直接取 template_name,取不到则为空
+  templateName.value = (query.template_name || '') as string
+}
+
+// 默认商品文案
+const defaultGoodsText = [
+  {
+    key: '设计理念',
+    value: '经典凹出兼具动感同时带来轻盈\n步调轻软,松弛自在蔓延\n立体质感让朝气肆意绽放'
+  },
+  {
+    key: '标题',
+    value: '休闲运动'
+  },
+  {
+    key: '帮面',
+    value: '网布+合成革'
+  },
+  {
+    key: '鞋底',
+    value: '橡胶底'
+  },
+  {
+    key: '颜色',
+    value: '黑色'
+  }
+]
+
+// 从路由 query 中解析 goods_images,如果没有就使用默认示例
+const defaultGoodsImages = [
+  {
+    key: '俯视',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png'
+  },
+  {
+    key: '侧视',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png'
+  },
+  {
+    key: '后跟',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\3.png'
+  },
+  {
+    key: '鞋底',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\4.png'
+  },
+  {
+    key: '内里',
+    value: 'C:\\Users\\Administrator\\Desktop\\img\\A596351\\5.png'
+  }
+]
+
+const goodsImages = computed(() => {
+  const raw = route.query.customer_template_images as string | undefined
+  if (!raw) return defaultGoodsImages
+
+  try {
+    const decoded = decodeURIComponent(raw)
+    const parsed = JSON.parse(decoded)
+    if (Array.isArray(parsed) && parsed.length > 0) {
+      return parsed
+    }
+    return defaultGoodsImages
+  } catch (e) {
+    console.error('解析 customer_template_images 失败:', e)
+    return defaultGoodsImages
+  }
+})
+
+
+
+const doSave = async (payload: any) => {
+  try {
+    // 组装请求参数
+    const requestData: any = {
+      customer_template_json: payload, // 所有画布的 JSON
+    }
+    // 编辑时必填 id
+    if (isEdit) {
+      requestData.id = route.params.id
+      requestData.template_name = templateName.value?.trim() || ''
+
+      // 编辑时使用原来的模板名称(不传 template_name,让后端保持原值)
+    } else {
+      // 新增时必填模板名称
+      requestData.template_name = templateName.value?.trim() || ''
+    }
+    // 封面图:取第一张画布的 preview
+    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 =  route.query.template_image_order as string | undefined
+
+    console.log('requestData' , requestData)
+
+    await saveCustomerTemplate(requestData)
+    ElMessage.success(isEdit ? '模板保存成功' : '模板创建成功')
+    // 保存成功后返回上一页(模板列表或详情页面)
+    router.back()
+  } catch (error: any) {
+    console.error('保存模板失败:', error)
+  }
+}
+
+const confirmName = () => {
+  if (!templateName.value || !templateName.value.trim()) {
+    // 简单校验,保持弹窗打开
+    return
+  }
+  showNameDialog.value = false
+  if (!isEdit && pendingPayload.value) {
+    doSave(pendingPayload.value)
+    pendingPayload.value = null
+  }
+}
+
+const save = (payload: any) => {
+  if (isEdit) {
+    // 编辑模式直接保存,不弹名称框
+    doSave(payload)
+    return
+  }
+
+  // 新增模式下,第一次点击“保存”且没填名称时,先弹输入名称框
+  if (!templateName.value || !templateName.value.trim()) {
+    pendingPayload.value = payload
+    showNameDialog.value = true
+    return
+  }
 
-/*
-* [
-*  {
-*     tpl_url:"",
-*     image_path:"",
-*     name:"",
-*     width:"",
-*     height:"",
-*     bg_color:"",
-*  }
-* ]
-* */
-
-
-const save = (data: any) => {
-  console.log(data)
+  doSave(payload)
 }
 </script>
 

+ 12 - 2
frontend/src/views/components/marketingEdit/index.vue

@@ -125,7 +125,7 @@ export default {
   methods: {
     loadTemplate(){
       if(this.hasLoadedTpl) return
-      const tplData = JSON.parse(JSON.stringify(canvas)).map(item => ({
+      const tplData = JSON.parse(JSON.stringify(this.data)).map(item => ({
         canvas_type: item.canvas_type || 'normal',
         canvas_json: (item.canvas_type === 'model' || item.canvas_type === 'scene') ? (item.canvas_type) : (item.canvas_json || ''),
         ...item,
@@ -491,6 +491,13 @@ export default {
       }
       return style
     },
+    handleBack(){
+      if(this.$router && typeof this.$router.back === 'function'){
+        this.$router.back()
+      }else{
+        this.$emit('back')
+      }
+    },
     async handleSave(){
       // 先确保当前激活画布的快照是最新的
       this.saveCanvasSnapshot()
@@ -525,7 +532,10 @@ export default {
     },
     async createImg(){
        // 1. 组装数据源(后续可以从接口/其他页面传入)
-       const goodsData = [JSON.parse(JSON.stringify(goods)), JSON.parse(JSON.stringify(goods1)), JSON.parse(JSON.stringify(goods2))]
+       const goodsData = [
+        JSON.parse(JSON.stringify(goods)), 
+        JSON.parse(JSON.stringify(goods1)), 
+        JSON.parse(JSON.stringify(goods2))]
        const canvasJson = JSON.parse(JSON.stringify(canvas))
 
        // 2. 针对每个款号,生成所有画布图片 + 组合长图(在内存中生成 dataURL)

+ 3 - 2
frontend/src/views/components/marketingEdit/tpl/header.js

@@ -68,7 +68,8 @@ export  default function tpl(){
             <el-button class="mar-left-10"  v-if="!isEmpty"  @click="handleAdjustCanvas">调整画布</el-button>
             <el-button @click="addCanvas"  class="mar-left-10">新增画布</el-button>
             <el-button type="primary" @click="handleSave">保存</el-button>
-            <el-button type="primary" @click="createImg">生成图片</el-button>
+             <!--  <el-button type="primary" @click="createImg">生成图片</el-button> -->
+            <el-button class="mar-left-10" @click="handleBack">返回</el-button>
            </div>
            
             <!-- 新增:调整画布尺寸弹窗 -->
@@ -84,7 +85,7 @@ export  default function tpl(){
                     <el-radio label="model">模特图</el-radio>
                     <el-radio label="scene">场景图</el-radio>
                   </el-radio-group>
-                  <div style="font-size: 12px; color: #999; margin-top: 8px; line-height: 1.6;">
+                  <div style="margin-left: 20px; font-size: 12px; color: #B0B0B0; margin-top: 0px; line-height: 1.6;">
                     <div v-if="canvasForm.canvas_type === 'normal'">普通画布,可编辑内容</div>
                     <div v-else-if="canvasForm.canvas_type === 'model'">模特图占位,用于前台/后端生成时替换</div>
                     <div v-else-if="canvasForm.canvas_type === 'scene'">场景图占位,用于前台/后端生成时替换</div>