hurx 13 horas atrás
pai
commit
1168c6d803
1 arquivos alterados com 508 adições e 247 exclusões
  1. 508 247
      src/views/enterprisePurchase/index.vue

+ 508 - 247
src/views/enterprisePurchase/index.vue

@@ -108,7 +108,7 @@
       <!-- 广告图编辑区 -->
       <div v-else-if="activeSubTab === 'carousel'" class="carousel-editor">
         <!-- 模块一:左侧广告设置 -->
-        <div class="editor-section">
+        <!-- <div class="editor-section">
           <div class="section-header">
             <span class="section-title">模块一:左侧广告设置</span>
             <span class="section-desc">尺寸要求:790 * 460,支持上传本地图片并设置跳转链接</span>
@@ -142,12 +142,12 @@
               </div>
             </div>
           </div>
-        </div>
+        </div> -->
 
         <!-- 模块二:轮播图管理 -->
         <div class="editor-section">
           <div class="section-header">
-            <span class="section-title">模块二:轮播图设置</span>
+            <span class="section-title">轮播图设置</span>
             <span class="section-desc">尺寸要求:552 * 190,支持拖拽排序与实时状态切换</span>
           </div>
 
@@ -330,52 +330,73 @@
         <div class="editor-section">
           <div class="section-header">
             <span class="section-title">分类实时预览</span>
-            <span class="section-desc">尺寸要求:280 * 398,悬停可查看右滑面板效果 (图1、图2)</span>
+            <span class="section-desc">尺寸要求:248 * 384,悬停可查看右滑面板效果 (图1、图2)</span>
           </div>
 
           <div class="category-preview-container">
             <!-- 图1: 分类菜单 -->
-            <div class="category-menu-mockup">
-              <div v-for="item in categoryList.filter((c) => c.status === 1).slice(0, 10)" :key="item.id" class="menu-item">
-                <div class="menu-icon">
-                  <img v-if="item.icon" :src="item.icon" alt="" />
-                  <el-icon v-else><Menu /></el-icon>
+            <div
+              class="category-menu-mockup"
+              :class="{ 'is-expanded': isPreviewExpanded }"
+              :style="menuMockupStyle"
+              @mouseenter="isPreviewExpanded = true"
+              @mouseleave="
+                isPreviewExpanded = false;
+                activeHoverId = null;
+              "
+            >
+              <div
+                v-for="item in categoryList.filter((c) => c.status === 1)"
+                :key="item.id"
+                class="menu-item"
+                :class="{ 'is-hovered': activeHoverId === item.id }"
+                @mouseenter="activeHoverId = item.id"
+              >
+                <div class="menu-item-inner">
+                  <div class="menu-icon">
+                    <img v-if="item.icon" :src="item.icon" alt="" />
+                    <el-icon v-else><Menu /></el-icon>
+                  </div>
+                  <div class="menu-name">
+                    <template v-for="(subName, subIdx) in getSubNames(item)" :key="subIdx">
+                      <span class="sub-menu-link" @click.stop="handleSubMenuClick(subName, item)">{{ subName }}</span>
+                      <span v-if="subIdx < getSubNames(item).length - 1" class="menu-sep">/</span>
+                    </template>
+                  </div>
                 </div>
-                <div class="menu-name">{{ item.name }}</div>
-
-                <!-- 图2: 悬停右滑面板 - CSS 控制显示 -->
-                <div class="category-panel-mockup">
-                  <div class="panel-header-line"></div>
-                  <div class="panel-content">
-                    <!-- 顶部标签栏 -->
-                    <div class="panel-tabs">
-                      <span v-for="tag in item.tags" :key="tag.name" class="panel-tab-item">{{ tag.name }}</span>
-                    </div>
 
-                    <!-- 品牌位 (图3) - 绝对定位至右上角 -->
-                    <div class="brand-box">
-                      <div class="brand-main-title">
-                        {{ item.panelData.mainTitle }}<span class="brand-strong">{{ item.panelData.subTitle }}</span>
-                      </div>
-                      <div class="brand-notes">
-                        <template v-for="(note, nIdx) in item.panelData.notes" :key="nIdx">
-                          <span class="note-item">{{ note.name }}</span>
-                          <span v-if="nIdx < item.panelData.notes.length - 1" class="note-sep">|</span>
-                        </template>
+                <!-- 悬停右滑面板 -->
+                <div
+                  class="category-panel-mockup"
+                  :class="{ 'is-visible': activeHoverId === item.id }"
+                  @mouseenter="
+                    isPreviewExpanded = true;
+                    activeHoverId = item.id;
+                  "
+                >
+                  <div class="panel-content-flex">
+                    <!-- 左侧:主要分类和顶部的标签 -->
+                    <div class="panel-main-side">
+                      <!-- 顶部标签栏 -->
+                      <div class="panel-tabs">
+                        <span v-for="tag in item.tags" :key="tag.name" class="panel-tab-item">{{ tag.name }}</span>
                       </div>
-                    </div>
-
-                    <div class="panel-body">
-                      <!-- 左侧分类列表 -->
-                      <div class="panel-main">
+                      <!-- 二级分类组 -->
+                      <div class="panel-groups-container">
                         <div v-for="group in item.panelData.groups" :key="group.title" class="category-group">
