liuhairui 2 недель назад
Родитель
Сommit
d92cad9b9e
2 измененных файлов с 542 добавлено и 248 удалено
  1. 195 62
      src/view/TemplateManagement/CreateTemplate.vue
  2. 347 186
      src/view/performance/QualityAssessment/Product.vue

+ 195 - 62
src/view/TemplateManagement/CreateTemplate.vue

@@ -19,16 +19,12 @@
           <el-icon class="design-toolbar-name-icon" @click="focusTemplateNameEl"><EditPen /></el-icon>
           <el-icon class="design-toolbar-name-icon" @click="focusTemplateNameEl"><EditPen /></el-icon>
         </div>
         </div>
       </div>
       </div>
-      <!-- 左侧竖条:模版设计 / 文字 / 素材库,文字单独一栏 -->
+      <!-- 左侧竖条:添加(上传+文字) / 素材库 / AI设计 -->
       <div class="toolbar-body">
       <div class="toolbar-body">
         <nav class="toolbar-vertical-nav">
         <nav class="toolbar-vertical-nav">
           <button type="button" class="nav-item" :class="{ active: activeTab === 'design' }" @click="activeTab = 'design'">
           <button type="button" class="nav-item" :class="{ active: activeTab === 'design' }" @click="activeTab = 'design'">
-            <el-icon class="nav-item-icon"><EditPen /></el-icon>
-            <span class="nav-item-text">上传</span>
-          </button>
-          <button type="button" class="nav-item" :class="{ active: activeTab === 'text' }" @click="activeTab = 'text'">
-            <el-icon class="nav-item-icon"><Document /></el-icon>
-            <span class="nav-item-text">文字</span>
+            <el-icon class="nav-item-icon"><Plus /></el-icon>
+            <span class="nav-item-text">添加</span>
           </button>
           </button>
           <button type="button" class="nav-item" :class="{ active: activeTab === 'material' }" @click="activeTab = 'material'; fetchMaterials()">
           <button type="button" class="nav-item" :class="{ active: activeTab === 'material' }" @click="activeTab = 'material'; fetchMaterials()">
             <el-icon class="nav-item-icon"><Box /></el-icon>
             <el-icon class="nav-item-icon"><Box /></el-icon>
@@ -42,48 +38,71 @@
         <div class="toolbar-right">
         <div class="toolbar-right">
         <!-- 可滚动区固定高度,切换 tab 时画布不跳动;底部按钮固定 -->
         <!-- 可滚动区固定高度,切换 tab 时画布不跳动;底部按钮固定 -->
         <div class="toolbar-tabs-content">
         <div class="toolbar-tabs-content">
-          <div v-show="activeTab === 'design'" class="toolbar-pane toolbar-pane-scroll">
-          <!-- 操作指引 -->
-          <div class="step-guide">
-            <div class="step-guide-title">操作步骤</div>
-            <div class="step-guide-steps">
-              <div class="step-item"><span class="step-num">1</span> 上传素材图片或从「素材库」选择</div>
-              <div class="step-item"><span class="step-num">2</span> 在「文字」标签中点击 标题/副标题/正文 添加文本</div>
-              <div class="step-item"><span class="step-num">3</span> 在画布上拖拽、调整图层</div>
-              <div class="step-item"><span class="step-num">4</span> 完成设计后 点击底部「生成模版」</div>
+          <!-- 添加:操作步骤 + 上传素材图 + 文字(标题/副标题/正文);点击文字「更多」进入详情 -->
+          <div v-show="activeTab === 'design'" class="toolbar-pane toolbar-pane-scroll add-tab-pane">
+            <!-- 文字更多详情:与素材库「更多」一致,进入详情后显示返回 + 内容 -->
+            <!-- 文字更多详情:与素材库详情同一套样式(返回 + 标题「文字」一行) -->
+            <div v-if="addTextViewMode === 'more'" class="materials-detail add-text-more-detail">
+              <div class="materials-detail-header">
+                <button type="button" class="materials-back" @click="backTextAddList">
+                  <el-icon><ArrowLeft /></el-icon>
+                </button>
+                <span class="materials-detail-title">文字</span>
+              </div>
+              <div class="text-add-btns">
+                <button type="button" class="text-add-btn text-add-btn-title" @click="addTextLayer('title')">标题</button>
+                <button type="button" class="text-add-btn text-add-btn-subtitle" @click="addTextLayer('subtitle')">副标题</button>
+                <button type="button" class="text-add-btn text-add-btn-body" @click="addTextLayer('body')">正文</button>
+              </div>
             </div>
             </div>
-          </div>
 
 
-          <el-upload
-            class="custom-upload"
-            drag
-            :show-file-list="false"
-            :before-upload="beforeUpload"
-            :http-request="handleUpload"
-            accept="image/png,image/jpg,image/webp"
-            multiple
-          >
-            <div class="upload-main">
-              <div class="upload-inner-button">
-                <el-icon class="custom-upload-icon">
-                  <Upload />
-                </el-icon>
-                <span class="upload-inner-text">上传素材图</span>
+            <template v-else>
+              <div class="step-guide">
+                <div class="step-guide-title">操作步骤</div>
+                <div class="step-guide-steps">
+                  <div class="step-item"><span class="step-num">1</span> 上传素材图片或从「素材库」选择</div>
+                  <div class="step-item"><span class="step-num">2</span> 在下方点击 标题/副标题/正文 添加文本</div>
+                  <div class="step-item"><span class="step-num">3</span> 在画布上拖拽、调整图层</div>
+                  <div class="step-item"><span class="step-num">4</span> 完成设计后 点击底部「生成模版」</div>
+                </div>
               </div>
               </div>
-            </div>
-            <div class="el-upload__tip">
-              拖拽或点击上传PNG 10M以内
-            </div>
-          </el-upload>
-          </div>
-          <!-- 文字:单独一栏,点击添加文本 + 标题/副标题/正文 -->
-          <div v-show="activeTab === 'text'" class="toolbar-pane toolbar-pane-scroll text-tab-pane">
-            <p class="text-add-heading">点击添加文本</p>
-            <div class="text-add-btns">
-              <button type="button" class="text-add-btn text-add-btn-title" @click="addTextLayer('title')">标题</button>
-              <button type="button" class="text-add-btn" @click="addTextLayer('subtitle')">副标题</button>
-              <button type="button" class="text-add-btn" @click="addTextLayer('body')">正文</button>
-            </div>
+
+              <el-upload
+                class="custom-upload"
+                drag
+                :show-file-list="false"
+                :before-upload="beforeUpload"
+                :http-request="handleUpload"
+                accept="image/png,image/jpg,image/webp"
+                multiple
+              >
+                <div class="upload-main">
+                  <div class="upload-inner-button">
+                    <el-icon class="custom-upload-icon">
+                      <Upload />
+                    </el-icon>
+                    <span class="upload-inner-text">上传素材图</span>
+                  </div>
+                </div>
+                <div class="el-upload__tip">
+                  拖拽或点击上传PNG 10M以内
+                </div>
+              </el-upload>
+
+              <div class="add-text-section">
+                <div class="text-add-header">
+                  <span class="text-add-heading">文字</span>
+                  <button type="button" class="materials-more" @click="openTextAddMore">
+                    更多 &gt;
+                  </button>
+                </div>
+                <div class="text-add-btns">
+                  <button type="button" class="text-add-btn text-add-btn-title" @click="addTextLayer('title')">标题</button>
+                  <button type="button" class="text-add-btn text-add-btn-subtitle" @click="addTextLayer('subtitle')">副标题</button>
+                  <button type="button" class="text-add-btn text-add-btn-body" @click="addTextLayer('body')">正文</button>
+                </div>
+              </div>
+            </template>
           </div>
           </div>
           <!-- AI 工具:需选中画布中的图片,未选中时提示 -->
           <!-- AI 工具:需选中画布中的图片,未选中时提示 -->
           <div v-show="activeTab === 'ai'" class="toolbar-pane toolbar-pane-scroll ai-tools-pane">
           <div v-show="activeTab === 'ai'" class="toolbar-pane toolbar-pane-scroll ai-tools-pane">