-                          <div class="group-title">{{ group.title }}</div>
+                          <div class="group-title" @click.stop="handleGroupTitleClick(group.title, item)">{{ group.title }}</div>
                           <div class="group-items">
                             <span v-for="sub in group.items" :key="sub" class="group-item">{{ sub }}</span>
                           </div>
                         </div>
                       </div>
                     </div>
+                    <!-- 右侧:5个广告位 -->
+                    <div class="panel-ads-side">
+                      <div v-for="(adUrl, adIdx) in getCategoryAds(item)" :key="adIdx" class="panel-ad-card">
+                        <img v-if="adUrl" :src="adUrl" class="panel-ad-img" />
+                      </div>
+                    </div>
                   </div>
                 </div>
               </div>
@@ -550,7 +571,7 @@
               </div>
               <div class="ad-products-subsidy">
                 <div v-for="item in adModules[0].items" :key="item.id" class="product-item">
-                  <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
+                  <div class="product-img"><img :src="item.image" alt="" /></div>
                   <div class="product-price">¥{{ item.price }}</div>
                 </div>
               </div>
@@ -566,10 +587,10 @@
                 <div class="ad-title-sub" :style="getAdTitleStyle(1, 'sub')">{{ getAdTitleText(1, 'sub') }}</div>
               </div>
               <div class="ad-products-ranking">
-                <div v-for="item in adModules[1].items" :key="item.id" class="ranking-item">
-                  <div class="ranking-badge">{{ item.tagText || '排行榜' }} ></div>
-                  <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
-                  <div class="ranking-footer">已售{{ item.salesCount }}件</div>
+                <div v-for="(item, idx) in adModules[1].items" :key="item.id" class="ranking-item" @click="handleAdItemClick(item)">
+                  <div class="product-img"><img :src="item.image" alt="" /></div>
+                  <div class="ranking-badge">{{ item.tag || '排行榜' }}</div>
+                  <div class="ranking-sales">{{ item.sales }}</div>
                 </div>
               </div>
               <div class="ad-hover-mask">
@@ -584,10 +605,10 @@
                 <div class="ad-title-sub" :style="getAdTitleStyle(2, 'sub')">{{ getAdTitleText(2, 'sub') }}</div>
               </div>
               <div class="ad-brands-content">
-                <div v-for="item in adModules[2].items" :key="item.id" class="brand-item">
-                  <div class="brand-logo"><img :src="item.imageUrl" alt="" /></div>
-                  <div class="brand-name">{{ item.tagLink || item.productName }}</div>
-                  <div class="brand-tag-btn">{{ item.tagText || '品质保障' }}</div>
+                <div v-for="item in adModules[2].items" :key="item.id" class="brand-item" @click="handleAdItemClick(item)">
+                  <div class="brand-logo"><img :src="item.image" alt="" /></div>
+                  <div class="brand-name-badge">{{ item.tag }}</div>
+                  <div class="brand-tag-text">{{ item.sales }}</div>
                 </div>
               </div>
               <div class="ad-hover-mask">
@@ -603,7 +624,7 @@
               </div>
               <div class="ad-products-selection">
                 <div v-for="item in adModules[3].items" :key="item.id" class="selection-item">
-                  <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
+                  <div class="product-img"><img :src="item.image" alt="" /></div>
                   <div class="product-price-row">
                     <span class="p-unit">¥</span>
                     <span class="p-val">{{ item.price }}</span>
@@ -623,7 +644,7 @@
               </div>
               <div class="ad-products-selection">
                 <div v-for="item in adModules[4].items" :key="item.id" class="selection-item">
-                  <div class="product-img"><img :src="item.imageUrl" alt="" /></div>
+                  <div class="product-img"><img :src="item.image" alt="" /></div>
                   <div class="product-price-row center">
                     <span class="p-unit">¥</span>
                     <span class="p-val">{{ item.price }}</span>
@@ -910,64 +931,73 @@
           <el-table-column label="位置" width="60" align="center">
             <template #default="scope">{{ scope.$index + 1 }}</template>
           </el-table-column>
-          <el-table-column label="图片" width="100" align="center">
-            <template #default="{ row }">
-              <img :src="row.imageUrl" style="width: 50px; height: 50px; object-fit: contain" />
+          <el-table-column label="图片" width="120" align="center">
+            <template #default="scope">
+              <!-- 企采榜单 / 品牌好店:系统默认上传组件 -->
+              <template v-if="currentAdIdx === 1 || currentAdIdx === 2">
+                <el-image
+                  v-if="scope.row.image"
+                  :src="scope.row.image"
+                  :preview-src-list="[scope.row.image]"
+                  preview-teleported
+                  fit="contain"
+                  style="width: 86px; height: 86px; border-radius: 4px"
+                />
+                <el-upload
+                  :action="uploadUrl"
+                  :headers="uploadHeaders"
+                  :show-file-list="false"
+                  :before-upload="(file: any) => beforeCellUpload(file)"
+                  :on-success="(res: any) => onCellUploadSuccess(res, scope.row)"
+                  accept="image/*"
+                >
+                  <el-button size="small" type="primary" link>
+                    {{ scope.row.image ? '更换' : '上传' }}
+                  </el-button>
+                </el-upload>
+              </template>
+              <el-image
+                v-else-if="scope.row.image"
+                :src="scope.row.image"
+                :preview-src-list="[scope.row.image]"
+                fit="contain"
+                style="width: 48px; height: 48px; border-radius: 4px; cursor: pointer"
+                preview-teleported
+              />
+              <span v-else class="text-gray">-</span>
             </template>
           </el-table-column>
           <!-- 通用商品信息列 (百亿补贴、企采精选、京东新品) -->
           <el-table-column v-if="currentAdIdx === 0 || currentAdIdx === 3 || currentAdIdx === 4" label="商品信息" min-width="180">
             <template #default="{ row }">
               <div class="table-info-cell">