@@ -333,7 +352,7 @@
                 'drag-over': dragState.dragOverId === layer.id,
                 'drag-over': dragState.dragOverId === layer.id,
                 'locked': layer.locked
                 'locked': layer.locked
               }"
               }"
-              :draggable="!layer.locked"
+              :draggable="!layer.locked && layerNameEditingId !== layer.id"
               @click="selectLayer(layer.id)"
               @click="selectLayer(layer.id)"
               @dragstart="handleDragStart($event, layer)"
               @dragstart="handleDragStart($event, layer)"
               @dragover="handleDragOver($event, layer)"
               @dragover="handleDragOver($event, layer)"
@@ -350,7 +369,18 @@
                 <img v-if="layer.url" :src="layer.url" class="layer-card-thumb" alt="" />
                 <img v-if="layer.url" :src="layer.url" class="layer-card-thumb" alt="" />
                 <div v-else class="layer-card-thumb img-placeholder"><el-icon><Picture /></el-icon></div>
                 <div v-else class="layer-card-thumb img-placeholder"><el-icon><Picture /></el-icon></div>
               </template>
               </template>
-              <span class="layer-card-name">{{ layer.name || (layer.type === 'text' ? '文字' : '图片') }}</span>
+              <div class="layer-card-name-wrap" draggable="false" @dragstart.stop>
+                <el-input
+                  v-model="layer.name"
+                  class="layer-card-name-input"
+                  size="small"
+                  :placeholder="layer.type === 'text' ? '文字' : '图片'"
+                  @click.stop
+                  @focus="layerNameEditingId = layer.id"
+                  @blur="layerNameEditingId = null"
+                />
+                <el-icon class="layer-card-name-edit-icon" title="编辑名称" @click.stop="focusLayerNameInput($event)"><EditPen /></el-icon>
+              </div>
               <el-icon class="layer-card-action" :title="layer.visible ? '隐藏' : '显示'" @click.stop="toggleLayerVisibility(layer.id)">
               <el-icon class="layer-card-action" :title="layer.visible ? '隐藏' : '显示'" @click.stop="toggleLayerVisibility(layer.id)">
                 <View v-if="layer.visible" />
                 <View v-if="layer.visible" />
                 <Hide v-else />
                 <Hide v-else />
@@ -370,7 +400,7 @@
         <!-- 调整:画布尺寸、图层属性、文字属性 全部展开不隐藏 -->
         <!-- 调整:画布尺寸、图层属性、文字属性 全部展开不隐藏 -->
         <div v-show="rightPanelTab === 'props'" class="right-section right-section-props">
         <div v-show="rightPanelTab === 'props'" class="right-section right-section-props">
           <section class="props-block">
           <section class="props-block">
-            <h4 class="props-block-title">画布尺寸</h4>
+            <h4 class="props-block-title">生成图片尺寸</h4>
             <div class="property-item">
             <div class="property-item">
               <span>比例</span>
               <span>比例</span>
               <el-select v-model="canvasRatio" size="small" style="flex: 1;" @change="handleCanvasRatioChange">
               <el-select v-model="canvasRatio" size="small" style="flex: 1;" @change="handleCanvasRatioChange">
@@ -460,6 +490,10 @@
                 <span>背景</span>
                 <span>背景</span>
                 <el-color-picker v-model="selectedLayer.backgroundColor" size="small" show-alpha />
                 <el-color-picker v-model="selectedLayer.backgroundColor" size="small" show-alpha />
               </div>
               </div>
+              <div class="property-item">
+                <span>背景圆角</span>
+                <el-slider v-model="selectedLayer.backgroundBorderRadius" :min="0" :max="50" :step="1" />
+              </div>
               <div class="property-item">
               <div class="property-item">
                 <span>对齐</span>
                 <span>对齐</span>
                 <el-radio-group v-model="selectedLayer.textAlign" size="small">
                 <el-radio-group v-model="selectedLayer.textAlign" size="small">
@@ -540,8 +574,7 @@ const rightPanelTab = ref('layer')
 const aiPromptDialogVisible = ref(false)
 const aiPromptDialogVisible = ref(false)
 const aiPromptToolName = ref('')
 const aiPromptToolName = ref('')
 const aiToolList = [
 const aiToolList = [
-  { key: 'cutout', name: '智能抠图', desc: '一键抠图,快速去除背景。', iconClass: 'ai-icon-cutout', iconText: '抠图' },
-  { key: 'hd', name: '变清晰', desc: '超清画质重生,告别渣画质。', iconClass: 'ai-icon-hd', iconText: '清' },
+  { key: 'hd', name: 'Ai高清', desc: '超清画质重生,告别渣画质。', iconClass: 'ai-icon-hd', iconText: '清' },
   { key: 'expand', name: 'AI扩图', desc: '在原图基础上对画面进行拓展。', iconClass: 'ai-icon-expand', iconText: '扩图' },
   { key: 'expand', name: 'AI扩图', desc: '在原图基础上对画面进行拓展。', iconClass: 'ai-icon-expand', iconText: '扩图' },
   { key: 'generate', name: 'AI生图', desc: '输入文字或参考图,AI根据描述内容生成新的素材图片。', iconClass: 'ai-icon-generate', iconText: '文图' }
   { key: 'generate', name: 'AI生图', desc: '输入文字或参考图,AI根据描述内容生成新的素材图片。', iconClass: 'ai-icon-generate', iconText: '文图' }
 ]
 ]
@@ -624,6 +657,10 @@ const dragState = reactive({
   dragOverId: null,
   dragOverId: null,
   draggedLayer: null
   draggedLayer: null
 })
 })