-                <div class="info-name">{{ row.productName || '未选择' }}</div>
+                <div class="info-name">{{ row.name || '未选择' }}</div>
                 <div class="info-price">价格:¥{{ row.price }}</div>
                 <div class="info-id">ID: {{ row.id || '-' }}</div>
               </div>
             </template>
           </el-table-column>
 
-          <!-- 企采榜单 专属列 -->
-          <template v-if="currentAdIdx === 1">
-            <el-table-column label="商品信息" min-width="150">
+          <!-- 企采榜单 & 品牌好店 统一配置列 -->
+          <template v-if="currentAdIdx === 1 || currentAdIdx === 2">
+            <el-table-column label="标签" min-width="150">
               <template #default="{ row }">
-                <div class="info-name">{{ row.productName || '未选择' }}</div>
-                <div class="info-id">ID: {{ row.id || '-' }}</div>
+                <el-input v-model="row.tag" placeholder="请输入标签" />
               </template>
             </el-table-column>
-            <el-table-column label="排行标签" width="220">
-              <template #default="{ row }">
-                <el-input v-model="row.tagText" placeholder="标签文字" size="small" class="m-b-5" />
-                <WebLinkInput v-model="row.tagLink" placeholder="跳转链接" size="small" />
-              </template>
-            </el-table-column>
-            <el-table-column label="销量数据" width="130">
-              <template #default="{ row }">
-                <el-input v-model="row.salesCount" placeholder="销量" size="small">
-                  <template #prepend>已售</template>
-                </el-input>
-              </template>
-            </el-table-column>
-          </template>
-
-          <!-- 品牌好店 专属列 -->
-          <template v-if="currentAdIdx === 2">
-            <el-table-column label="品牌名称" min-width="180">
+            <el-table-column label="标题" min-width="150">
               <template #default="{ row }">
-                <div class="brand-name-display">{{ row.productName || '未选择' }}</div>
-                <div class="info-id m-t-5">ID: {{ row.id || '-' }}</div>
+                <el-input v-model="row.sales" placeholder="请输入标题" />
               </template>
             </el-table-column>
-            <el-table-column label="品牌标签" width="300">
+            <el-table-column label="跳转地址" min-width="200">
               <template #default="{ row }">
-                <div class="flex-column gap-10">
-                  <el-input v-model="row.tagText" placeholder="标签文字 (如: 品质保障)" />
-                  <el-input v-model="row.tagLink" placeholder="描述文字 (控制预览品牌名称)" />
-                </div>
+                <WebLinkInput v-model="row.link" placeholder="请输入跳转地址" />
               </template>
             </el-table-column>
           </template>
 
-          <el-table-column label="操作" width="110" align="center" fixed="right">
+          <el-table-column v-if="currentAdIdx !== 1 && currentAdIdx !== 2" label="操作" width="110" align="center" fixed="right">
             <template #default="scope">
               <el-button type="primary" link @click="openProductSelect(scope.$index)">
                 <el-icon class="m-r-5"><Edit /></el-icon>
@@ -983,6 +1013,12 @@
       </template>
     </el-dialog>
 
+    <!-- 隐藏文件上传input -->
+    <input ref="fileInput" type="file" style="display: none" accept="image/*" @change="onFileChange" />
+
+    <!-- 全局图片大图查看器 -->
+    <el-image-viewer v-if="showImageViewer" :url-list="[previewImageUrl]" @close="showImageViewer = false" />
+
     <!-- 选择商品/品牌抽屉 (加宽版) -->
     <el-drawer v-model="selectDialogVisible" :title="currentAdIdx === 2 ? '选择品牌' : '选择商品'" size="850px" append-to-body>
       <div class="select-dialog-content">
@@ -1178,28 +1214,27 @@
           </el-tab-pane>
           <el-tab-pane label="右滑面板配置">
             <div class="panel-config-section">
-              <div class="config-subtitle">品牌位设置 (图3)</div>
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="主标题:">
-                    <el-input v-model="categoryForm.panelData.mainTitle" placeholder="如:京东" />
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="副标题:">
-                    <el-input v-model="categoryForm.panelData.subTitle" placeholder="如:3C数码" />
-                  </el-form-item>
-                </el-col>
-              </el-row>
-
-              <el-form-item label="便签列表:">
+              <div class="config-subtitle" style="display: flex; align-items: center; gap: 12px">
+                <span>广告位设置 (图3)</span>
+                <span style="font-size: 12px; color: #909399; font-weight: normal">(建议尺寸:116 * 54px,最多支持 5 张广告图)</span>
+              </div>
+
+              <el-form-item label="广告位列表:">
                 <div class="notes-config-list">
                   <div v-for="(note, index) in categoryForm.panelData.notes" :key="index" class="note-config-row">
-                    <el-input v-model="note.name" placeholder="便签名称" style="width: 120px" />
-                    <WebLinkInput v-model="note.link" placeholder="跳转地址" style="flex: 1" />
+                    <UploadImage v-model="note.name" :limit="1" width="116px" height="54px" />
+                    <WebLinkInput v-model="note.link" placeholder="请输入广告跳转地址" style="flex: 1" />
                     <el-button type="danger" icon="Delete" circle plain size="small" @click="removePanelNote(index)" />
                   </div>
-                  <el-button type="primary" icon="Plus" link @click="addPanelNote">添加便签</el-button>
+                  <el-button
+                    v-if="!categoryForm.panelData.notes || categoryForm.panelData.notes.length < 5"
+                    type="primary"
+                    icon="Plus"
+                    link
+                    @click="addPanelNote"
+                  >
+                    添加广告位
+                  </el-button>
                 </div>
               </el-form-item>
             </div>