+// 正在编辑名称的图层 id,该图层在编辑时不可拖拽
+const layerNameEditingId = ref(null)
+// 添加-文字视图:list=主列表,more=点击「更多」进入的详情(与素材库更多一致)
+const addTextViewMode = ref('list')
 
 
 // 画布比例配置 - 使用更小的默认尺寸以适应屏幕
 // 画布比例配置 - 使用更小的默认尺寸以适应屏幕
 const ratioConfig = {
 const ratioConfig = {
@@ -732,6 +769,7 @@ const useTemplate = async (template) => {
         textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
         textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
         lineHeight: Number(row.line_height || 1.5) || 1.5,
         lineHeight: Number(row.line_height || 1.5) || 1.5,
         letterSpacing: Number(row.letter_spacing || 0) || 0,
         letterSpacing: Number(row.letter_spacing || 0) || 0,
+        backgroundBorderRadius: Number(row.background_border_radius ?? 0) || 0,
         originalWidth: width,
         originalWidth: width,
         originalHeight: height,
         originalHeight: height,
         zIndex: Number(row.z_index || 0) || 0,
         zIndex: Number(row.z_index || 0) || 0,
@@ -803,6 +841,7 @@ const useAsTemplate = async (template) => {
         textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
         textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
         lineHeight: Number(row.line_height || 1.5) || 1.5,
         lineHeight: Number(row.line_height || 1.5) || 1.5,
         letterSpacing: Number(row.letter_spacing || 0) || 0,
         letterSpacing: Number(row.letter_spacing || 0) || 0,
+        backgroundBorderRadius: Number(row.background_border_radius ?? 0) || 0,
         originalWidth: width,
         originalWidth: width,
         originalHeight: height,
         originalHeight: height,
         zIndex: Number(row.z_index || 0) || 0,
         zIndex: Number(row.z_index || 0) || 0,
@@ -932,6 +971,7 @@ const addTextLayer = (preset) => {
     color: '#000000',
     color: '#000000',
     textAlign: 'left',
     textAlign: 'left',
     backgroundColor: 'transparent',
     backgroundColor: 'transparent',
+    backgroundBorderRadius: 0,
     lineHeight: 1.5,
     lineHeight: 1.5,
     letterSpacing: 0
     letterSpacing: 0
   }
   }
@@ -1133,6 +1173,13 @@ const backToMaterialCategory = () => {
   materialViewMode.value = 'category'
   materialViewMode.value = 'category'
 }
 }
 
 
+const openTextAddMore = () => {
+  addTextViewMode.value = 'more'
+}
+const backTextAddList = () => {
+  addTextViewMode.value = 'list'
+}
+
 // 标签页点击事件
 // 标签页点击事件
 const handleTabClick = (tab) => {
 const handleTabClick = (tab) => {
   console.log('Tab clicked:', tab.props.name)
   console.log('Tab clicked:', tab.props.name)
@@ -1264,6 +1311,7 @@ const saveTemplate = async () => {
       lineHeight: layer.lineHeight,
       lineHeight: layer.lineHeight,
       letterSpacing: layer.letterSpacing,
       letterSpacing: layer.letterSpacing,
       textAlign: layer.textAlign,
       textAlign: layer.textAlign,
+      background_border_radius: layer.backgroundBorderRadius ?? 0,
       originalWidth: layer.originalWidth,
       originalWidth: layer.originalWidth,
       originalHeight: layer.originalHeight
       originalHeight: layer.originalHeight
     }))
     }))
@@ -1314,6 +1362,7 @@ const getTextStyle = (layer) => {
     color: layer.color,
     color: layer.color,
     textAlign: layer.textAlign,
     textAlign: layer.textAlign,
     backgroundColor: layer.backgroundColor,
     backgroundColor: layer.backgroundColor,
+    borderRadius: (layer.backgroundBorderRadius ?? 0) + 'px',
     lineHeight: layer.lineHeight,
     lineHeight: layer.lineHeight,
     letterSpacing: layer.letterSpacing + 'px',
     letterSpacing: layer.letterSpacing + 'px',
     padding: '4px 8px',
     padding: '4px 8px',
@@ -1447,8 +1496,24 @@ const moveLayerDown = () => {
   }
   }
 }
 }
 
 
+// 点击图层名称后的编辑图标时,聚焦到该行输入框
+const focusLayerNameInput = (e) => {
+  const wrap = e.currentTarget?.closest('.layer-card-name-wrap')
+  const input = wrap?.querySelector('.layer-card-name-input input')
+  if (input) input.focus()
+}
+
 // 拖拽排序相关函数
 // 拖拽排序相关函数
 const handleDragStart = (e, layer) => {
 const handleDragStart = (e, layer) => {
+  // 仅在「正在编辑图层名称」时禁止拖拽(名称输入框已聚焦),方便全选/编辑文字;未聚焦时仍可从名称区域拖拽
+  const wrap = e.target.closest('.layer-card-name-wrap')
+  if (wrap) {
+    const input = wrap.querySelector('.layer-card-name-input input')
+    if (input && document.activeElement === input) {
+      e.preventDefault()
+      return
+    }
+  }
   // 如果图层被锁定,禁止拖拽排序
   // 如果图层被锁定,禁止拖拽排序
   if (layer.locked) {
   if (layer.locked) {
     e.preventDefault()
     e.preventDefault()
@@ -2208,37 +2273,63 @@ const handleCanvasWheel = (e) => {
   padding: 10px 10px 12px;
   padding: 10px 10px 12px;
   border: 1px solid #eee;
   border: 1px solid #eee;
 }
 }
-/* 文字标签页:点击添加文本 + 标题/副标题/正文,虚线框样式 */
-.text-tab-pane {
-  padding: 16px 12px;
+/* 文字标签页:点击添加文本 + 标题/副标题/正文,实线框 + 浅灰底 */
+.add-tab-pane {
+  padding: 12px 20px;
+}
+.add-text-section {
+  margin-top: 10px;
 }
 }
+.text-add-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+/* 与素材库分组标题一致:12px、600、#303133 */
 .text-add-heading {
 .text-add-heading {
-  font-size: 13px;
-  color: #909399;
-  margin: 0 0 14px 0;
+  font-size: 12px;
+  font-weight: 600;
+  color: #303133;
+  margin: 0;
 }
 }
 .text-add-btns {
 .text-add-btns {
   display: flex;
   display: flex;
   gap: 10px;
   gap: 10px;
 }
 }
+.text-add-btns-more {
+  margin-top: 10px;
+}
 .text-add-btn {
 .text-add-btn {
   flex: 1;
   flex: 1;
   padding: 10px 12px;
   padding: 10px 12px;
   font-size: 14px;
   font-size: 14px;
   color: #303133;
   color: #303133;
-  background: #fff;
-  border: 1px dashed #dcdfe6;
+  background: #f5f7fa;
+  border: 1px solid #e4e7ed;
   border-radius: 8px;
   border-radius: 8px;
   cursor: pointer;
   cursor: pointer;
   transition: background 0.2s, border-color 0.2s;
   transition: background 0.2s, border-color 0.2s;
 }
 }
 .text-add-btn:hover {
 .text-add-btn:hover {
-  background: #fafafa;
+  background: #eef1f6;
   border-color: #c0c4cc;
   border-color: #c0c4cc;
 }
 }
 .text-add-btn-title {
 .text-add-btn-title {
+  font-size: 18px;
   font-weight: 700;
   font-weight: 700;
 }
 }
+.text-add-btn-subtitle {
+  font-size: 15px;
+  font-weight: 500;
+}
+.text-add-btn-body {
+  font-size: 14px;
+  font-weight: 400;
+}
+.text-add-btn-effect {
+  font-size: 13px;
+}
 
 
 .design-section-title {
 .design-section-title {
   margin: 0 0 8px 0;
   margin: 0 0 8px 0;
@@ -2903,12 +2994,12 @@ const handleCanvasWheel = (e) => {
 }
 }
 
 
 .text-layer {
 .text-layer {
-  border: 1px dashed #ccc;
+  border: 1px solid transparent;
 }
 }
 
 
 .text-layer.selected {
 .text-layer.selected {
   border-color: #409eff;
   border-color: #409eff;
-  border-style: solid;
+  box-shadow: 0 0 0 1px #409eff;
 }
 }
 
 
 .resize-handle {
 .resize-handle {
@@ -3160,6 +3251,48 @@ const handleCanvasWheel = (e) => {
   white-space: nowrap;
   white-space: nowrap;
 }
 }
 
 
+.layer-card-name-wrap {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.layer-card-name-input {
+  flex: 1;
+  min-width: 0;
+}
+.layer-card-name-edit-icon {
+  flex-shrink: 0;
+  font-size: 14px;
+  color: #909399;
+  cursor: pointer;
+  padding: 2px;
+}
+.layer-card-name-edit-icon:hover {
+  color: #409eff;
+}
+.layer-card-name-input :deep(.el-input__wrapper) {
+  padding: 0 6px;
+  box-shadow: none;
+  background: transparent;
+  min-height: 24px;
+}
+.layer-card-name-input :deep(.el-input__wrapper:hover),
+.layer-card-name-input :deep(.el-input__wrapper.is-focus) {
+  background: #f5f7fa;
+  box-shadow: none;
+}
+.layer-card-name-input :deep(.el-input__inner) {
+  font-size: 13px;
+  color: #303133;
+  height: 24px;
+  line-height: 24px;
+}
+.layer-card-name-input :deep(.el-input__inner::placeholder) {
+  color: #c0c4cc;
+}
+
 .layer-card-action {
 .layer-card-action {
   flex-shrink: 0;
   flex-shrink: 0;
   font-size: 16px;
   font-size: 16px;

+ 347 - 186
src/view/performance/QualityAssessment/Product.vue

@@ -101,30 +101,17 @@
       <el-icon><Close /></el-icon>关闭
       <el-icon><Close /></el-icon>关闭
     </el-button>
     </el-button>
   </div>
   </div>
-  <div class="image-edit-container" style="display: flex; height: 94vh; overflow: hidden; padding-top: 20px;">
+  <div class="image-edit-container">
     <!-- 左侧:原图新图 + 输入框 -->
     <!-- 左侧:原图新图 + 输入框 -->
-    <div class="left-column" style="flex: 0.7; display: flex; flex-direction: column; gap: 15px; padding: 15px; overflow-y: hidden;">
+    <div class="left-column">
       <!-- 标题 -->
       <!-- 标题 -->
       <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 10px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;">②出图</div>
       <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 10px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;">②出图</div>
         <!-- 图片对比区域 -->
         <!-- 图片对比区域 -->
-      <div class="image-comparison-section" style="display: flex; gap: 20px;">
+      <div class="image-comparison-section">
         <!-- 原图区域 -->
         <!-- 原图区域 -->
         <div class="image-preview" style="flex: 1; min-width: 120px; display: flex; flex-direction: column; align-items: center;">
         <div class="image-preview" style="flex: 1; min-width: 120px; display: flex; flex-direction: column; align-items: center;">
           <h3 style="margin-top: 0; margin-bottom: 8px; font-size: 14px; font-weight: bold; color: #303133; align-self: flex-start;">上传商品白底图</h3>
           <h3 style="margin-top: 0; margin-bottom: 8px; font-size: 14px; font-weight: bold; color: #303133; align-self: flex-start;">上传商品白底图</h3>
-                   <el-button 
-            type="text" 
-            size="small"
-            @click="toggleProductExample"
-            style="padding: 0; font-size: 14px; font-weight: normal; color: #409eff; align-self: flex-end; margin-bottom: 10px;margin-right: 110px;"
-          >
-            <template #icon>
-              <el-icon :size="16">
-                <component :is="productExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
-              </el-icon>
-            </template>
-            商品图例
-          </el-button>
-          <div v-if="editFormData.original_image_url" style="width:200px; height: 200px; display: flex; justify-content: center; align-items: center; background-color: #f9f9f9; border-radius: 4px; overflow: hidden; border: 1px solid #e4e7ed;">
+          <div v-if="editFormData.original_image_url" class="upload-image-box">
             <el-image
             <el-image
               :src="formatImageUrl(editFormData.original_image_url)"
               :src="formatImageUrl(editFormData.original_image_url)"
               :preview-src-list="[formatImageUrl(editFormData.original_image_url)]"
               :preview-src-list="[formatImageUrl(editFormData.original_image_url)]"
@@ -132,20 +119,23 @@
               fit="contain"
               fit="contain"
               preview-teleported/>
               preview-teleported/>
           </div>
           </div>
-          <div v-else class="image-placeholder" style="width: 100px; height: 100px; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #f9f9f9; border-radius: 4px; border: 1px solid #e4e7ed;">
+          <div v-else class="image-placeholder image-placeholder-small">
             <el-icon :size="30"><Picture /></el-icon>
             <el-icon :size="30"><Picture /></el-icon>
             <span style="margin-top: 5px; font-size: 10px;">暂无原图</span>
             <span style="margin-top: 5px; font-size: 10px;">暂无原图</span>
           </div>
           </div>
         </div>
         </div>
 
 
-        <!-- 按钮区域 -->
-        <div style="display: flex; align-items: center; gap: 20px; margin-left:480px">
-          <el-button 
-            type="text" 
-            size="small"
-            @click="toggleCaseExample"
-            style="padding: 0; font-size: 14px; font-weight: normal; color: #409eff;"
-          >
+        <!-- 商品图例、案例:右侧一列,与图片等高上下对齐,不用固定 margin 避免小屏跑偏 -->
+        <div class="case-links-column">
+          <el-button type="text" size="small" @click="toggleProductExample" class="case-link-btn">
+            <template #icon>
+              <el-icon :size="16">
+                <component :is="productExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
+              </el-icon>
+            </template>
+            商品图例
+          </el-button>
+          <el-button type="text" size="small" @click="toggleCaseExample" class="case-link-btn">
             <template #icon>
             <template #icon>
               <el-icon :size="16">
               <el-icon :size="16">
                 <component :is="caseExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
                 <component :is="caseExampleVisible ? 'ArrowDown' : 'ArrowRight'" />
@@ -154,8 +144,6 @@
             案例
             案例
           </el-button>
           </el-button>
         </div>
         </div>
-
-        
       </div>
       </div>
           
           
     <!-- 输入框区域 -->
     <!-- 输入框区域 -->
@@ -237,16 +225,16 @@
     </div>
     </div>
     </div>
     </div>
     
     
-   <!-- 中间:产品信息和历史记录 -->
-<div class="middle-column" style="width: 430px; border-left: 1px solid #e4e7ed; border-right: 1px solid #e4e7ed; padding: 15px 20px; position: relative;">
+   <!-- 中间:产品信息和历史记录(保证两侧边框可见,不与其他列重叠) -->
+<div class="middle-column">
   <!-- 历史记录覆盖层 -->
   <!-- 历史记录覆盖层 -->
   <div v-if="showHistoryPanel" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
   <div v-if="showHistoryPanel" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
     <div style="width: 95%; height: 100%; overflow-y: auto;">
     <div style="width: 95%; height: 100%; overflow-y: auto;">
       <div style="padding: 20px;">
       <div style="padding: 20px;">
         <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
         <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
           <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">图片历史记录</h3>
           <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">图片历史记录</h3>
-          <el-button type="text" size="small" @click="toggleHistoryPanel">
-            <el-icon><Close /></el-icon>
+          <el-button type="danger" size="small" @click="toggleHistoryPanel">
+            <el-icon><Close /></el-icon>关闭
           </el-button>
           </el-button>
         </div>
         </div>
         
         
@@ -308,7 +296,7 @@
     <!-- 左侧:历史记录面板 (已移至覆盖层) -->
     <!-- 左侧:历史记录面板 (已移至覆盖层) -->
     
     
     <!-- 右侧:原始内容 -->
     <!-- 右侧:原始内容 -->
-    <div class="original-content" style="flex: 1; overflow-y: auto;">
+    <div class="original-content" style="flex: 1; overflow-y: auto; min-width: 0;">
       <!-- 商品图例覆盖层 -->
       <!-- 商品图例覆盖层 -->
       <div v-if="productExampleVisible" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
       <div v-if="productExampleVisible" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
         <div style="width: 95%; height: 100%; overflow-y: auto;">
         <div style="width: 95%; height: 100%; overflow-y: auto;">
@@ -467,7 +455,7 @@
       </div>
       </div>
       
       
       <!-- 案例覆盖层 -->
       <!-- 案例覆盖层 -->
-      <div v-if="caseExampleVisible" style="position: absolute; top: 0; left: 0; width: 600px; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
+      <div v-if="caseExampleVisible" style="position: absolute; top: 0; left: 0; right: 0; width: 100%; max-width: 600px; height: 100%; background: #ffffff; border-radius: 0; z-index: 1000; display: flex; justify-content: center; align-items: flex-start; box-shadow: 4px 0 20px rgba(0,0,0,0.15); border-right: 1px solid #e4e7ed;">
         <div style="width: 95%; height: 100%; overflow-y: auto;">
         <div style="width: 95%; height: 100%; overflow-y: auto;">
           <div style="padding: 20px;">
           <div style="padding: 20px;">
             <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
             <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e4e7ed;">
@@ -687,13 +675,13 @@
         </div>
         </div>
       </div>
       </div>
       
       
-      <div style="height: 100%; display: flex; flex-direction: column;">
+      <div style="height: 100%; display: flex; flex-direction: column; width: 100%; min-width: 0; align-items: center;">
         <!-- 上:留出的空白区域 -->
         <!-- 上:留出的空白区域 -->
-        <div style="height: 90px; background-image: url('/src/assets/top-bg.png'); background-size: cover; background-position: center; border-top-left-radius: 8px; border-top-right-radius: 8px;"></div>
+        <div style="width: 100%; height: 90px; background-image: url('/src/assets/top-bg.png'); background-size: cover; background-position: center; border-top-left-radius: 8px; border-top-right-radius: 8px;"></div>
         <!-- 上中:图片显示区域 -->
         <!-- 上中:图片显示区域 -->
-        <div style="display: flex; flex-direction: column; align-items: center;">
+        <div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
           <!-- 产品图片显示(默认) -->
           <!-- 产品图片显示(默认) -->
-          <div style="width: 430px; height: 290px; display: flex; justify-content: center; align-items: center; background-color: white; border-radius: 4px;">
+          <div style="width: 100%; height: 290px; max-width: 430px; display: flex; justify-content: center; align-items: center; background-color: white; border-radius: 4px;">
             <el-carousel v-if="newImages.length > 0" indicator-position="outside" style="width: 100%; height: 100%;">
             <el-carousel v-if="newImages.length > 0" indicator-position="outside" style="width: 100%; height: 100%;">
               <el-carousel-item v-for="(image, index) in newImages" :key="index">
               <el-carousel-item v-for="(image, index) in newImages" :key="index">
                 <el-image
                 <el-image
@@ -702,7 +690,11 @@
                   style="width: 100%; height: 100%; object-fit: fill;"
                   style="width: 100%; height: 100%; object-fit: fill;"
                   fit="fill"
                   fit="fill"
                   preview-teleported
                   preview-teleported
-                />
+                >
+                  <template #error>
+                    <div style="width: 100%; height: 100%; background-color: white;" />
+                  </template>
+                </el-image>
               </el-carousel-item>
               </el-carousel-item>
             </el-carousel>
             </el-carousel>
             <el-image
             <el-image
@@ -712,15 +704,19 @@
               style="width: 100%; height: 100%; object-fit: fill;"
               style="width: 100%; height: 100%; object-fit: fill;"
               fit="fill"
               fit="fill"
               preview-teleported
               preview-teleported
-            />
+            >
+              <template #error>
+                <div style="width: 100%; height: 100%; background-color: white;" />
+              </template>
+            </el-image>
             <div v-else-if="!showProductImage || !editFormData.new_image_url" class="image-placeholder">
             <div v-else-if="!showProductImage || !editFormData.new_image_url" class="image-placeholder">
               <el-icon :size="60"><Picture /></el-icon>
               <el-icon :size="60"><Picture /></el-icon>
-              <span style="margin-top: 10px; display: block;">暂无产品图片</span>
+              <span style="margin-top: 10px; display: block;">暂无效果图</span>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-        <!-- 中:商品信息表单 -->
-        <div class="product-info-container" style="width: 430px; height: 320px;border: 12px solid #f6E0dd; border-radius: 4px; padding: 15px;">
+        <!-- 中:商品信息表单(宽度 100% 含边框,避免右侧边框被裁) -->
+        <div class="product-info-container" style="width: 100%; max-width: 430px; height: 320px; border: 12px solid #f6E0dd; border-radius: 4px; padding: 15px; box-sizing: border-box;">
           <div class="product-info-item">
           <div class="product-info-item">
             <span class="product-info-label">商品条码:</span>
             <span class="product-info-label">商品条码:</span>
             <span class="product-info-value">{{ editFormData.barcode || '-' }}</span>
             <span class="product-info-value">{{ editFormData.barcode || '-' }}</span>
@@ -747,14 +743,14 @@
           </div>
           </div>
         </div>
         </div>
         <!-- 下:空出来的部分 -->
         <!-- 下:空出来的部分 -->
-        <div style="width: 430px; height: 61px;"></div>
+        <div style="width: 100%; max-width: 430px; height: 61px;"></div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
 </div>
 </div>
     
     
-    <!-- 右侧:模板列表 -->
-    <div class="right-column" style="flex: 1;">
+    <!-- 右侧:模版列表(与中间列左右留白一致) -->
+    <div class="right-column">
       <div class="right-template" style="height: 100%; display: flex; flex-direction: column;">
       <div class="right-template" style="height: 100%; display: flex; flex-direction: column;">
         <!-- 标题 -->
         <!-- 标题 -->
         <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 15px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;margin-top:15px">①模版</div>
         <div style="font-size: 18px; font-weight: bold; color: white; margin-bottom: 15px; background-color: #404040; padding: 10px; border-radius: 4px; text-align: center;margin-top:15px">①模版</div>
@@ -795,7 +791,7 @@
                     <el-image
                     <el-image
                       :src="template.template_image_url"
                       :src="template.template_image_url"
                       style="width: 100%; height: 100%;"
                       style="width: 100%; height: 100%;"
-                      fit="cover"
+                      fit="contain"
                     >
                     >
                       <template #error>
                       <template #error>
                         <div class="thumbnail-error">
                         <div class="thumbnail-error">
@@ -833,7 +829,8 @@
           <!-- 图片预览组件 -->
           <!-- 图片预览组件 -->
           <el-image-viewer
           <el-image-viewer
             v-if="previewVisible"
             v-if="previewVisible"
-            :url-list="[previewImageUrl]"
+            :url-list="previewImageUrl ? [previewImageUrl] : []"
+            :hide-on-click-modal="true"
             @close="previewVisible = false"
             @close="previewVisible = false"
           />
           />
         </div>
         </div>
@@ -1007,46 +1004,54 @@
 
 
       <!-- 产品图片 -->
       <!-- 产品图片 -->
       <el-form-item label="产品图片" prop="product_img">
       <el-form-item label="产品图片" prop="product_img">
-        <div class="upload-container">
-          <!-- 图片展示 -->
-          <div v-if="productForm.product_img" class="image-preview">
-            <el-image
-              :src="productForm.product_img"
-              :preview-src-list="[productForm.product_img]"
-              fit="cover"
-              class="preview-image"
-            />
-            <div class="image-actions">
-              <el-button type="text" @click="handleViewImage">查看</el-button>
-              <el-button type="text" @click="handleRemoveImage">删除</el-button>
+        <div class="add-dialog-upload-wrap">
+          <!-- 已上传:预览 + 替换/删除 -->
+          <div v-if="productForm.product_img" class="add-dialog-preview-box">
+            <div class="add-dialog-preview-img">
+              <img
+                v-if="addDialogPreviewSrc"
+                :src="addDialogPreviewSrc"
+                class="add-dialog-preview-img-inner"
+                alt="产品图片预览"
+              />
+            </div>
+            <div class="add-dialog-preview-actions">
+              <el-upload
+                :show-file-list="false"
+                :before-upload="handleAddDialogSelectImage"
+                accept="image/jpeg,image/png,image/jpg,image/webp"
+              >
+                <el-button type="primary" size="small">
+                  <el-icon><Upload /></el-icon>
+                  替换
+                </el-button>
+              </el-upload>
+              <el-button type="danger" size="small" plain @click="handleRemoveImage">
+                删除
+              </el-button>
             </div>
             </div>
           </div>
           </div>
-          
-          <!-- 上传区域 -->
+          <!-- 未上传:点击上传区域 -->
           <el-upload
           <el-upload
             v-else
             v-else
-            class="image-uploader"
-            :action="uploadUrl"
+            class="add-dialog-upload-area"
             :show-file-list="false"
             :show-file-list="false"
-            :on-success="handleUploadSuccess"
-            :on-error="handleUploadError"
-            :before-upload="beforeUpload"
-            accept="image/*"
+            :before-upload="handleAddDialogSelectImage"
+            accept="image/jpeg,image/png,image/jpg,image/webp"
           >
           >
-            <el-button type="primary" plain>
-              <el-icon><Plus /></el-icon>
-              上传图片
-            </el-button>
-            <div class="upload-tip">支持 JPG、PNG 格式,大小不超过 5MB</div>
+            <div class="add-dialog-upload-inner">
+              <el-icon class="add-dialog-upload-icon"><Plus /></el-icon>
+              <span class="add-dialog-upload-text">点击上传图片</span>
+              <span class="add-dialog-upload-tip">支持 JPG、PNG,不超过 5MB</span>
+            </div>
           </el-upload>
           </el-upload>
         </div>
         </div>
-        <div class="form-tip">请上传产品图片</div>
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
 
 
     <template #footer>
     <template #footer>
       <span class="dialog-footer">
       <span class="dialog-footer">
-        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button @click="handleClose">取消</el-button>
         <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
         <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
           确定
           确定
         </el-button>
         </el-button>
@@ -1060,12 +1065,12 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive, toRaw, onMounted ,watch } from 'vue'
+import { ref, reactive, computed, toRaw, onMounted, watch } from 'vue'
 import { ElMessage, ElLoading} from 'element-plus'
 import { ElMessage, ElLoading} from 'element-plus'
 import { getTable, imageToText, Template_ids,txttoimg_moxing,GetHttpUrl,txttoimg_update, getSide,merchantGetab,productList,productDetail,
 import { getTable, imageToText, Template_ids,txttoimg_moxing,GetHttpUrl,txttoimg_update, getSide,merchantGetab,productList,productDetail,
   product_template,GetTxtToTxt,GetProductFind,productAdd,getMerchantId,GetImageStatus } from '@/api/mes/job'
   product_template,GetTxtToTxt,GetProductFind,productAdd,getMerchantId,GetImageStatus } from '@/api/mes/job'
 import { useUserStore } from '@/pinia/modules/user'
 import { useUserStore } from '@/pinia/modules/user'
-import {  ZoomIn, Camera,SuccessFilled, WarningFilled, More, ArrowRight, ArrowDown, Loading } from '@element-plus/icons-vue'
+import {  ZoomIn, Camera, SuccessFilled, WarningFilled, More, ArrowRight, ArrowDown, Loading, Plus, Upload, Picture } from '@element-plus/icons-vue'
 import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
 import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
 
 
 
 
@@ -1394,10 +1399,18 @@ const handleTemplateTreeClick = (data) => {
 }
 }
 
 
 const onRowDblClick = async (row) => {
 const onRowDblClick = async (row) => {
+    // 打开新产品前先清空上一产品的历史与展示,避免残留
+    showHistoryPanel.value = false
+    newImages.value = []
+    templateTreeData.value = []
+    filteredImages.value = []
+    currentTemplateId.value = null
+    currentTemplateName.value = ''
+
     // 设置编辑表单ID
     // 设置编辑表单ID
     editFormData.id = row.id
     editFormData.id = row.id
     editDialogVisible.value = true
     editDialogVisible.value = true
-    
+
     // 1. 获取产品详情并设置原图
     // 1. 获取产品详情并设置原图
     const detailResponse = await productDetail({ id: row.id })
     const detailResponse = await productDetail({ id: row.id })
     if (detailResponse.code === 0 && detailResponse.data) {
     if (detailResponse.code === 0 && detailResponse.data) {
@@ -1406,18 +1419,16 @@ const onRowDblClick = async (row) => {
       editFormData.original_name = detailResponse.data['产品名称']
       editFormData.original_name = detailResponse.data['产品名称']
       editFormData.product_name = detailResponse.data['产品名称']
       editFormData.product_name = detailResponse.data['产品名称']
       editFormData.product_code = detailResponse.data['产品编码']
       editFormData.product_code = detailResponse.data['产品编码']
-      
+
       // 如果有新图片,自动显示到产品信息区域
       // 如果有新图片,自动显示到产品信息区域
       if (detailResponse.data['产品效果图']) {
       if (detailResponse.data['产品效果图']) {
         showProductImage.value = true
         showProductImage.value = true
+      } else {
+        showProductImage.value = false
       }
       }
-      
+
       // 获取image数组数据
       // 获取image数组数据
       if (detailResponse.image && Array.isArray(detailResponse.image)) {
       if (detailResponse.image && Array.isArray(detailResponse.image)) {
-        // 清空之前的新图数据
-        newImages.value = []
-        
-        // 遍历image数组,添加到newImages
         detailResponse.image.forEach(item => {
         detailResponse.image.forEach(item => {
           if (item.product_new_img) {
           if (item.product_new_img) {
             newImages.value.push({
             newImages.value.push({
@@ -1428,20 +1439,21 @@ const onRowDblClick = async (row) => {
             })
             })
           }
           }
         })
         })
-        
+
         // 如果有image数据,默认显示第一个的product_content
         // 如果有image数据,默认显示第一个的product_content
         if (newImages.value.length > 0 && newImages.value[0].product_content) {
         if (newImages.value.length > 0 && newImages.value[0].product_content) {
           editFormData.chinese_description = newImages.value[0].product_content
           editFormData.chinese_description = newImages.value[0].product_content
         }
         }
-        
+
         // 处理模板分类树数据
         // 处理模板分类树数据
         processTemplateTreeData(detailResponse.image)
         processTemplateTreeData(detailResponse.image)
+      } else {
+        editFormData.chinese_description = ''
       }
       }
     }
     }
-    
+
     // 2. 获取所有模板数据
     // 2. 获取所有模板数据
     await fetchTemplates()
     await fetchTemplates()
-    
 }
 }
 
 
 // 存储所有模板数据
 // 存储所有模板数据
@@ -1462,7 +1474,9 @@ const fetchTemplates = async () => {
       // if (templateList.value.length > 0) {
       // if (templateList.value.length > 0) {
       //   selectTemplate(templateList.value[0])
       //   selectTemplate(templateList.value[0])
       // }
       // }
-      updateProductImage(newImages.value[0].url)
+      if (newImages.value.length > 0) {
+        updateProductImage(newImages.value[0].url)
+      }
     }
     }
 }
 }
 
 
@@ -2061,6 +2075,8 @@ const handleSearch = async () => {
 const AdddialogVisible = ref(false)
 const AdddialogVisible = ref(false)
 const productFormRef = ref()
 const productFormRef = ref()
 const submitLoading = ref(false)
 const submitLoading = ref(false)
+// 新增产品:选中的图片文件(点确定时随 productAdd 一起提交,不单独调 ImgUpload)
+const productImageFile = ref(null)
 
 
 // 表单数据
 // 表单数据
 const productForm = reactive({
 const productForm = reactive({
@@ -2068,7 +2084,7 @@ const productForm = reactive({
   product_code: '',
   product_code: '',
   create_name: '',
   create_name: '',
   merchant_id: '',
   merchant_id: '',
-  product_img: ''
+  product_img: '' // 预览用:blob URL 或空
 })
 })
 
 
 
 
@@ -2078,6 +2094,11 @@ const onADD = async() => {
     ElMessage.error('请先选择商户')
     ElMessage.error('请先选择商户')
     return
     return
   }
   }
+  if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
+    URL.revokeObjectURL(productForm.product_img)
+  }
+  productForm.product_img = ''
+  productImageFile.value = null
   AdddialogVisible.value = true
   AdddialogVisible.value = true
   const res = await getMerchantId({
   const res = await getMerchantId({
     merchant_code: nodeid.value,
     merchant_code: nodeid.value,
@@ -2088,34 +2109,73 @@ const onADD = async() => {
 }
 }
 
 
 
 
+// 将 File 转为 data:image/xxx;base64,... 字符串
+const fileToDataUrl = (file) => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = () => resolve(reader.result)
+    reader.onerror = reject
+    reader.readAsDataURL(file)
+  })
+}
+
 const handleSubmit = async()=>{
 const handleSubmit = async()=>{
-  productForm.product_img = '/uploads/merchant/690377511/6903775111138/oldimg/鲜美番茄酱.png'
+  if (!productImageFile.value) {
+    ElMessage.warning('请先选择产品图片')
+    return
+  }
   productForm.create_name = userStore.userInfo.nickName
   productForm.create_name = userStore.userInfo.nickName
   try {
   try {
-    const response = await productAdd(productForm)
+    submitLoading.value = true
+    const productImgBase64 = await fileToDataUrl(productImageFile.value)
+    const payload = {
+      product_name: productForm.product_name,
+      product_code: productForm.product_code,
+      merchant_id: productForm.merchant_id,
+      create_name: productForm.create_name,
+      product_img: productImgBase64
+    }
+    const response = await productAdd(payload)
     if (response.code === 0) {
     if (response.code === 0) {
       ElMessage.success('新增成功')
       ElMessage.success('新增成功')
       handleClose()
       handleClose()
     } else {
     } else {
-      ElMessage.error(response.message || '新增失败')
+      ElMessage.error(response.message || response.msg || '新增失败')
     }
     }
   } catch (error) {
   } catch (error) {
     console.error('新增失败:', error)
     console.error('新增失败:', error)
     ElMessage.error('新增失败,请联系管理员')
     ElMessage.error('新增失败,请联系管理员')
-  } 
+  } finally {
+    submitLoading.value = false
+  }
 }
 }
 
 
-// 上传相关配置
-// 环境配置
-const uploadUrl = ref(`${full_url.value}/api/Facility/ImgUpload`)
-
-// 确保上传URL与服务器地址同步
-watch(full_url, (newUrl) => {
-  uploadUrl.value = `${newUrl}/api/Facility/ImgUpload`
+// 新增产品弹窗:预览图地址(blob 或后端返回的 URL)
+const addDialogPreviewSrc = computed(() => {
+  const p = productForm.product_img
+  if (!p) return ''
+  if (p.startsWith('blob:')) return p
+  return formatImageUrl(p)
 })
 })
-const uploadHeaders = { 
-  'Content-Type': 'multipart/form-data',
-  'Authorization': 'Bearer ' + localStorage.getItem('token')
+
+// 新增产品:选择图片仅做本地预览,不调上传接口;点确定时随 productAdd 一起提交
+const handleAddDialogSelectImage = (file) => {
+  const isImage = file.type.startsWith('image/')
+  const isLt5M = file.size / 1024 / 1024 < 5
+  if (!isImage) {
+    ElMessage.error('只能上传图片文件')
+    return false
+  }
+  if (!isLt5M) {
+    ElMessage.error('图片大小不能超过 5MB')
+    return false
+  }
+  if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
+    URL.revokeObjectURL(productForm.product_img)
+  }
+  productImageFile.value = file
+  productForm.product_img = URL.createObjectURL(file)
+  return false // 阻止 el-upload 默认上传
 }
 }
 
 
 // 打开弹窗方法(外部调用)
 // 打开弹窗方法(外部调用)
@@ -2131,82 +2191,27 @@ const openDialog = () => {
   // productForm.create_name = '默认创建人'
   // productForm.create_name = '默认创建人'
 }
 }
 
 
-// 关闭弹窗
+// 关闭新增产品弹窗并重置表单
 const handleClose = () => {
 const handleClose = () => {
-  productFormRef.value?.resetFields()
-  submitLoading.value = false
-}
-
-// 自定义上传逻辑
-const customUpload = async (options) => {
-  const { file, onProgress, onSuccess, onError } = options
-  try {
-    const formData = new FormData()
-    formData.append('image', file)
-    const response = await axios.post(uploadUrl.value, formData, {
-      headers: uploadHeaders,
-      onUploadProgress: (progressEvent) => {
-        const percent = Math.round(
-          (progressEvent.loaded * 100) / progressEvent.total
-        )
-        onProgress({ percent }, file)
-      },
-      timeout: 60000 // 60秒超时
-    })
-
-  } catch (error) {
-    
-  } finally {
-    // 关闭加载中
-    if (file.loadingInstance) {
-      file.loadingInstance.close()
-    }
-  }
-}
-
-// 图片上传成功处理
-const handleUploadSuccess = (response, file) => {
-  // 根据实际接口返回的数据结构调整
-  if (response.code === 0) {
-    productForm.product_img = response.data.url
-    ElMessage.success('上传成功')
-  } else {
-    ElMessage.error(response.message || '上传失败')
-  }
-}
-
-// 上传失败处理
-const handleUploadError = (error, file) => {
-  console.error('上传失败:', error)
-  ElMessage.error('图片上传失败,请重试')
-}
-
-// 上传前验证
-const beforeUpload = (file) => {
-  const isImage = file.type.startsWith('image/')
-  const isLt5M = file.size / 1024 / 1024 < 5
-
-  if (!isImage) {
-    ElMessage.error('只能上传图片文件')
-    return false
-  }
-  
-  if (!isLt5M) {
-    ElMessage.error('图片大小不能超过 5MB')
-    return false
+  if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
+    URL.revokeObjectURL(productForm.product_img)
   }
   }
-  
-  return true
-}
-
-// 查看图片
-const handleViewImage = () => {
-  // el-image 的预览功能会自动处理
+  productForm.product_img = ''
+  productForm.product_name = ''
+  productForm.product_code = ''
+  productImageFile.value = null
+  submitLoading.value = false
+  productFormRef.value?.resetFields()
+  AdddialogVisible.value = false
 }
 }
 
 
-// 删除图片
+// 删除图片(仅清空预览与文件,不调接口)
 const handleRemoveImage = () => {
 const handleRemoveImage = () => {
+  if (productForm.product_img && productForm.product_img.startsWith('blob:')) {
+    URL.revokeObjectURL(productForm.product_img)
+  }
   productForm.product_img = ''
   productForm.product_img = ''
+  productImageFile.value = null
 }
 }
 
 
 // 暴露方法给父组件
 // 暴露方法给父组件
@@ -2232,6 +2237,83 @@ defineExpose({
     margin-top: 10px;
     margin-top: 10px;
     text-align: right;
     text-align: right;
   }
   }
+
+  /* 新增产品弹窗 - 上传与预览 */
+  .add-dialog-upload-wrap {
+    width: 100%;
+  }
+  .add-dialog-preview-box {
+    border: 1px solid #e4e7ed;
+    border-radius: 8px;
+    overflow: hidden;
+    background: #fafafa;
+  }
+  .add-dialog-preview-img {
+    width: 100%;
+    height: 200px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #f5f7fa;
+  }
+  .add-dialog-preview-img-inner {
+    max-width: 100%;
+    max-height: 200px;
+    width: auto;
+    height: auto;
+    object-fit: contain;
+    display: block;
+  }
+  .add-dialog-preview-actions {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+    padding: 12px;
+    border-top: 1px solid #e4e7ed;
+    background: #fff;
+  }
+  .add-dialog-upload-area {
+    width: 100%;
+  }
+  .add-dialog-upload-area :deep(.el-upload) {
+    width: 100%;
+    display: block;
+  }
+  .add-dialog-upload-inner {
+    width: 100%;
+    height: 160px;
+    border: 2px dashed #dcdfe6;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    background: #fafafa;
+    color: #606266;
+    transition: border-color 0.2s, background 0.2s;
+  }
+  .add-dialog-upload-inner:hover {
+    border-color: #409eff;
+    background: #ecf5ff;
+    color: #409eff;
+  }
+  .add-dialog-upload-icon {
+    font-size: 36px;
+    color: #c0c4cc;
+  }
+  .add-dialog-upload-inner:hover .add-dialog-upload-icon {
+    color: #409eff;
+  }
+  .add-dialog-upload-text {
+    font-size: 14px;
+    font-weight: 500;
+  }
+  .add-dialog-upload-tip {
+    font-size: 12px;
+    color: #909399;
+  }
   :deep(.el-table__body tr.current-row) > td {
   :deep(.el-table__body tr.current-row) > td {
     background: #ff80ff !important;
     background: #ff80ff !important;
   }
   }
@@ -2248,26 +2330,104 @@ defineExpose({
     border-bottom: 1px solid #e4e7ed;
     border-bottom: 1px solid #e4e7ed;
   }
   }
   
   
-  /* 新布局CSS */
+  /* 编辑弹窗三列布局:自适应,小屏横向滚动防跑偏 */
   .image-edit-container {
   .image-edit-container {
     display: flex;
     display: flex;
-    gap: 20px;
-    height: calc(100vh - 140px);
+    height: 94vh;
+    padding-top: 20px;
+    gap: 0;
+    width: 100%;
+    overflow-x: auto;
+    overflow-y: hidden;
+    box-sizing: border-box;
   }
   }
-  
+
   .left-column {
   .left-column {
-    flex: 2;
+    flex: 0.7;
+    min-width: 280px;
+    max-width: 560px;
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    gap: 20px;
+    gap: 15px;
+    padding: 15px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    flex-shrink: 1;
+  }
+
+  .middle-column {
+    width: 430px;
+    min-width: 380px;
+    flex-shrink: 0;
+    border-left: 1px solid #e4e7ed;
+    border-right: 1px solid #e4e7ed;
+    padding: 15px 12px;
+    position: relative;
+    box-sizing: border-box;
+    background: #fff;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
+  .right-column {
+    flex: 1;
+    min-width: 280px;
+    min-height: 0;
+    flex-shrink: 1;
+    padding: 0 12px 0 15px;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
   }
   }
   
   
   .image-comparison-section {
   .image-comparison-section {
-    flex: none; /* 改为不伸缩 */
-    height: 300px; /* 固定高度,和原来一样 */
+    flex: none;
+    display: flex;
+    flex-direction: row;
+    align-items: flex-start;
+    gap: 16px;
+    min-height: 200px;
+  }
+
+  .upload-image-box {
+    width: 200px;
+    height: 200px;
+    max-width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: #f9f9f9;
+    border-radius: 4px;
+    overflow: hidden;
+    border: 1px solid #e4e7ed;
+  }
+
+  .image-placeholder-small {
+    width: 100px;
+    height: 100px;
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    gap: 15px;
+    justify-content: center;
+    align-items: center;
+    background-color: #f9f9f9;
+    border-radius: 4px;
+    border: 1px solid #e4e7ed;
+  }
+
+  .case-links-column {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    justify-content: space-between;
+    flex-shrink: 0;
+    height: 200px;
+  }
+
+  .case-link-btn {
+    padding: 0 !important;
+    font-size: 14px !important;
+    font-weight: normal !important;
+    color: #409eff !important;
   }
   }
   
   
   .image-item {
   .image-item {
@@ -2393,14 +2553,15 @@ defineExpose({
   .template-item {
   .template-item {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    width: calc(33.33% - 7px); /* 一行显示3个,减去gap */
+    width: calc(33.33% - 7px);
+    min-width: 100px; /* 笔记本小屏时不被压得过窄 */
     padding: 10px;
     padding: 10px;
     border: 2px solid #e4e7ed;
     border: 2px solid #e4e7ed;
     border-radius: 6px;
     border-radius: 6px;
     cursor: pointer;
     cursor: pointer;
     transition: all 0.3s;
     transition: all 0.3s;
     background: white;
     background: white;
-    flex-shrink: 0; /* 防止被压缩 */
+    flex-shrink: 0;
     box-sizing: border-box;
     box-sizing: border-box;
   }
   }
   
   
@@ -2428,7 +2589,7 @@ defineExpose({
   .template-thumbnail img {
   .template-thumbnail img {
     width: 100%;
     width: 100%;
     height: 100%;
     height: 100%;
-    object-fit: cover;
+    object-fit: contain;
   }
   }
   
   
   .image-container {
   .image-container {