@@ -1564,6 +1599,7 @@ import WebLinkInput from '@/components/WebLinkInput/index.vue';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { globalHeaders } from '@/utils/request';
 import {
   listSearchConfig,
   getSearchConfig,
@@ -1651,7 +1687,8 @@ import {
   Search,
   InfoFilled,
   ArrowUp,
-  ArrowDown
+  ArrowDown,
+  ZoomIn
 } from '@element-plus/icons-vue';
 import { any } from 'vue-types';
 const uploadAction = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
@@ -2151,6 +2188,53 @@ const handleDeleteLeftAd = () => {
 
 // 分类设置模块逻辑
 const categoryThemeColor = ref('#e60012');
+const categoryThemeColorAlpha = computed(() => {
+  const hex = categoryThemeColor.value.replace('#', '');
+  const r = parseInt(hex.substring(0, 2), 16);
+  const g = parseInt(hex.substring(2, 4), 16);
+  const b = parseInt(hex.substring(4, 6), 16);
+  return `rgba(${r}, ${g}, ${b}, 0.3)`;
+});
+const isPreviewExpanded = ref(false);
+const activeHoverId = ref<number | null>(null);
+
+// 获取二级分类名称列表
+const getSubNames = (item: any) => {
+  if (item.panelData && item.panelData.groups && item.panelData.groups.length) {
+    return item.panelData.groups.slice(0, 3).map((g: any) => g.title);
+  }
+  return item.name ? item.name.split('/').map((s: string) => s.trim()) : [];
+};
+
+const handleSubMenuClick = (subName: string, item: any) => {
+  ElMessage.success(`您点击了子分类:${subName} (属于 ${item.name} 一级分类)`);
+};
+
+const handleGroupTitleClick = (title: string, item: any) => {
+  ElMessage.success(`您点击了二级分类:${title} (属于 ${item.name} 一级分类)`);
+};
+
+const getCategoryAds = (item: any) => {
+  const result: string[] = [];
+  if (item.panelData && item.panelData.notes && item.panelData.notes.length) {
+    const list = item.panelData.notes
+      .map((n: any) => {
+        const name = n.name || '';
+        const isImg = name.startsWith('data:') || name.startsWith('http') || name.startsWith('/') || name.includes('.');
+        return isImg ? name : '';
+      })
+      .slice(0, 5);
+    result.push(...list);
+  }
+  while (result.length < 5) {
+    result.push('');
+  }
+  return result;
+};
+
+const menuMockupStyle = computed(() => ({
+  '--panel-width': '640px'
+}));
 const syncCategoryOptions = computed(() => {
   return (categoryOptions.value || []).map((item: any) => ({
     value: item.id,
@@ -2860,10 +2944,10 @@ const adModules = ref([
     subTitleColor: '#999999',
     type: 'subsidy',
     items: [
-      { id: 101, productName: '企业商用台式机', imageUrl: '/static/images/purchase/pc_desktop.jpg', price: '69.9' },
-      { id: 102, productName: '商务笔记本', imageUrl: '/static/images/purchase/laptop_hp.jpg', price: '84.8' },
-      { id: 103, productName: '智能打印机', imageUrl: '/static/images/purchase/printer_office.jpg', price: '139.9' },
-      { id: 104, productName: '高效办公组网', imageUrl: '/static/images/purchase/network_router.jpg', price: '1749' }
+      { id: 101, name: '企业商用台式机', image: '/static/images/purchase/pc_desktop.jpg', price: '69.9', link: '' },
+      { id: 102, name: '商务笔记本', image: '/static/images/purchase/laptop_hp.jpg', price: '84.8', link: '' },
+      { id: 103, name: '智能打印机', image: '/static/images/purchase/printer_office.jpg', price: '139.9', link: '' },
+      { id: 104, name: '高效办公组网', image: '/static/images/purchase/network_router.jpg', price: '1749', link: '' }
     ]
   },
   {
@@ -2874,24 +2958,8 @@ const adModules = ref([
     subTitleColor: '#f58220',
     type: 'ranking',
     items: [
-      {
-        id: 201,
-        productName: '办公电脑榜',
-        imageUrl: '/static/images/purchase/laptop_lenovo.jpg',
-        price: '0',
-        tagText: '办公电脑榜',
-        tagLink: '',
-        salesCount: '1543'
-      },
-      {
-        id: 202,
-        productName: '文具榜',
-        imageUrl: '/static/images/purchase/stationery_ranking.jpg',
-        price: '0',
-        tagText: '文具榜',
-        tagLink: '',
-        salesCount: '1200'
-      }
+      { id: 201, image: '/static/images/purchase/laptop_lenovo.jpg', tag: '办公电脑榜', sales: '已售1543件' },
+      { id: 202, image: '/static/images/purchase/stationery_ranking.jpg', tag: '文具榜', sales: '已售1200件' }
     ]
   },
   {
@@ -2902,8 +2970,8 @@ const adModules = ref([
     subTitleColor: '#f58220',
     type: 'brand',
     items: [
-      { id: 301, productName: '鲁花', imageUrl: '/static/images/purchase/oil_luhua.jpg', tagText: '品质保障', tagLink: '鲁花京东自营旗舰店' },
-      { id: 302, productName: '金龙鱼', imageUrl: '/static/images/purchase/oil_jinlongyu.jpg', tagText: '热销品牌', tagLink: '金龙鱼京东自营旗舰店' }
+      { id: 301, image: '/static/images/purchase/oil_luhua.jpg', tag: '鲁花', sales: '品质保障' },
+      { id: 302, image: '/static/images/purchase/oil_jinlongyu.jpg', tag: '金龙鱼', sales: '热销品牌' }
     ]
   },
   {
@@ -2914,8 +2982,8 @@ const adModules = ref([
     subTitleColor: '#999999',
     type: 'selection',
     items: [
-      { id: 401, productName: '高性能工作站', imageUrl: '/static/images/purchase/pc_desktop.jpg', price: '10740' },
-      { id: 402, productName: '办公咖啡机', imageUrl: '/static/images/purchase/coffee_machine.jpg', price: '877' }
+      { id: 401, name: '高性能工作站', image: '/static/images/purchase/pc_desktop.jpg', price: '10740', link: '' },
+      { id: 402, name: '办公咖啡机', image: '/static/images/purchase/coffee_machine.jpg', price: '877', link: '' }
     ]
   },
   {
@@ -2926,8 +2994,8 @@ const adModules = ref([
     subTitleColor: '#f58220',
     type: 'new',
     items: [
-      { id: 501, productName: '商用冷柜', imageUrl: '/static/images/purchase/freezer.jpg', price: '7188' },
-      { id: 502, productName: '得力笔记本', imageUrl: '/static/images/purchase/notebook_deli.jpg', price: '34.9' }
+      { id: 501, name: '商用冷柜', image: '/static/images/purchase/freezer.jpg', price: '7188', link: '' },
+      { id: 502, name: '得力笔记本', image: '/static/images/purchase/notebook_deli.jpg', price: '34.9', link: '' }
     ]
   }
 ]) as any;
@@ -2936,6 +3004,38 @@ const adDialogVisible = ref(false);
 const currentAdIdx = ref(-1);
 const currentItemIdx = ref(-1);
 const adForm = reactive({ title: '', titleColor: '#333333', subTitle: '', subTitleColor: '#f58220', items: [] });
+const fileInput = ref(null);
+const showImageViewer = ref(false);
+const previewImageUrl = ref('');
+const adUploadIndex = ref(-1);
+
+// 触发广告行图片上传
+const triggerAdRowUpload = (index: number) => {
+  adUploadIndex.value = index;
+  fileInput.value?.click();
+};
+
+// 预览行图片大图
+const handlePreviewRowImage = (url: string) => {
+  previewImageUrl.value = url;
+  showImageViewer.value = true;
+};
+
+// 文件选择变化处理
+const onFileChange = (e: Event) => {
+  const file = (e.target as HTMLInputElement).files?.[0];
+  if (!file) return;
+  const reader = new FileReader();
+  reader.onload = (event: ProgressEvent<FileReader>) => {
+    const base64 = event.target?.result as string;
+    if (adUploadIndex.value >= 0 && adForm.items[adUploadIndex.value]) {
+      adForm.items[adUploadIndex.value].image = base64;
+    }
+    ElMessage.success('图片上传成功');
+    (e.target as HTMLInputElement).value = '';
+  };
+  reader.readAsDataURL(file);
+};
 
 // 实时预览辅助函数
 const getAdTitleStyle = (index: number, type: string) => {
@@ -2952,7 +3052,33 @@ const getAdTitleText = (index: number, type: string) => {
   return isEditing ? adForm[key] : adModules.value[index]?.[key] || '';
 };
 
-const emptyItem = () => ({ id: null, productId: '', productName: '', imageUrl: '', price: '', tagText: '', tagLink: '', salesCount: '' });
+const emptyItem = () => ({ id: null, name: '', image: '', price: '', tag: '', sales: '', link: '' });
+
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
+const uploadHeaders = ref(globalHeaders());
+
+const beforeCellUpload = (file: any) => {
+  const isImg = file.type.indexOf('image') > -1;
+  if (!isImg) {
+    ElMessage.error('只能上传图片文件');
+    return false;
+  }
+  const isLt5M = file.size / 1024 / 1024 < 5;
+  if (!isLt5M) {
+    ElMessage.error('图片大小不能超过 5MB');
+    return false;
+  }
+  return true;
+};
+
+const onCellUploadSuccess = (res: any, row: any) => {
+  if (res.code === 200) {
+    row.image = res.data.url;
+    ElMessage.success('上传成功');
+  } else {
+    ElMessage.error(res.msg || '上传失败');
+  }
+};
 const slotCounts = [4, 2, 2, 2, 2]; // 每个模块固定条目数
 
 const handleEditAd = (index: number) => {
@@ -2968,6 +3094,12 @@ const handleEditAd = (index: number) => {
   adDialogVisible.value = true;
 };
 
+const handleAdItemClick = (item: any) => {
+  if (item && item.link) {
+    window.open(item.link, '_blank');
+  }
+};
+
 const submitAdForm = async () => {
   const moduleData = adModules.value[currentAdIdx.value] as any;
   if (!moduleData) return;
@@ -2983,15 +3115,15 @@ const submitAdForm = async () => {
     status: 1,
     sortOrder: currentAdIdx.value,
     adModuleItemList: adForm.items
-      .filter((item: any) => item.productId || item.imageUrl)
+      .filter((item: any) => item.name || item.image)
       .map((item: any) => ({
         productId: item.productId || item.id,
-        productName: item.productName || '',
-        imageUrl: item.imageUrl || '',
+        productName: item.name || '',
+        imageUrl: item.image || '',
         price: item.price || 0,
-        tagText: item.tagText || '',
-        tagLink: item.tagLink || '',
-        salesCount: item.salesCount || 0
+        tagText: item.tag || '',
+        tagLink: item.tagLink || item.link || '',
+        salesCount: item.sales || ''
       }))
   };
 
@@ -3037,13 +3169,12 @@ const getAdModuleList = async () => {
         type: item.moduleCode || '',
         items: (item.adModuleItemList || []).map((sub: any) => ({
           id: sub.id,
-          productId: sub.productId || '',
-          productName: sub.productName || '',
-          imageUrl: sub.imageUrl || '',
+          name: sub.productName || sub.name || '',
+          image: sub.imageUrl || sub.image || '',
+          tag: sub.tagText || sub.tag || '',
+          sales: sub.sales || sub.salesCount || '',
           price: sub.price || 0,
-          tagText: sub.tagText || '',
-          tagLink: sub.tagLink || '',
-          salesCount: sub.salesCount || 0
+          link: sub.linkUrl || sub.link || ''
         }))
       }));
       // 确保始终有 5 个模块(模板固定访问 adModules[0]~[4])
@@ -3531,9 +3662,8 @@ const confirmSelect = () => {
   if (item) {
     const target = adForm.items[currentItemIdx.value];
     target.id = item.id;
-    target.productId = item.id;
-    target.productName = item.name;
-    target.imageUrl = item.image;
+    target.name = item.name;
+    target.image = item.image;
     target.price = item.price;
     selectDialogVisible.value = false;
     ElMessage.success('选择成功');
@@ -4461,51 +4591,62 @@ watch(activeSubTab, (newVal) => {
   padding: 20px 0;
   display: flex;
   justify-content: flex-start;
+  position: relative;
+  height: 424px; /* 384px最低高度 + padding上下40px = 424px,固定占位高度 */
+  box-sizing: border-box;
 }
 
 /* 图1: 仿真菜单 */
 .category-menu-mockup {
-  width: 280px;
-  height: 398px;
+  width: 248px;
+  height: 384px; /* 默认最低高度为 384px */
   background: #f8f9fa;
   border-radius: 4px;
-  padding: 10px 0;
+  padding: 12px 12px;
   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
-  position: relative;
-  z-index: 10;
-  overflow: visible; /* 必须可见,否则遮罩层无法伸出 */
+  position: absolute;
+  left: 0;
+  top: 20px;
+  z-index: 100; /* 悬浮在下方表格之上 */
+  overflow: hidden; /* 默认截断多余的分类 */
+  box-sizing: border-box;
+  transition:
+    height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+    box-shadow 0.3s ease;
+}
+
+/* 只有悬停在左侧背景区域才会将延伸将全部左侧菜单显示出来 */
+.category-menu-mockup.is-expanded {
+  overflow: visible; /* 必须展开可见,以便右侧面板可以向右伸出 */
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
 }
 
 /* 仿真菜单项 */
 .menu-item {
-  height: 44px;
+  height: 36px;
+  width: 248px; /* 物理宽度拉满仿真菜单的248px宽度,保证悬浮移动时无缝贴合 */
+  margin: 0 -12px; /* 抵消外层菜单容器左右各12px的padding,使热区无缝延伸到右侧面板 */
+  padding: 0 12px; /* 内部补充12px的padding以维持文字原本对齐位置 */
   display: flex;
   align-items: center;
-  padding: 0 15px;
-  cursor: pointer;
-  position: relative;
-  transition: all 0.1s;
-  background: transparent;
   box-sizing: border-box;
+  position: static; /* 必须设为static,使子级右滑面板能相对于category-menu-mockup定位实现等高 */
 }
 
-.menu-item:hover {
-  background-color: #fff !important;
-  color: v-bind(categoryThemeColor);
-  border: 1px solid v-bind(categoryThemeColor);
-  border-right: none;
-  border-radius: 12px 0 0 12px;
-  z-index: 1000;
-  margin-left: 10px;
-  padding-left: 15px;
-  width: calc(100% - 10px);
+.menu-item-inner {
+  width: 224px; /* 248px - 12px*2 = 224px */
+  height: 36px;
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+  cursor: pointer;
 }
 
 .menu-icon {
-  width: 16px;
-  height: 16px;
-  margin-right: 15px; /* 增加间距 */
-  color: #999;
+  width: 14px;
+  height: 14px;
+  margin-right: 8px;
+  color: #666;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -4525,74 +4666,83 @@ watch(activeSubTab, (newVal) => {
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  transition: color 0.15s ease;
 }
 
-/* 遮盖层:仅抹除中间垂直线,保留上下边框连贯 */
-.menu-item:hover::after {
-  content: '';
-  position: absolute;
-  top: 0; /* 保持在边框内侧 */
-  bottom: 0; /* 保持在边框内侧 */
-  right: -1px; /* 贴合右边缘 */
-  width: 2px; /* 覆盖面板左边框 */
-  background: #fff;
-  z-index: 1001;
+/* 单独悬停到哪个分类,对应分类文字变色并支持点击跳转 */
+.sub-menu-link {
+  transition: all 0.15s ease;
+  color: #333;
 }
 
-.menu-item:hover .menu-icon {
-  color: v-bind(categoryThemeColor);
+.sub-menu-link:hover {
+  color: v-bind(categoryThemeColor) !important;
+  font-weight: bold;
+  text-decoration: underline;
 }
 
-.menu-name {
-  font-size: 14px;
-  color: #333;
+.menu-sep {
+  margin: 0 4px;
+  color: #bbb;
+  font-weight: normal;
+  pointer-events: none; /* 分隔符不要响应鼠标事件 */
 }
 
-.menu-item:hover .menu-name {
+.menu-item.is-hovered .menu-icon {
   color: v-bind(categoryThemeColor);
-  font-weight: bold;
 }
 
-/* 图2: 右面板 */
+/* 图2: 右侧悬停面板 */
 .category-panel-mockup {
   position: absolute;
-  left: 100%; /* 紧贴菜单项右侧 */
-  margin-left: 0;
-  top: -1px; /* 顶部边框对齐 */
-  width: 980px;
-  min-height: 480px;
+  left: 248px; /* 紧贴左侧菜单的总宽度 */
+  top: 0;
+  width: 985px; /* 右侧悬停面板宽度985px */
+  height: 100%; /* 悬停时右侧悬停面板与左侧面板高度一致 */
   background: #fff;
   box-shadow: 15px 15px 40px rgba(0, 0, 0, 0.1);
-  border-radius: 0 12px 12px 12px;
+  border-radius: 12px;
   z-index: 500;
   border: 1px solid v-bind(categoryThemeColor);
   display: none;
-  flex-direction: column;
   box-sizing: border-box;
   cursor: default;
+  overflow: hidden;
+  transform: translateZ(0); /* 开启3D硬件加速,完美触发圆角剪裁 */
 }
 
-/* 悬停时显示面板 */
-.menu-item:hover .category-panel-mockup {
+/* 悬停显示面板 */
+.category-panel-mockup.is-visible {
+  display: block;
+}
+
+.panel-content-flex {
   display: flex;
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
 }
 
-.panel-content {
+/* 面板左侧分类菜单与标签 */
+.panel-main-side {
   flex: 1;
-  padding: 25px 30px;
-  position: relative; /* 为品牌位定位提供基准 */
+  height: 100%;
+  padding: 20px 24px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
   overflow: hidden;
 }
 
 .panel-tabs {
   display: flex;
-  gap: 15px;
-  margin-bottom: 25px;
-  padding-right: 200px; /* 为右上角品牌位留出空间 */
+  gap: 12px;
+  margin-bottom: 20px;
+  flex-shrink: 0;
 }
 
 .panel-tab-item {
-  padding: 6px 14px;
+  padding: 6px 12px;
   background-color: #f5f5f5;
   color: #666;
   font-size: 12px;
@@ -4602,100 +4752,211 @@ watch(activeSubTab, (newVal) => {
 }
 
 .panel-tab-item:hover {
-  background-color: #f5f5f5; /* 保持浅灰或根据主题调整 */
-  color: v-bind(categoryThemeColor);
-  filter: brightness(0.95);
+  background-color: v-bind(categoryThemeColor);
+  color: #fff;
 }
 
-.panel-body {
-  display: flex;
-  flex-direction: column; /* 改为垂直布局,内容横向撑开 */
+/* 二级分类包裹器 - 自定义半透明主题色滚动条 */
+.panel-groups-container {
   flex: 1;
+  overflow-y: auto;
+  padding-right: 12px;
+  box-sizing: border-box;
 }
 
-.panel-main {
-  flex: 1;
-  overflow-y: auto;
-  max-height: 400px;
+/* 自定义滚动条样式 */
+.panel-groups-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+.panel-groups-container::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.panel-groups-container::-webkit-scrollbar-thumb {
+  background: v-bind(categoryThemeColorAlpha);
+  border-radius: 3px;
+  transition: background 0.2s ease;
+}
+
+.panel-groups-container::-webkit-scrollbar-thumb:hover {
+  background: v-bind(categoryThemeColor);
 }
 
 .category-group {
-  margin-bottom: 15px;
+  margin-bottom: 18px;
   display: flex;
+  align-items: flex-start;
 }
 
 .group-title {
-  width: 80px;
+  width: 90px;
   font-size: 12px;
   font-weight: bold;
   color: #333;
   flex-shrink: 0;
-  line-height: 1.6;
+  line-height: 1.5;
+  padding-top: 1px;
+  cursor: pointer;
+  transition: color 0.15s ease;
+}
+
+.group-title:hover {
+  color: v-bind(categoryThemeColor);
+  text-decoration: underline;
 }
 
 .group-items {
   flex: 1;
   display: flex;
   flex-wrap: wrap;
-  gap: 6px 15px;
+  gap: 8px 16px;
 }
 
 .group-item {
   font-size: 12px;
   color: #666;
   cursor: pointer;
-  transition: color 0.2s;
+  transition: color 0.15s;
 }
 
 .group-item:hover {
   color: v-bind(categoryThemeColor);
 }
 
-/* 品牌位移至右上角 (图3) */
-.panel-side {
-  display: none; /* 移除侧边栏布局 */
+/* 面板右侧多张图片广告位 */
+.panel-ads-side {
+  width: 148px;
+  padding: 20px 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  box-sizing: border-box;
+  flex-shrink: 0;
+  background: #fff;
+  justify-content: flex-start;
+  align-items: center;
+  overflow-y: auto;
+  height: 100%;
 }
 
-.brand-box {
-  position: absolute;
-  top: 25px;
-  right: 30px;
+/* 广告位的隐藏滚动条样式 */
+.panel-ads-side::-webkit-scrollbar {
+  width: 4px;
+}
+
+.panel-ads-side::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.panel-ads-side::-webkit-scrollbar-thumb {
+  background: v-bind(categoryThemeColorAlpha);
+  border-radius: 2px;
+}
+
+.panel-ad-card {
+  width: 116px;
+  height: 54px;
+  border-radius: 4px;
+  overflow: hidden;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+  cursor: pointer;
+  transition:
+    transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+    box-shadow 0.2s ease;
+  flex-shrink: 0;
+  background: #fff;
+}
+
+/* 图片广告悬浮动效 */
+.panel-ad-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+}
+
+.panel-ad-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+}
+
+/* 广告弹窗图片上传卡片 */
+.custom-upload-card-wrapper {
   display: flex;
   flex-direction: column;
-  align-items: flex-end; /* 右对齐更美观 */
-  z-index: 105;
+  align-items: center;
+  gap: 4px;
 }
 
-.brand-main-title {
-  font-size: 20px;
-  font-weight: 900;
-  color: v-bind(categoryThemeColor);
-  margin-bottom: 4px;
-  display: flex;
-  align-items: baseline;
+.custom-upload-card {
+  width: 86px;
+  height: 86px;
+  border-radius: 4px;
+  border: 1px dashed #d9d9d9;
+  overflow: hidden;
+  position: relative;
+  cursor: pointer;
+  background: #fafafa;
+  transition: border-color 0.2s;
 }
 
-.brand-strong {
-  color: #333;
-  margin-left: 2px;
-  font-size: 20px;
+.custom-upload-card:hover {
+  border-color: v-bind(categoryThemeColor);
 }
 
-.brand-notes {
+.custom-upload-card .upload-thumbnail {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.upload-placeholder-box {
+  width: 100%;
+  height: 100%;
   display: flex;
-  gap: 5px;
-  color: v-bind(categoryThemeColor);
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
   font-size: 12px;
-  justify-content: flex-end;
+  color: #999;
+}
+
+.upload-actions-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  opacity: 0;
+  transition: opacity 0.2s;
+}
+
+.custom-upload-card:hover .upload-actions-mask {
+  opacity: 1;
 }
 
-.note-item {
+.upload-actions-mask .action-btn {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.2);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
   cursor: pointer;
+  transition: background 0.2s;
 }
 
-.note-sep {
-  color: #eee;
-  margin: 0 2px;
+.upload-actions-mask .action-btn:hover {
+  background: rgba(255, 255, 255, 0.4);
 }
 
 /* 工具栏主题色设置 - 专业版 */