Quellcode durchsuchen

feat(product): 添加产品专题和特卖链接管理功能

- 新增产品专题表单页面,支持专题标题、副标题、推文类型等字段配置
- 实现专题封面图上传和方案文件选择功能
- 添加产品程序表单页面,支持产品标题、类别、场景等字段配置
- 实现产品封面图和内页广告图的上传与管理功能
- 集成商品列表展示和导入功能,支持批量添加商品
- 新增特卖链接API接口,包括查询、新增、修改、删除等操作
- 完善路由配置,添加专题表单页面路由映射
肖路 vor 1 Monat
Ursprung
Commit
426caa0dd9

+ 5 - 0
src/api/pmsProduct/program/types.ts

@@ -136,6 +136,11 @@ export interface ProgramForm extends BaseEntity {
    */
   remark?: string;
 
+  /**
+   * 商品ID数组
+   */
+  productIds?: Array<string | number>;
+
 }
 
 export interface ProgramQuery extends PageQuery {

+ 74 - 0
src/api/pmsProduct/specialLink/index.ts

@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ProductSpecialLinkVO, ProductSpecialLinkForm, ProductSpecialLinkQuery } from './types';
+
+/**
+ * 查询特价商品列表
+ * @param query
+ * @returns {*}
+ */
+export const listSpecialLink = (query?: ProductSpecialLinkQuery): AxiosPromise<ProductSpecialLinkVO[]> => {
+  return request({
+    url: '/product/specialLink/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询特价商品详细
+ * @param id
+ */
+export const getSpecialLink = (id: string | number): AxiosPromise<ProductSpecialLinkVO> => {
+  return request({
+    url: '/product/specialLink/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增特价商品
+ * @param data
+ */
+export const addSpecialLink = (data: ProductSpecialLinkForm) => {
+  return request({
+    url: '/product/specialLink',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改特价商品
+ * @param data
+ */
+export const updateSpecialLink = (data: ProductSpecialLinkForm) => {
+  return request({
+    url: '/product/specialLink',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除特价商品
+ * @param id
+ */
+export const delSpecialLink = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/specialLink/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 导出特价商品
+ * @param query
+ */
+export const exportSpecialLink = (query?: ProductSpecialLinkQuery) => {
+  return request({
+    url: '/product/specialLink/export',
+    method: 'post',
+    data: query
+  });
+};

+ 154 - 0
src/api/pmsProduct/specialLink/types.ts

@@ -0,0 +1,154 @@
+export interface ProductSpecialLinkVO {
+  /**
+   * 主键ID
+   */
+  id: string | number;
+
+  /**
+   * 商品id
+   */
+  productId: string | number;
+
+  /**
+   * 特殊价格
+   */
+  specialPrice: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+  /**
+   * 产品图片URL
+   */
+  productImage: string;
+
+  /**
+   * 产品名称
+   */
+  itemName: string;
+
+  /**
+   * 市场价
+   */
+  marketPrice: number;
+
+  /**
+   * 平台价
+   */
+  minSellingPrice: number;
+
+  /**
+   * 总库存
+   */
+  totalInventory: number;
+
+}
+
+export interface ProductSpecialLinkForm extends BaseEntity {
+  /**
+   * 主键ID
+   */
+  id?: string | number;
+
+  /**
+   * 商品id
+   */
+  productId?: string | number;
+
+  /**
+   * 特殊价格
+   */
+  specialPrice?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+  /**
+   * 产品图片URL
+   */
+  productImage?: string;
+
+  /**
+   * 产品名称
+   */
+  itemName?: string;
+
+  /**
+   * 市场价
+   */
+  marketPrice?: number;
+
+  /**
+   * 平台价
+   */
+  minSellingPrice?: number;
+
+  /**
+   * 总库存
+   */
+  totalInventory?: number;
+
+}
+
+export interface ProductSpecialLinkQuery extends PageQuery {
+
+  /**
+   * 商品id
+   */
+  productId?: string | number;
+
+  /**
+   * 特殊价格
+   */
+  specialPrice?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+  /**
+   * 产品图片URL
+   */
+  productImage?: string;
+
+  /**
+   * 产品名称
+   */
+  itemName?: string;
+
+  /**
+   * 市场价
+   */
+  marketPrice?: number;
+
+  /**
+   * 平台价
+   */
+  minSellingPrice?: number;
+
+  /**
+   * 总库存
+   */
+  totalInventory?: number;
+
+}

+ 12 - 0
src/router/index.ts

@@ -161,6 +161,18 @@ export const constantRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/product/program/groupProduct.vue'),
         name: 'GroupProduct',
         meta: { title: '商品列表', activeMenu: '/product/program', noCache: true }
+      },
+      {
+        path: 'topics/form',
+        component: () => import('@/views/product/topics/form.vue'),
+        name: 'TopicsForm',
+        meta: { title: '编辑专题', activeMenu: '/product/topics', noCache: true }
+      },
+      {
+        path: 'productProgram/form',
+        component: () => import('@/views/product/productProgram/form.vue'),
+        name: 'ProductProgramForm',
+        meta: { title: '编辑产品', activeMenu: '/product/productProgram', noCache: true }
       }
     ]
   },

+ 434 - 471
src/views/platform/decoration/case/index.vue

@@ -1,578 +1,541 @@
 <template>
-  <div class="case-page">
-    <div class="case-container">
-      <!-- 标题区域(独立卡片,点击可编辑) -->
-      <div class="section-header-card" @click="handleEditHeader">
-        <div class="header-left">
-          <span class="section-title">{{ headerConfig.title }}</span>
-          <span class="section-subtitle">{{ headerConfig.subtitle }}</span>
-        </div>
-        <div class="header-right">
-          <span class="more-link" @click.stop="handleLinkClick">{{ headerConfig.linkText }} ></span>
+  <div class="p-2">
+    <!-- 搜索区域 -->
+    <el-card shadow="never" class="mb-2">
+      <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
+        <el-form-item label="服务标题" prop="caseTitle">
+          <el-input v-model="queryParams.caseTitle" placeholder="请输入服务标题" clearable style="width: 240px" @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="服务类别" prop="projectTypeId">
+          <el-select v-model="queryParams.projectTypeId" placeholder="请选择" clearable style="width: 160px">
+            <el-option v-for="item in projectTypeOptions" :key="item.id" :label="item.projectTypeName" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">
+            <el-icon><Search /></el-icon>搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <el-icon><Refresh /></el-icon>重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 列表区域 -->
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>项目案例信息列表</span>
+          <el-button type="primary" @click="handleAdd">
+            <el-icon><Plus /></el-icon>新增
+          </el-button>
         </div>
-      </div>
-
-      <!-- 项目案例轮播展示区域 -->
-      <div class="case-card">
-        <el-carousel v-if="caseList.length > 0" :interval="5000" height="340px" :autoplay="true" arrow="always" indicator-position="outside">
-          <el-carousel-item v-for="(group, gIndex) in caseGroups" :key="gIndex">
-            <div class="case-row">
-              <div v-for="item in group" :key="item.id" class="case-item">
-                <el-icon class="close-icon" @click.stop="handleHideCase(item)"><CircleClose /></el-icon>
-                <div class="case-image">
-                  <el-image :src="item.imageUrl" fit="cover" class="image">
-                    <template #error>
-                      <div class="image-slot"><el-icon><Picture /></el-icon></div>
-                    </template>
-                  </el-image>
-                </div>
-                <div class="case-info">
-                  <h4 class="case-name">{{ item.title }}</h4>
-                  <p class="case-desc">{{ item.description }}</p>
+      </template>
+
+      <el-table v-loading="loading" :data="tableData" border>
+        <el-table-column label="编号" prop="serviceCaseNo" align="center" width="100" />
+        <el-table-column label="封面图片" align="center" width="120">
+          <template #default="{ row }">
+            <el-image
+              :src="row.caseImage"
+              fit="cover"
+              style="width: 80px; height: 60px; border-radius: 4px"
+              :preview-src-list="[row.caseImage]"
+              preview-teleported
+              lazy
+            >
+              <template #error>
+                <div class="image-placeholder">
+                  <el-icon><Picture /></el-icon>
                 </div>
-              </div>
-            </div>
-          </el-carousel-item>
-        </el-carousel>
-        <el-empty v-else description="暂无推荐案例,请点击搜索添加" />
-      </div>
-
-      <!-- 项目案例搜索列表区域 -->
-      <div class="list-card">
-        <div class="list-title">项目案例搜索列表</div>
-        <div class="search-area">
-          <el-form :inline="true">
-            <el-form-item>
-              <el-input v-model="queryParams.keyword" placeholder="请输入项目案例编号/名称" clearable style="width: 280px" />
+              </template>
+            </el-image>
+          </template>
+        </el-table-column>
+        <el-table-column label="标题" prop="caseTitle" align="center" min-width="200" show-overflow-tooltip />
+        <el-table-column label="分类" prop="projectTypeName" align="center" width="120" />
+        <el-table-column label="推荐" align="center" width="80">
+          <template #default="{ row }">
+            <span :class="row.isRecommend === '1' ? 'status-active' : 'status-inactive'">
+              {{ row.isRecommend === '1' ? '推荐' : '推荐' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="相关推荐" align="center" width="100">
+          <template #default="{ row }">
+            <span :class="row.isRelatedRecommend === '1' ? 'status-active' : 'status-inactive'">
+              {{ row.isRelatedRecommend === '1' ? '推荐' : '推荐' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="是否显示" align="center" width="100">
+          <template #default="{ row }">
+            <span :class="row.isShow === '1' ? 'status-active' : 'status-inactive'">
+              {{ row.isShow === '1' ? '显示' : '显示' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="发布时间" prop="createTime" align="center" width="120" />
+        <el-table-column label="相关" align="center" width="80">
+          <template #default="{ row }">
+            阅读:{{ row.readCount || 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="handleView(row)">查看</el-button>
+            <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
+            <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total > 0"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        :total="total"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 新增/编辑/查看对话框 -->
+    <el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" :disabled="dialog.isView">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="案例标题" prop="caseTitle">
+              <el-input v-model="form.caseTitle" placeholder="请输入案例标题" />
             </el-form-item>
-            <el-form-item>
-              <el-button type="primary" @click="handleOpenSelect">搜索</el-button>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="客户行业" prop="industryCategoryId">
+              <el-select v-model="form.industryCategoryId" placeholder="请选择客户行业" style="width: 100%">
+                <el-option v-for="item in industryOptions" :key="item.id" :label="item.industryCategoryName" :value="item.id" />
+              </el-select>
             </el-form-item>
-          </el-form>
-        </div>
-      </div>
-    </div>
-
-    <!-- 编辑标题对话框 -->
-    <el-dialog v-model="headerDialog.visible" title="编辑标题" width="600px" append-to-body>
-      <el-form :model="headerForm" label-width="100px">
+          </el-col>
+        </el-row>
         <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="标题名称" required>
-              <el-input v-model="headerForm.title" placeholder="请输入标题名称" />
+            <el-form-item label="项目类型" prop="projectTypeId">
+              <el-select v-model="form.projectTypeId" placeholder="请选择项目类型" style="width: 100%">
+                <el-option v-for="item in projectTypeOptions" :key="item.id" :label="item.projectTypeName" :value="item.id" />
+              </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="副标题" required>
-              <el-input v-model="headerForm.subtitle" placeholder="请输入副标题" />
+            <el-form-item label="是否显示">
+              <el-switch v-model="form.isShow" active-value="1" inactive-value="0" />
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="链接文字">
-              <el-input v-model="headerForm.linkText" placeholder="请输入链接文字" />
+            <el-form-item label="是否推荐">
+              <el-switch v-model="form.isRecommend" active-value="1" inactive-value="0" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="链接">
-              <el-input v-model="headerForm.linkUrl" placeholder="请输入链接地址" />
+            <el-form-item label="相关推荐">
+              <el-switch v-model="form.isRelatedRecommend" active-value="1" inactive-value="0" />
             </el-form-item>
           </el-col>
         </el-row>
+        <el-form-item label="上传方案">
+          <file-upload v-model="form.planFile" :limit="1" :disabled="dialog.isView" />
+        </el-form-item>
+        <el-form-item label="案例图片">
+          <div class="case-images">
+            <div v-for="(img, index) in imageList" :key="index" class="image-item">
+              <el-image :src="img.url" fit="cover" class="image-preview" />
+              <div class="image-actions" v-if="!dialog.isView">
+                <el-button type="primary" link size="small" @click="setMainImage(index)">
+                  {{ img.isMain ? '主图' : '主图' }}
+                </el-button>
+                <el-button type="danger" link size="small" @click="removeImage(index)">删除</el-button>
+              </div>
+              <div v-if="img.isMain" class="main-tag">主图</div>
+            </div>
+            <el-upload
+              v-if="!dialog.isView && imageList.length < 5"
+              :action="uploadUrl"
+              :headers="uploadHeaders"
+              :show-file-list="false"
+              :on-success="handleImageSuccess"
+              :before-upload="handleBeforeImageUpload"
+              accept=".jpg,.jpeg,.png"
+              class="image-uploader"
+            >
+              <div class="upload-btn">
+                <el-icon><Plus /></el-icon>
+                <span>上传图片</span>
+              </div>
+            </el-upload>
+          </div>
+        </el-form-item>
+        <el-form-item label="项目简介" prop="projectBrief">
+          <el-input v-model="form.projectBrief" type="textarea" :rows="3" placeholder="请输入项目简介" />
+        </el-form-item>
+        <el-form-item label="项目详情" prop="projectDetail">
+          <editor v-model="form.projectDetail" :min-height="200" :read-only="dialog.isView" />
+        </el-form-item>
       </el-form>
       <template #footer>
-        <el-button type="primary" @click="saveHeaderConfig">确 认</el-button>
-        <el-button @click="headerDialog.visible = false">取 消</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 选择案例对话框 -->
-    <el-dialog v-model="selectDialog.visible" title="选择项目案例" width="900px" append-to-body>
-      <div class="search-bar">
-        <el-input v-model="selectDialog.keyword" placeholder="请输入项目案例编号/名称" clearable style="width: 280px" />
-        <el-button type="primary" @click="loadSelectList">搜索</el-button>
-      </div>
-      <el-table :data="selectList" border style="margin-top: 15px">
-        <el-table-column label="案例编号" align="center" prop="serviceCaseNo" width="120" />
-        <el-table-column label="案例图片" align="center" width="100">
-          <template #default="scope">
-            <el-image :src="scope.row.imageUrl" fit="cover" style="width: 60px; height: 60px; border-radius: 4px" lazy>
-              <template #error><div class="image-placeholder-small"><el-icon><Picture /></el-icon></div></template>
-            </el-image>
-          </template>
-        </el-table-column>
-        <el-table-column label="案例名称" align="center" prop="title" :show-overflow-tooltip="true" min-width="200" />
-        <el-table-column label="首页推荐" align="center" width="100">
-          <template #default="scope">
-            <span :class="scope.row.isLinked ? 'status-show' : 'status-hide'">
-              {{ scope.row.isLinked ? '推荐' : '不推荐' }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" width="120">
-          <template #default="scope">
-            <span v-if="scope.row.isLinked" class="action-link danger" @click="handleUnrecommend(scope.row)">不推荐</span>
-            <span v-else class="action-link primary" @click="handleRecommend(scope.row)">推 荐</span>
-          </template>
-        </el-table-column>
-      </el-table>
-      <pagination
-        v-show="selectDialog.total > 0"
-        v-model:page="selectDialog.pageNum"
-        v-model:limit="selectDialog.pageSize"
-        :total="selectDialog.total"
-        @pagination="loadSelectList"
-      />
-      <template #footer>
-        <el-button @click="selectDialog.visible = false">关 闭</el-button>
+        <el-button type="primary" v-if="!dialog.isView" @click="submitForm">确 认</el-button>
+        <el-button @click="dialog.visible = false">{{ dialog.isView ? '关 闭' : '取 消' }}</el-button>
       </template>
     </el-dialog>
-
   </div>
 </template>
 
 <script setup name="DecorationCase" lang="ts">
-import { ref, reactive, computed, onMounted } from 'vue';
-import { ElMessage, ElMessageBox } from 'element-plus';
-import { Picture, CircleClose } from '@element-plus/icons-vue';
-import { getFloorTitle, addFloorTitle, updateFloorTitle } from '@/api/system/floorTitle';
-import { listServiceCase } from '@/api/product/serviceCase';
-import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
-
-// 标题配置ID(项目案例用id=8)
-const TITLE_ID = 8;
-// 推荐位编号
-const RECOMMEND_NO = 'decoration_case';
-
-// 标题配置
-const headerConfig = ref({
-  id: null as number | null,
-  title: '项目案例',
-  subtitle: '为近5000家终端企业提供电商化集采服务',
-  linkText: '更多案例',
-  linkUrl: ''
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
+import type { ComponentInternalInstance, Ref } from 'vue';
+import type { FormInstance } from 'element-plus';
+import { Search, Refresh, Plus, Picture } from '@element-plus/icons-vue';
+import { listServiceCase, getServiceCase, addServiceCase, updateServiceCase, delServiceCase } from '@/api/product/serviceCase';
+import { listIndustryCategory } from '@/api/customer/industryCategory';
+import { listProjectType } from '@/api/globalSetting/projectType';
+import { globalHeaders } from '@/utils/request';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 上传地址
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
+const uploadHeaders = globalHeaders();
+
+// 查询表单
+const queryFormRef = ref<FormInstance>();
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  caseTitle: '',
+  projectTypeId: undefined as number | undefined
 });
 
-const headerDialog = reactive({ visible: false });
-const headerForm = reactive({
-  id: null as number | null,
+// 列表数据
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+
+// 下拉选项
+const projectTypeOptions = ref<any[]>([]);
+const industryOptions = ref<any[]>([]);
+
+// 表单相关
+const formRef = ref<FormInstance>();
+const dialog = reactive({
+  visible: false,
   title: '',
-  subtitle: '',
-  linkText: '',
-  linkUrl: ''
+  isView: false
 });
 
-// 加载标题配置
-const loadHeaderConfig = async () => {
-  try {
-    const res = await getFloorTitle(TITLE_ID);
-    if (res.data) {
-      const data = res.data;
-      headerConfig.value = {
-        id: data.id,
-        title: data.title || '项目案例',
-        subtitle: data.subtitle || '为近5000家终端企业提供电商化集采服务',
-        linkText: data.linkWord || '更多案例',
-        linkUrl: data.linkUrl || ''
-      };
-    }
-  } catch (error) {
-    console.error('加载标题配置失败', error);
-  }
+const initForm = {
+  id: undefined as number | undefined,
+  caseTitle: '',
+  industryCategoryId: undefined as number | undefined,
+  projectTypeId: undefined as number | undefined,
+  isShow: '1',
+  isRecommend: '0',
+  isRelatedRecommend: '0',
+  planFile: '',
+  caseImage: '',
+  projectBrief: '',
+  projectDetail: ''
 };
+const form = ref({ ...initForm });
+const imageList = ref<{ url: string; ossId?: string; isMain: boolean }[]>([]);
 
-const handleEditHeader = () => {
-  Object.assign(headerForm, headerConfig.value);
-  headerDialog.visible = true;
+const rules = {
+  caseTitle: [{ required: true, message: '请输入案例标题', trigger: 'blur' }],
+  projectTypeId: [{ required: true, message: '请选择项目类型', trigger: 'change' }]
 };
 
-const saveHeaderConfig = async () => {
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
   try {
-    const data = {
-      id: headerForm.id,
-      title: headerForm.title,
-      subtitle: headerForm.subtitle,
-      linkWord: headerForm.linkText,
-      linkUrl: headerForm.linkUrl
-    };
-    if (headerForm.id) {
-      await updateFloorTitle(data);
-    } else {
-      await addFloorTitle(data);
-    }
-    await loadHeaderConfig();
-    headerDialog.visible = false;
-    ElMessage.success('标题配置已保存');
-  } catch (error) {
-    ElMessage.error('保存失败');
+    const res = await listServiceCase(queryParams);
+    tableData.value = res.rows || [];
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
   }
 };
 
-const handleLinkClick = () => {
-  if (headerConfig.value.linkUrl) {
-    window.open(headerConfig.value.linkUrl, '_blank');
+/** 加载下拉选项 */
+const loadOptions = async () => {
+  try {
+    const [typeRes, industryRes] = await Promise.all([
+      listProjectType({ pageSize: 100 }),
+      listIndustryCategory({ pageSize: 100 })
+    ]);
+    projectTypeOptions.value = typeRes.rows || [];
+    industryOptions.value = industryRes.rows || [];
+  } catch (error) {
+    console.error('加载选项失败', error);
   }
 };
 
-// 推荐位
-const recommendId = ref<number | null>(null);
-const linkedCaseIds = ref<Set<string>>(new Set());
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
 
-// 项目案例展示列表
-const caseList = ref<any[]>([]);
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  queryParams.caseTitle = '';
+  queryParams.projectTypeId = undefined;
+  handleQuery();
+};
 
-// 每4个一组,用于轮播
-const caseGroups = computed(() => {
-  const groups: any[][] = [];
-  for (let i = 0; i < caseList.value.length; i += 4) {
-    groups.push(caseList.value.slice(i, i + 4));
-  }
-  return groups;
-});
+/** 新增 */
+const handleAdd = () => {
+  resetForm();
+  dialog.title = '新增项目案例';
+  dialog.isView = false;
+  dialog.visible = true;
+};
 
-// 获取推荐位ID
-const loadRecommendId = async () => {
-  try {
-    const res: any = await listRecommend({ recommendNo: RECOMMEND_NO, pageSize: 1 });
-    if (res.rows && res.rows.length > 0) {
-      recommendId.value = res.rows[0].id;
-    }
-  } catch (error) {
-    console.error('获取推荐位失败', error);
-  }
+/** 查看 */
+const handleView = async (row: any) => {
+  resetForm();
+  dialog.title = '查看项目案例';
+  dialog.isView = true;
+  dialog.visible = true;
+  await loadDetail(row.id);
 };
 
-// 加载已推荐的案例
-const loadCaseList = async () => {
+/** 编辑 */
+const handleEdit = async (row: any) => {
+  resetForm();
+  dialog.title = '编辑项目案例';
+  dialog.isView = false;
+  dialog.visible = true;
+  await loadDetail(row.id);
+};
+
+/** 加载详情 */
+const loadDetail = async (id: number) => {
   try {
-    if (!recommendId.value) await loadRecommendId();
-    if (!recommendId.value) {
-      caseList.value = [];
-      linkedCaseIds.value = new Set();
-      return;
-    }
-    const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
-    const links = linkRes.rows || [];
-    linkedCaseIds.value = new Set(links.map((l: any) => String(l.serviceCaseId)));
-    if (links.length === 0) {
-      caseList.value = [];
-      return;
+    const res = await getServiceCase(id);
+    const data = res.data || {};
+    form.value = {
+      id: data.id,
+      caseTitle: data.caseTitle || '',
+      industryCategoryId: data.industryCategoryId,
+      projectTypeId: data.projectTypeId,
+      isShow: data.isShow || '0',
+      isRecommend: data.isRecommend || '0',
+      isRelatedRecommend: data.isRelatedRecommend || '0',
+      planFile: data.planFile || '',
+      caseImage: data.caseImage || '',
+      projectBrief: data.projectBrief || '',
+      projectDetail: data.projectDetail || ''
+    };
+    // 解析图片列表
+    if (data.caseImage) {
+      const images = data.caseImage.split(',').filter((s: string) => s);
+      imageList.value = images.map((url: string, index: number) => ({
+        url,
+        isMain: index === 0
+      }));
     }
-    const caseIds = links.map((link: any) => link.serviceCaseId);
-    const caseRes: any = await listServiceCase({ ids: caseIds.join(','), pageSize: 100 });
-    const caseMap = new Map((caseRes.rows || []).map((c: any) => [String(c.id), c]));
-    caseList.value = links.map((link: any) => {
-      const serviceCase: any = caseMap.get(String(link.serviceCaseId)) || {};
-      return {
-        id: serviceCase.id || link.serviceCaseId,
-        linkId: link.id,
-        serviceCaseNo: serviceCase.serviceCaseNo || '',
-        title: serviceCase.caseTitle || `案例${link.serviceCaseId}`,
-        description: serviceCase.projectBrief || '',
-        imageUrl: serviceCase.caseImage
-      };
-    });
   } catch (error) {
-    console.error('加载案例列表失败', error);
-    caseList.value = [];
+    console.error('获取详情失败', error);
   }
 };
 
-// 查询参数
-const queryParams = reactive({ keyword: '' });
-
-// 选择案例对话框
-const selectDialog = reactive({
-  visible: false,
-  keyword: '',
-  pageNum: 1,
-  pageSize: 10,
-  total: 0
-});
-const selectList = ref<any[]>([]);
+/** 重置表单 */
+const resetForm = () => {
+  form.value = { ...initForm };
+  imageList.value = [];
+  formRef.value?.resetFields();
+};
 
-const handleOpenSelect = () => {
-  selectDialog.keyword = queryParams.keyword;
-  selectDialog.pageNum = 1;
-  selectDialog.visible = true;
-  loadSelectList();
+/** 删除 */
+const handleDelete = (row: any) => {
+  proxy?.$modal.confirm(`是否确认删除案例"${row.caseTitle}"?`).then(async () => {
+    await delServiceCase(row.id);
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  });
 };
 
-const loadSelectList = async () => {
-  try {
-    const params: any = { pageNum: selectDialog.pageNum, pageSize: selectDialog.pageSize };
-    if (selectDialog.keyword) {
-      // 用caseTitle做模糊查询
-      params.caseTitle = selectDialog.keyword;
-    }
-    const res: any = await listServiceCase(params);
-    selectList.value = (res.rows || []).map((item: any) => ({
-      id: item.id,
-      serviceCaseNo: item.serviceCaseNo,
-      title: item.caseTitle,
-      imageUrl: item.caseImage,
-      isLinked: linkedCaseIds.value.has(String(item.id)),
-      linkId: null
-    }));
-    // 查找linkId
-    if (recommendId.value) {
-      const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
-      const linkMap = new Map((linkRes.rows || []).map((l: any) => [String(l.serviceCaseId), l.id]));
-      selectList.value.forEach((c: any) => {
-        c.linkId = linkMap.get(String(c.id)) || null;
-      });
-    }
-    selectDialog.total = res.total || 0;
-  } catch (error) {
-    console.error('加载案例失败', error);
-    selectList.value = [];
+/** 图片上传前校验 */
+const handleBeforeImageUpload = (file: any) => {
+  const isImage = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type);
+  const isLt5M = file.size / 1024 / 1024 < 5;
+  if (!isImage) {
+    proxy?.$modal.msgError('只能上传 JPG/PNG 格式图片!');
+    return false;
+  }
+  if (!isLt5M) {
+    proxy?.$modal.msgError('图片大小不能超过 5MB!');
+    return false;
   }
+  proxy?.$modal.loading('正在上传...');
+  return true;
 };
 
-const resetSelectQuery = () => {
-  selectDialog.keyword = '';
-  selectDialog.pageNum = 1;
-  loadSelectList();
+/** 图片上传成功 */
+const handleImageSuccess = (res: any) => {
+  proxy?.$modal.closeLoading();
+  if (res.code === 200) {
+    imageList.value.push({
+      url: res.data.url,
+      ossId: res.data.ossId,
+      isMain: imageList.value.length === 0
+    });
+  } else {
+    proxy?.$modal.msgError(res.msg || '上传失败');
+  }
 };
 
-// 推荐案例
-const handleRecommend = async (row: any) => {
-  try {
-    if (!recommendId.value) {
-      ElMessage.error('推荐位不存在');
-      return;
-    }
-    await addRecommendLink({ recommendId: recommendId.value, serviceCaseId: row.id });
-    row.isLinked = true;
-    linkedCaseIds.value.add(String(row.id));
-    await loadCaseList();
-    ElMessage.success('已推荐');
-  } catch (error) {
-    ElMessage.error('操作失败');
-  }
+/** 设置主图 */
+const setMainImage = (index: number) => {
+  imageList.value.forEach((img, i) => {
+    img.isMain = i === index;
+  });
 };
 
-// 取消推荐
-const handleUnrecommend = async (row: any) => {
-  try {
-    if (row.linkId) {
-      await delRecommendLink(row.linkId);
-    }
-    row.isLinked = false;
-    linkedCaseIds.value.delete(String(row.id));
-    await loadCaseList();
-    ElMessage.success('已取消推荐');
-  } catch (error) {
-    ElMessage.error('操作失败');
+/** 删除图片 */
+const removeImage = (index: number) => {
+  const wasMain = imageList.value[index].isMain;
+  imageList.value.splice(index, 1);
+  if (wasMain && imageList.value.length > 0) {
+    imageList.value[0].isMain = true;
   }
 };
 
-// 从展示区隐藏案例
-const handleHideCase = async (item: any) => {
+/** 提交表单 */
+const submitForm = async () => {
+  const valid = await formRef.value?.validate();
+  if (!valid) return;
+
+  // 组装图片URL,主图放第一个
+  const mainImg = imageList.value.find(img => img.isMain);
+  const otherImgs = imageList.value.filter(img => !img.isMain);
+  const allImgs = mainImg ? [mainImg, ...otherImgs] : otherImgs;
+  form.value.caseImage = allImgs.map(img => img.url).join(',');
+
   try {
-    if (item.linkId) {
-      await delRecommendLink(item.linkId);
-      linkedCaseIds.value.delete(String(item.id));
-      await loadCaseList();
-      ElMessage.success('已移除');
+    if (form.value.id) {
+      await updateServiceCase(form.value);
+      proxy?.$modal.msgSuccess('修改成功');
+    } else {
+      await addServiceCase(form.value);
+      proxy?.$modal.msgSuccess('添加成功');
     }
+    dialog.visible = false;
+    getList();
   } catch (error) {
-    ElMessage.error('移除失败');
+    console.error('保存失败', error);
   }
 };
 
 onMounted(() => {
-  loadHeaderConfig();
-  loadRecommendId().then(() => loadCaseList());
+  loadOptions();
+  getList();
 });
 </script>
 
 <style scoped lang="scss">
-.case-page {
-  min-height: 100vh;
-  background: #f5f5f5;
-  padding: 20px;
-}
-
-.case-container {
-  max-width: 1200px;
-  margin: 0 auto;
-}
-
-.section-header-card {
+.card-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding: 15px 20px;
-  background: #fff;
-  border-radius: 4px;
-  margin-bottom: 12px;
-  cursor: pointer;
-
-  &:hover { background: #fafafa; }
-
-  .header-left {
-    display: flex;
-    align-items: baseline;
-    gap: 12px;
-
-    .section-title { font-size: 16px; font-weight: 600; color: #333; }
-    .section-subtitle { font-size: 12px; color: #999; }
-  }
-
-  .header-right {
-    .more-link { font-size: 12px; color: #666; cursor: pointer; &:hover { color: #333; } }
-  }
-}
-
-.case-card {
-  background: #fff;
-  border-radius: 4px;
-  padding: 20px;
-  margin-bottom: 12px;
-
-  :deep(.el-carousel) {
-    .el-carousel__arrow {
-      background-color: rgba(0, 0, 0, 0.3);
-      width: 36px;
-      height: 36px;
-      font-size: 14px;
-      &:hover { background-color: rgba(0, 0, 0, 0.5); }
-    }
-    .el-carousel__arrow--left { left: 10px; }
-    .el-carousel__arrow--right { right: 10px; }
-    .el-carousel__indicators--outside { margin-top: 10px; }
-  }
 }
 
-.case-row {
+.image-placeholder {
+  width: 80px;
+  height: 60px;
   display: flex;
-  gap: 15px;
-  width: 100%;
-  height: 100%;
+  align-items: center;
+  justify-content: center;
+  background: #f5f5f5;
+  color: #ccc;
+  font-size: 24px;
 }
 
-.case-item {
-  flex: 0 0 calc(25% - 12px);
-  max-width: calc(25% - 12px);
-  position: relative;
+.status-active {
+  color: #409eff;
   cursor: pointer;
-  border-radius: 4px;
-  overflow: hidden;
-  background: #f5f5f5;
+}
 
-  .close-icon {
-    position: absolute;
-    top: 8px;
-    right: 8px;
-    font-size: 20px;
-    color: #fff;
-    cursor: pointer;
-    z-index: 10;
-    background: rgba(0, 0, 0, 0.5);
-    border-radius: 50%;
-    padding: 3px;
-    
-    &:hover { background: rgba(245, 108, 108, 0.8); }
-  }
+.status-inactive {
+  color: #909399;
+  cursor: pointer;
+}
 
-  .case-image {
-    width: 100%;
-    height: 160px;
+.case-images {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
 
-    .image { width: 100%; height: 100%; }
+  .image-item {
+    position: relative;
+    width: 100px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    overflow: hidden;
 
-    .image-slot {
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      width: 100%;
-      height: 100%;
-      background: #e8e8e8;
-      color: #909399;
-      font-size: 40px;
+    .image-preview {
+      width: 100px;
+      height: 100px;
     }
-  }
 
-  .case-info {
-    padding: 12px;
-    background: #f5f5f5;
-
-    .case-name {
-      margin: 0 0 8px 0;
-      font-size: 14px;
-      font-weight: 500;
-      color: #303133;
-      line-height: 1.4;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
+    .image-actions {
+      display: flex;
+      justify-content: space-around;
+      padding: 5px 0;
+      background: #f5f7fa;
     }
 
-    .case-desc {
-      margin: 0;
+    .main-tag {
+      position: absolute;
+      top: 0;
+      left: 0;
+      background: #409eff;
+      color: #fff;
       font-size: 12px;
-      color: #606266;
-      line-height: 1.6;
-      height: 58px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      display: -webkit-box;
-      -webkit-line-clamp: 3;
-      -webkit-box-orient: vertical;
+      padding: 2px 6px;
     }
   }
 
-  .case-actions {
-    padding: 5px 12px 10px;
-    background: #f5f5f5;
-  }
-}
-
-.list-card {
-  background: #fff;
-  border-radius: 4px;
-  padding: 20px;
-}
-
-.list-title {
-  font-size: 16px;
-  font-weight: 600;
-  color: #303133;
-  margin-bottom: 20px;
-  padding-bottom: 15px;
-  border-bottom: 1px solid #ebeef5;
-}
-
-.search-area {
-  margin-bottom: 15px;
-}
+  .image-uploader {
+    width: 100px;
+    height: 130px;
+    border: 1px dashed #dcdfe6;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
 
-.search-bar {
-  display: flex;
-  gap: 10px;
-  align-items: center;
-}
+    &:hover {
+      border-color: #409eff;
+    }
 
-.status-show { color: #409eff; }
-.status-hide { color: #f56c6c; }
+    .upload-btn {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      color: #8c939d;
 
-.action-link {
-  cursor: pointer;
-  
-  &.primary { color: #409eff; &:hover { color: #66b1ff; } }
-  &.danger { color: #f56c6c; &:hover { color: #f78989; } }
-}
+      .el-icon {
+        font-size: 24px;
+        margin-bottom: 5px;
+      }
 
-.image-placeholder-small {
-  width: 60px;
-  height: 60px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: #f5f5f5;
-  color: #ccc;
-  font-size: 20px;
+      span {
+        font-size: 12px;
+      }
+    }
+  }
 }
 </style>

+ 4 - 4
src/views/platform/decoration/floor/manage.vue

@@ -23,7 +23,7 @@
       <div class="ad-section">
         <!-- 左侧大图卡片 -->
         <div class="ad-card ad-left-card" @click="handleEditBanner">
-          <img v-if="bannerAd.advertiseImageUrl" :src="bannerAd.advertiseImageUrl" alt="banner" class="banner-img" />
+          <img v-if="bannerAd.advertiseImage" :src="bannerAd.advertiseImage" alt="banner" class="banner-img" />
           <div v-else class="empty-banner">
             <div class="banner-placeholder">
               <div class="placeholder-text">{{ floorInfo.floorName || '楼层名称' }}</div>
@@ -39,7 +39,7 @@
             <div v-for="(item, index) in recommendAds" :key="index" class="recommend-item" @click="handleEditAd(item, index + 1)">
               <span class="index-badge" :class="'badge-' + (index + 1)">{{ index + 1 }}</span>
               <div class="recommend-img">
-                <img v-if="item.advertiseImageUrl" :src="item.advertiseImageUrl" alt="" />
+                <img v-if="item.advertiseImage" :src="item.advertiseImage" alt="" />
                 <div v-else class="img-placeholder"></div>
               </div>
               <div class="recommend-content">
@@ -63,7 +63,7 @@
                   <div v-if="item.advertiseLink" class="discover-btn">立即进入</div>
                 </div>
                 <div class="discover-img-wrap">
-                  <img v-if="item.advertiseImageUrl" :src="item.advertiseImageUrl" class="discover-img" />
+                  <img v-if="item.advertiseImage" :src="item.advertiseImage" class="discover-img" />
                   <div v-else class="discover-img-placeholder"></div>
                 </div>
               </div>
@@ -74,7 +74,7 @@
                 <div class="discover-title">{{ discoverAds[2]?.advertiseName || '标题' }}</div>
                 <div class="discover-sub-big">{{ discoverAds[2]?.advertiseDescribe || '副标题' }}</div>
                 <div class="discover-img-wrap-big">
-                  <img v-if="discoverAds[2]?.advertiseImageUrl" :src="discoverAds[2].advertiseImageUrl" class="discover-img" />
+                  <img v-if="discoverAds[2]?.advertiseImage" :src="discoverAds[2].advertiseImage" class="discover-img" />
                   <div v-else class="discover-img-placeholder"></div>
                 </div>
               </div>

+ 7 - 3
src/views/platform/decoration/special/index.vue

@@ -152,7 +152,7 @@ import { ref, reactive, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
 import { Picture, CircleClose } from '@element-plus/icons-vue';
 import { getFloorTitle, addFloorTitle, updateFloorTitle } from '@/api/system/floorTitle';
-import { listProduct } from '@/api/product/base';
+import { listProduct, getProduct } from '@/api/product/base';
 import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
 
 // 标题配置ID(特价管理用id=5)
@@ -265,8 +265,12 @@ const loadProductList = async () => {
       return;
     }
     const productIds = links.map((link: any) => link.productId);
-    const productRes: any = await listProduct({ ids: productIds.join(','), pageSize: 100 });
-    const productMap = new Map((productRes.rows || []).map((p: any) => [String(p.id), p]));
+    const productDetails = await Promise.all(productIds.map((id: any) => getProduct(id).catch(() => null)));
+    const productMap = new Map(
+      productDetails
+        .filter((res: any) => res && res.data)
+        .map((res: any) => [String(res.data.id), res.data])
+    );
     expertProducts.value = links.map((link: any) => {
       const product: any = productMap.get(String(link.productId)) || {};
       return {

+ 24 - 14
src/views/platform/industrial/booth/index.vue

@@ -142,18 +142,22 @@
       @close="handleSelectDialogClose"
     >
       <!-- 搜索栏 -->
-      <div class="search-bar mb-[10px]">
+      <div class="search-bar mb-[10px]" style="display: flex; gap: 10px; align-items: center;">
         <el-input
-          v-model="searchKeyword"
-          placeholder="请输入商品编号名称输入搜索"
+          v-model="searchProductNo"
+          placeholder="请输入商品编号"
           clearable
-          style="width: 400px"
+          style="width: 200px"
           @keyup.enter="handleSearchProducts"
-        >
-          <template #append>
-            <el-button icon="Search" @click="handleSearchProducts">搜索</el-button>
-          </template>
-        </el-input>
+        />
+        <el-input
+          v-model="searchProductName"
+          placeholder="请输入商品名称"
+          clearable
+          style="width: 200px"
+          @keyup.enter="handleSearchProducts"
+        />
+        <el-button type="primary" icon="Search" @click="handleSearchProducts">搜索</el-button>
       </div>
 
       <!-- 商品表格 -->
@@ -268,7 +272,8 @@ const importDialog = reactive({
 const importProductIds = ref('');
 
 // 搜索关键词
-const searchKeyword = ref('');
+const searchProductNo = ref('');
+const searchProductName = ref('');
 
 // 可选商品列表(从接口获取)
 const availableProducts = ref<any[]>([]);
@@ -301,8 +306,11 @@ const loadAvailableProducts = async () => {
       pageNum: newProductPagination.pageNum,
       pageSize: newProductPagination.pageSize
     };
-    if (searchKeyword.value) {
-      params.itemName = searchKeyword.value;
+    if (searchProductNo.value) {
+      params.productNo = searchProductNo.value;
+    }
+    if (searchProductName.value) {
+      params.itemName = searchProductName.value;
     }
     const res: any = await listProduct(params);
     // 字段映射
@@ -418,7 +426,8 @@ const handleDialogClose = () => {
 const handleAddProduct = () => {
   selectDialog.visible = true;
   selectedProductIds.value = [];
-  searchKeyword.value = '';
+  searchProductNo.value = '';
+  searchProductName.value = '';
   newProductPagination.pageNum = 1;
   // 加载可选商品列表
   loadAvailableProducts();
@@ -434,7 +443,8 @@ const handleAddProduct = () => {
 const handleSelectDialogClose = () => {
   selectDialog.visible = false;
   selectedProductIds.value = [];
-  searchKeyword.value = '';
+  searchProductNo.value = '';
+  searchProductName.value = '';
   // 清除表格选中状态
   if (newProductTableRef.value) {
     newProductTableRef.value.clearSelection();

+ 42 - 5
src/views/platform/industrial/brandFloor/index.vue

@@ -370,8 +370,24 @@ const preloadLinkedData = async () => {
     }));
     linkedProductsCache.value.set(floor.id, products);
     
-    // 品牌缓存
-    linkedBrandsCache.value.set(floor.id, brandRes.data || []);
+    // 品牌缓存:通过 brandId 批量补充 brandName / brandLogo
+    const brandLinks = brandRes.data || [];
+    if (brandLinks.length > 0) {
+      const bIds = brandLinks.map((item: any) => item.brandId).filter(Boolean);
+      if (bIds.length > 0) {
+        const bDetailRes = await listBrand({ ids: bIds.join(','), pageSize: 200 });
+        const bMap = new Map<string, any>((bDetailRes.rows || []).map((b: any) => [String(b.id), b]));
+        const enriched = brandLinks.map((item: any) => {
+          const b = bMap.get(String(item.brandId)) || {};
+          return { ...item, brandName: b.brandName || item.brandName || '', brandLogo: b.brandLogo || item.brandLogo || '' };
+        });
+        linkedBrandsCache.value.set(floor.id, enriched);
+      } else {
+        linkedBrandsCache.value.set(floor.id, brandLinks);
+      }
+    } else {
+      linkedBrandsCache.value.set(floor.id, []);
+    }
   });
 
   await Promise.all(promises);
@@ -516,7 +532,7 @@ const confirmImportProducts = async () => {
   try {
     // 先查询商品信息获取productId
     const productRes = await listProduct({ productNos: newProductNos.join(','), pageSize: 1000 });
-    const productMap = new Map((productRes.rows || []).map((p: any) => [p.productNo, p]));
+    const productMap = new Map<string, any>((productRes.rows || []).map((p: any) => [p.productNo, p]));
 
     for (const productNo of newProductNos) {
       const product = productMap.get(productNo);
@@ -625,9 +641,29 @@ const getLinkedBrands = async () => {
   brandDialog.loading = true;
   try {
     const res = await listIndustrialFloorLinkWithProduct(brandDialog.floorId, 2);
-    linkedBrands.value = res.data || [];
+    const links = res.data || [];
+    if (links.length === 0) {
+      linkedBrands.value = [];
+      linkedBrandsCache.value.set(brandDialog.floorId!, []);
+      return;
+    }
+    // 通过 brandId 批量查询品牌详情,补充 brandName / brandLogo
+    const brandIds = links.map((item: any) => item.brandId).filter(Boolean);
+    const brandMap = new Map<string, any>();
+    if (brandIds.length > 0) {
+      const brandRes = await listBrand({ ids: brandIds.join(','), pageSize: 200 });
+      (brandRes.rows || []).forEach((b: any) => brandMap.set(String(b.id), b));
+    }
+    linkedBrands.value = links.map((item: any) => {
+      const brand = brandMap.get(String(item.brandId)) || {};
+      return {
+        ...item,
+        brandName: brand.brandName || item.brandName || '',
+        brandLogo: brand.brandLogo || item.brandLogo || ''
+      };
+    });
     // 更新缓存
-    linkedBrandsCache.value.set(brandDialog.floorId, [...linkedBrands.value]);
+    linkedBrandsCache.value.set(brandDialog.floorId!, [...linkedBrands.value]);
   } catch (error) {
     console.error('加载关联品牌失败', error);
   } finally {
@@ -703,6 +739,7 @@ const confirmSelectBrands = async () => {
       await addIndustrialFloorLink({
         floorId: brandDialog.floorId,
         type: 2, // 2=品牌
+        brandId: brand.id,
         brandNo: brand.brandNo,
         sort: 0,
         status: '0'

+ 2 - 2
src/views/product/category/index.vue

@@ -46,7 +46,7 @@
         <el-table-column prop="sort" align="center" label="排序" width="150"></el-table-column>
         <el-table-column prop="platform" align="center" label="平台" width="150">
           <template #default="scope">
-            <span>{{ scope.row.platform === 0 ? '工业品' : scope.row.platform === 1 ? 'PC端' : '未知' }}</span>
+            <span>{{ scope.row.platform === 1 ? '工业品' : scope.row.platform === 0 ? 'PC端' : '未知' }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="isShow" align="center" label="是否提示" width="150">
@@ -365,7 +365,7 @@ const handleSetReviewer = (row: CategoryVO) => {
 /** 审核员选择回调 */
 const handleReviewerSelected = async (users: UserVO[]) => {
   if (!currentReviewerCategory.value || users.length === 0) return;
-  
+
   const user = users[0];
   try {
     await setCategoryReviewer(currentReviewerCategory.value.id, user.userId);

+ 647 - 0
src/views/product/productProgram/form.vue

@@ -0,0 +1,647 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex items-center">
+          <el-button link @click="handleBack">
+            <el-icon><ArrowLeft /></el-icon>
+            <span class="ml-1">返回</span>
+          </el-button>
+          <el-divider direction="vertical" />
+          <span class="text-lg font-medium">{{ pageTitle }}</span>
+        </div>
+      </template>
+
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" :disabled="isViewMode">
+        <el-row :gutter="20">
+          <!-- 产品标题 -->
+          <el-col :span="8">
+            <el-form-item label="产品标题" prop="title">
+              <el-input v-model="form.title" placeholder="请输入产品标题" />
+            </el-form-item>
+          </el-col>
+          <!-- 推文类别 -->
+          <el-col :span="8">
+            <el-form-item label="推文类别" prop="category">
+              <el-select v-model="form.category" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="String(item.id)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 适配场景 -->
+          <el-col :span="8">
+            <el-form-item label="适配场景" prop="adaptNo">
+              <el-select v-model="form.adaptNo" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in sceneOptions"
+                  :key="item.id"
+                  :label="item.sceneName"
+                  :value="String(item.id)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 适配行业 -->
+          <el-col :span="8">
+            <el-form-item label="适配行业" prop="industry">
+              <el-select v-model="form.industry" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in industryOptions"
+                  :key="item.id"
+                  :label="item.industryCategoryName"
+                  :value="String(item.id)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 标签 -->
+          <el-col :span="8">
+            <el-form-item label="标签" prop="label">
+              <el-select v-model="form.label" placeholder="请选择" clearable style="width: 100%" @change="handleLabelChange">
+                <el-option
+                  v-for="item in tagOptions"
+                  :key="item.id"
+                  :label="item.tagName"
+                  :value="String(item.id)"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 显示设置 -->
+          <el-col :span="8">
+            <el-form-item label="显示设置" prop="isShow">
+              <el-switch
+                v-model="form.isShow"
+                active-value="1"
+                inactive-value="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 封面图 -->
+          <el-col :span="24">
+            <el-form-item label="封面图" prop="coverImage">
+              <div class="flex items-center gap-2">
+                <el-button @click="openCoverSelector" :disabled="isViewMode">
+                  <el-icon><Picture /></el-icon>
+                  <span class="ml-1">从图片库选择</span>
+                </el-button>
+                <div v-if="form.coverImage" class="ml-4 flex items-center">
+                  <el-image
+                    :src="coverImageUrl"
+                    style="width: 80px; height: 80px"
+                    fit="cover"
+                    :preview-src-list="[coverImageUrl]"
+                  />
+                  <el-button v-if="!isViewMode" link type="danger" class="ml-2" @click="clearCoverImage">
+                    <el-icon><Delete /></el-icon>
+                  </el-button>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 内页广告 -->
+          <el-col :span="24">
+            <el-form-item label="内页广告" prop="innerAdvert">
+              <div class="flex items-center gap-2">
+                <el-button @click="openAdvertSelector" :disabled="isViewMode">
+                  <el-icon><Picture /></el-icon>
+                  <span class="ml-1">从图片库选择</span>
+                </el-button>
+                <div v-if="form.innerAdvert" class="ml-4 flex items-center">
+                  <el-image
+                    :src="advertImageUrl"
+                    style="width: 80px; height: 80px"
+                    fit="cover"
+                    :preview-src-list="[advertImageUrl]"
+                  />
+                  <el-button v-if="!isViewMode" link type="danger" class="ml-2" @click="clearAdvertImage">
+                    <el-icon><Delete /></el-icon>
+                  </el-button>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 描述 -->
+          <el-col :span="24">
+            <el-form-item label="描述" prop="describe">
+              <el-input
+                v-model="form.describe"
+                type="textarea"
+                :rows="5"
+                placeholder="请输入描述内容"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 添加商品区域 -->
+        <el-row :gutter="20" >
+          <el-col :span="24">
+            <el-form-item label="添加商品">
+              <div class="flex items-center gap-2">
+                <el-input
+                  v-model="productKeyword"
+                  placeholder="商品名称/商品编号"
+                  style="width: 200px"
+                  :disabled="isViewMode"
+                />
+                <!-- <el-button @click="handleAddProduct" :disabled="isViewMode">
+                  <el-icon><Plus /></el-icon>
+                  <span class="ml-1">加入</span>
+                </el-button> -->
+                <el-button type="primary" @click="handleImportProduct" :disabled="isViewMode">
+                  <el-icon><Upload /></el-icon>
+                  <span class="ml-1">导入</span>
+                </el-button>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 商品列表 -->
+        <el-row :gutter="20" v-if="productList.length > 0">
+          <el-col :span="24">
+            <el-table :data="productList" border style="width: 100%">
+              <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
+              <el-table-column label="商品图片" align="center" width="100">
+                <template #default="scope">
+                  <image-preview :src="scope.row.productImage" :width="50" :height="50" />
+                </template>
+              </el-table-column>
+              <el-table-column label="商品信息" align="left" min-width="180">
+                <template #default="scope">
+                  <div>{{ scope.row.productName }}</div>
+                  <div class="text-gray">品牌:{{ scope.row.brandName }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column label="类别" align="center" prop="categoryName" width="100" />
+              <el-table-column label="单位" align="center" prop="unit" width="80" />
+              <el-table-column label="市场价" align="center" prop="marketPrice" width="100" />
+              <el-table-column label="平台售价" align="center" prop="platformPrice" width="100" />
+              <el-table-column label="最低售价" align="center" prop="minPrice" width="100" />
+              <el-table-column label="标准成本" align="center" prop="standardCost" width="100" />
+              <el-table-column label="毛利率" align="center" prop="profitRate" width="80" />
+              <el-table-column label="起订量" align="center" prop="minOrder" width="80" />
+              <el-table-column label="操作" align="center" width="80" fixed="right">
+                <template #default="scope">
+                  <el-button link type="danger" @click="handleRemoveProduct(scope.$index)" :disabled="isViewMode">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="!isViewMode" class="mt-4">
+          <el-col :span="24">
+            <el-form-item>
+              <el-button type="primary" :loading="submitLoading" @click="handleSubmit">提 交</el-button>
+              <el-button @click="handleBack">取 消</el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+
+    <!-- 封面图选择器 -->
+    <FileSelector
+      v-model="coverSelectorVisible"
+      title="选择封面图"
+      :allowed-types="[1]"
+      :multiple="false"
+      :allow-upload="true"
+      @confirm="handleCoverSelected"
+    />
+
+    <!-- 广告图选择器 -->
+    <FileSelector
+      v-model="advertSelectorVisible"
+      title="选择内页广告"
+      :allowed-types="[1]"
+      :multiple="false"
+      :allow-upload="true"
+      @confirm="handleAdvertSelected"
+    />
+
+    <!-- 导入商品弹框 -->
+    <el-dialog v-model="importDialogVisible" title="导入商品" width="500px" append-to-body>
+      <el-form :model="importForm" label-width="80px">
+        <el-form-item label="商品编号" required>
+          <el-input
+            v-model="importForm.productNos"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入商品编号,多个商品编号请用逗号隔开,例如:000118163,000118162"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="importDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="importLoading" @click="handleConfirmImport">确 认</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="ProductProgramForm">
+import { ArrowLeft, Picture, Delete, Plus, Upload } from '@element-plus/icons-vue';
+import { getProgram, addProgram, updateProgram } from '@/api/pmsProduct/program';
+import { listPurchaseCategory } from '@/api/globalSetting/purchaseCategory';
+import { listScene } from '@/api/globalSetting/scene';
+import { listIndustryCategory } from '@/api/customer/industryCategory';
+import { listCustomerTag } from '@/api/customer/customerTag';
+import { ProgramForm } from '@/api/pmsProduct/program/types';
+import FileSelector from '@/components/FileSelector/index.vue';
+import { img } from '@/utils/common';
+import { listBase, getBase } from '@/api/pmsProduct/base';
+import { BaseVO } from '@/api/pmsProduct/base/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const route = useRoute();
+
+// 表单引用
+const formRef = ref<ElFormInstance>();
+
+// 加载状态
+const submitLoading = ref(false);
+
+// 页面模式:view-查看 / edit-编辑 / add-新增
+const isViewMode = computed(() => route.query.mode === 'view');
+const pageTitle = computed(() => {
+  if (isViewMode.value) return '查看产品';
+  return form.value.id ? '编辑产品' : '新增产品';
+});
+
+// 下拉选项数据
+const purchaseCategoryOptions = ref<any[]>([]);
+const sceneOptions = ref<any[]>([]);
+const industryOptions = ref<any[]>([]);
+const tagOptions = ref<any[]>([]);
+
+// 对话框状态
+const coverSelectorVisible = ref(false);
+const advertSelectorVisible = ref(false);
+
+// 商品相关
+const productKeyword = ref('');
+const productList = ref<any[]>([]);
+
+// 导入商品弹框
+const importDialogVisible = ref(false);
+const importLoading = ref(false);
+const importForm = reactive({
+  productNos: ''
+});
+
+// 表单数据
+const initFormData: ProgramForm = {
+  id: undefined,
+  programNo: undefined,
+  title: undefined,
+  describe: undefined,
+  category: undefined,
+  isShow: '1',
+  coverImage: undefined,
+  content: undefined,
+  industry: undefined,
+  adaptNo: undefined,
+  label: undefined,
+  innerAdvert: undefined,
+  remark: undefined,
+  productIds: []
+};
+
+const form = ref<ProgramForm>({ ...initFormData });
+
+// 表单验证规则
+const rules = reactive({
+  title: [{ required: true, message: '产品标题不能为空', trigger: 'blur' }]
+});
+
+// 封面图URL计算
+const coverImageUrl = computed(() => {
+  if (form.value.coverImage) {
+    return img(form.value.coverImage);
+  }
+  return '';
+});
+
+// 广告图URL计算
+const advertImageUrl = computed(() => {
+  if (form.value.innerAdvert) {
+    return img(form.value.innerAdvert);
+  }
+  return '';
+});
+
+/** 获取采购分类列表 */
+const getPurchaseCategoryList = async () => {
+  const res = await listPurchaseCategory({ isShow: 1 });
+  purchaseCategoryOptions.value = res.rows || [];
+};
+
+/** 获取适配场景列表 */
+const getSceneList = async () => {
+  const res = await listScene({ isShow: 1 });
+  sceneOptions.value = res.rows || [];
+};
+
+/** 获取行业列表 */
+const getIndustryList = async () => {
+  const res = await listIndustryCategory({ isShow: 1 });
+  industryOptions.value = res.rows || [];
+};
+
+/** 获取标签列表 */
+const getTagList = async () => {
+  const res = await listCustomerTag({ isShow: 1 });
+  tagOptions.value = res.rows || [];
+};
+
+/** 获取详情数据 */
+const getDetail = async (id: string | number) => {
+  const res = await getProgram(id);
+  const data: any = res.data;
+  
+  // 复制字段
+  Object.assign(form.value, data);
+  // 确保 adaptNo、label、industry、category 是字符串类型
+  form.value.adaptNo = data.adaptNo ? String(data.adaptNo) : undefined;
+  form.value.label = data.label ? String(data.label) : undefined;
+  form.value.industry = data.industry ? String(data.industry) : undefined;
+  form.value.category = data.category ? String(data.category) : undefined;
+  
+  // 通过 productIds 查询商品列表并回显
+  const ids: Array<string | number> = data.productIds || [];
+  if (ids.length > 0) {
+    try {
+      const results = await Promise.all(ids.map((pid: string | number) => getBase(pid)));
+      productList.value = results
+        .map(r => r.data)
+        .filter(Boolean)
+        .map((item: any) => ({
+          id: item.id,
+          productNo: item.productNo,
+          productImage: item.productImageUrl || item.productImage,
+          productName: item.itemName || item.productName,
+          brandName: item.brandName || '-',
+          categoryName: item.categoryName || '-',
+          unit: item.unitName || item.unit || '-',
+          marketPrice: item.marketPrice || item.midRangePrice || 0,
+          platformPrice: item.standardPrice || 0,
+          minPrice: item.minSellingPrice || item.certificatePrice || 0,
+          standardCost: item.purchasePrice || item.purchasingPrice || 0,
+          profitRate: item.tempGrossMargin ? `${item.tempGrossMargin}%` : '-',
+          minOrder: item.minOrderQuantity || 1
+        }));
+    } catch (error) {
+      console.error('查询商品详情失败', error);
+    }
+  }
+};
+
+/** 返回列表 */
+const handleBack = () => {
+  router.back();
+};
+
+/** 标签变更时的处理 */
+const handleLabelChange = (value: any) => {
+  if (value !== undefined && value !== null && value !== '') {
+    form.value.label = String(value);
+  }
+};
+
+/** 打开封面图选择器 */
+const openCoverSelector = () => {
+  coverSelectorVisible.value = true;
+};
+
+/** 打开广告图选择器 */
+const openAdvertSelector = () => {
+  advertSelectorVisible.value = true;
+};
+
+/** 处理封面图选择 */
+const handleCoverSelected = (files: any[]) => {
+  if (files && files.length > 0) {
+    form.value.coverImage = files[0].path || files[0].url;
+  }
+};
+
+/** 清除封面图 */
+const clearCoverImage = () => {
+  form.value.coverImage = undefined;
+};
+
+/** 处理广告图选择 */
+const handleAdvertSelected = (files: any[]) => {
+  if (files && files.length > 0) {
+    form.value.innerAdvert = files[0].path || files[0].url;
+  }
+};
+
+/** 清除广告图 */
+const clearAdvertImage = () => {
+  form.value.innerAdvert = undefined;
+};
+
+/** 添加商品 */
+const handleAddProduct = async () => {
+  if (!productKeyword.value) {
+    proxy?.$modal.msgWarning('请输入商品名称或编号');
+    return;
+  }
+  try {
+    const res = await listBase({ searchText: productKeyword.value, pageNum: 1, pageSize: 100 });
+    if (res.rows && res.rows.length > 0) {
+      addProductsToList(res.rows);
+      productKeyword.value = '';
+    } else {
+      proxy?.$modal.msgWarning('未找到匹配的商品');
+    }
+  } catch (error) {
+    proxy?.$modal.msgError('查询商品失败');
+  }
+};
+
+/** 导入商品 */
+const handleImportProduct = () => {
+  importForm.productNos = '';
+  importDialogVisible.value = true;
+};
+
+/** 确认导入商品 */
+const handleConfirmImport = async () => {
+  if (!importForm.productNos.trim()) {
+    proxy?.$modal.msgWarning('请输入商品编号');
+    return;
+  }
+
+  importLoading.value = true;
+  try {
+    // 解析商品编号,支持中英文逗号分隔
+    const productNos = importForm.productNos
+      .replace(/,/g, ',')
+      .split(',')
+      .map(no => no.trim())
+      .filter(no => no);
+
+    if (productNos.length === 0) {
+      proxy?.$modal.msgWarning('请输入有效的商品编号');
+      return;
+    }
+
+    // 逐个查询商品编号
+    const foundProducts: BaseVO[] = [];
+    const notFoundNos: string[] = [];
+
+    for (const productNo of productNos) {
+      const res = await listBase({ productNo, pageNum: 1, pageSize: 100 });
+      if (res.rows && res.rows.length > 0) {
+        foundProducts.push(...res.rows);
+      } else {
+        notFoundNos.push(productNo);
+      }
+    }
+
+    if (foundProducts.length > 0) {
+      addProductsToList(foundProducts);
+    }
+
+    if (notFoundNos.length > 0) {
+      proxy?.$modal.msgWarning(`以下商品编号未找到:${notFoundNos.join(', ')}`);
+    } else {
+      proxy?.$modal.msgSuccess('导入成功');
+    }
+
+    importDialogVisible.value = false;
+  } catch (error) {
+    proxy?.$modal.msgError('导入商品失败');
+  } finally {
+    importLoading.value = false;
+  }
+};
+
+/** 将商品添加到列表 */
+const addProductsToList = (products: BaseVO[]) => {
+  for (const product of products) {
+    // 检查是否已存在
+    const exists = productList.value.some(item => item.productNo === product.productNo);
+    if (!exists) {
+      const productAny = product as any;
+      productList.value.push({
+        id: product.id,
+        productNo: product.productNo,
+        productImage: product.productImageUrl || product.productImage,
+        productName: product.itemName,
+        brandName: productAny.brandName || '-',
+        categoryName: productAny.categoryName || '-',
+        unit: productAny.unitName || '-',
+        marketPrice: product.marketPrice || product.midRangePrice || 0,
+        platformPrice: product.standardPrice || 0,
+        minPrice: product.minSellingPrice || product.certificatePrice || 0,
+        standardCost: product.purchasePrice || product.purchasingPrice || 0,
+        profitRate: product.tempGrossMargin ? `${product.tempGrossMargin}%` : '-',
+        minOrder: product.minOrderQuantity || 1
+      });
+    }
+  }
+};
+
+/** 移除商品 */
+const handleRemoveProduct = (index: number) => {
+  productList.value.splice(index, 1);
+};
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  await formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      submitLoading.value = true;
+      try {
+        // 收集商品ID
+        form.value.productIds = productList.value.map(item => item.id).filter(id => id);
+        
+        if (form.value.id) {
+          await updateProgram(form.value);
+        } else {
+          await addProgram(form.value);
+        }
+        proxy?.$modal.msgSuccess('操作成功');
+        router.back();
+      } finally {
+        submitLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 初始化 */
+onMounted(async () => {
+  // 加载下拉选项
+  await Promise.all([
+    getPurchaseCategoryList(),
+    getSceneList(),
+    getIndustryList(),
+    getTagList()
+  ]);
+
+  // 如果有id参数,加载详情
+  const id = route.query.id;
+  if (id) {
+    await getDetail(id as string);
+  }
+});
+</script>
+
+<style scoped>
+.flex {
+  display: flex;
+}
+.items-center {
+  align-items: center;
+}
+.gap-2 {
+  gap: 8px;
+}
+.ml-1 {
+  margin-left: 4px;
+}
+.ml-2 {
+  margin-left: 8px;
+}
+.ml-4 {
+  margin-left: 16px;
+}
+.mt-4 {
+  margin-top: 16px;
+}
+.text-lg {
+  font-size: 18px;
+}
+.font-medium {
+  font-weight: 500;
+}
+.text-gray {
+  color: #999;
+  font-size: 12px;
+}
+</style>

+ 78 - 230
src/views/product/productProgram/index.vue

@@ -4,35 +4,18 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="方案编号" prop="programNo">
-              <el-input v-model="queryParams.programNo" placeholder="请输入方案编号" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="推文标题" prop="title">
+              <el-input v-model="queryParams.title" placeholder="请输入推文标题" clearable style="width: 240px" @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="方案标题" prop="title">
-              <el-input v-model="queryParams.title" placeholder="请输入方案标题" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="方案描述" prop="describe">
-              <el-input v-model="queryParams.describe" placeholder="请输入方案描述" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="所属分类编号或名称" prop="category">
-              <el-input v-model="queryParams.category" placeholder="请输入所属分类编号或名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否显示:1=是,0=否" prop="isShow">
-              <el-input v-model="queryParams.isShow" placeholder="请输入是否显示:1=是,0=否" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="所属行业编号或名称" prop="industry">
-              <el-input v-model="queryParams.industry" placeholder="请输入所属行业编号或名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="适配产品/设备编号" prop="adaptNo">
-              <el-input v-model="queryParams.adaptNo" placeholder="请输入适配产品/设备编号" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="标签" prop="label">
-              <el-input v-model="queryParams.label" placeholder="请输入标签" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="内部广告内容" prop="innerAdvert">
-              <el-input v-model="queryParams.innerAdvert" placeholder="请输入内部广告内容" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="平台标识" prop="platformCode">
-              <el-input v-model="queryParams.platformCode" placeholder="请输入平台标识" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="推文类别" prop="category">
+              <el-select v-model="queryParams.category" placeholder="请选择" clearable style="width: 200px">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
             </el-form-item>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -45,183 +28,93 @@
 
     <el-card shadow="never">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:program:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:program:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:program:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:program:export']">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
+        <div class="flex justify-between items-center">
+          <span class="text-lg font-medium">产品推荐信息列表</span>
+          <div>
+            <el-button type="primary" icon="Plus" @click="handleAdd" >新增</el-button>
+            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" style="display: inline-block; margin-left: 8px;"></right-toolbar>
+          </div>
+        </div>
       </template>
 
-      <el-table v-loading="loading" border :data="programList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="主键,自增ID" align="center" prop="id" v-if="true" />
-        <el-table-column label="方案编号" align="center" prop="programNo" />
-        <el-table-column label="方案标题" align="center" prop="title" />
-        <el-table-column label="方案描述" align="center" prop="describe" />
-        <el-table-column label="所属分类编号或名称" align="center" prop="category" />
-        <el-table-column label="是否显示:1=是,0=否" align="center" prop="isShow" />
-        <el-table-column label="封面图片URL" align="center" prop="coverImageUrl" width="100">
+      <el-table v-loading="loading" border :data="programList">
+        <el-table-column label="编号" align="center" prop="programNo" width="120" />
+        <el-table-column label="封面图片" align="center" prop="coverImageUrl" width="120">
           <template #default="scope">
-            <image-preview :src="scope.row.coverImageUrl" :width="50" :height="50"/>
+            <image-preview :src="scope.row.coverImageUrl" :width="60" :height="60"/>
           </template>
         </el-table-column>
-        <el-table-column label="方案内容" align="center" prop="content" />
-        <el-table-column label="所属行业编号或名称" align="center" prop="industry" />
-        <el-table-column label="适配产品/设备编号" align="center" prop="adaptNo" />
-        <el-table-column label="标签" align="center" prop="label" />
-        <el-table-column label="内部广告内容" align="center" prop="innerAdvert" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="推文标题" align="center" prop="title" min-width="200" show-overflow-tooltip>
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:program:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:program:remove']"></el-button>
-            </el-tooltip>
+            <el-button link type="primary" @click="handleView(scope.row)">{{ scope.row.title }}</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="推文类型" align="center" prop="category" width="150">
+          <template #default="scope">
+            <span>{{ getCategoryName(scope.row.category) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="是否显示" align="center" prop="isShow" width="100">
+          <template #default="scope">
+            <span :style="{ color: scope.row.isShow === '1' ? '#409eff' : '#f56c6c' }">
+              {{ scope.row.isShow === '1' ? '显示' : '不显示' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="120" fixed="right">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
+            <el-button link type="primary" @click="handleUpdate(scope.row)" >编辑</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)" >删除</el-button>
           </template>
         </el-table-column>
       </el-table>
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
-    <!-- 添加或修改产品解决方案/项目方案对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-      <el-form ref="programFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="方案编号" prop="programNo">
-          <el-input v-model="form.programNo" placeholder="请输入方案编号" />
-        </el-form-item>
-        <el-form-item label="方案标题" prop="title">
-          <el-input v-model="form.title" placeholder="请输入方案标题" />
-        </el-form-item>
-        <el-form-item label="方案描述" prop="describe">
-            <el-input v-model="form.describe" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-        <el-form-item label="所属分类编号或名称" prop="category">
-          <el-input v-model="form.category" placeholder="请输入所属分类编号或名称" />
-        </el-form-item>
-        <el-form-item label="是否显示:1=是,0=否" prop="isShow">
-          <el-input v-model="form.isShow" placeholder="请输入是否显示:1=是,0=否" />
-        </el-form-item>
-        <el-form-item label="封面图片URL" prop="coverImage">
-          <image-upload v-model="form.coverImage"/>
-        </el-form-item>
-        <el-form-item label="方案内容">
-          <editor v-model="form.content" :min-height="192"/>
-        </el-form-item>
-        <el-form-item label="所属行业编号或名称" prop="industry">
-          <el-input v-model="form.industry" placeholder="请输入所属行业编号或名称" />
-        </el-form-item>
-        <el-form-item label="适配产品/设备编号" prop="adaptNo">
-          <el-input v-model="form.adaptNo" placeholder="请输入适配产品/设备编号" />
-        </el-form-item>
-        <el-form-item label="标签" prop="label">
-          <el-input v-model="form.label" placeholder="请输入标签" />
-        </el-form-item>
-        <el-form-item label="内部广告内容" prop="innerAdvert">
-          <el-input v-model="form.innerAdvert" placeholder="请输入内部广告内容" />
-        </el-form-item>
-        <el-form-item label="备注" prop="remark">
-            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script setup name="Program" lang="ts">
-import { listProgram, getProgram, delProgram, addProgram, updateProgram } from '@/api/pmsProduct/program';
-import { ProgramVO, ProgramQuery, ProgramForm } from '@/api/pmsProduct/program/types';
+import { listProgram, delProgram } from '@/api/pmsProduct/program';
+import { listPurchaseCategory } from '@/api/globalSetting/purchaseCategory';
+import { ProgramVO, ProgramQuery } from '@/api/pmsProduct/program/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
 
 const programList = ref<ProgramVO[]>([]);
-const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 
 const queryFormRef = ref<ElFormInstance>();
-const programFormRef = ref<ElFormInstance>();
 
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
+// 下拉选项数据
+const purchaseCategoryOptions = ref<any[]>([]);
 
-const initFormData: ProgramForm = {
-  id: undefined,
-  programNo: undefined,
+const queryParams = ref<ProgramQuery>({
+  pageNum: 1,
+  pageSize: 10,
   title: undefined,
-  describe: undefined,
   category: undefined,
-  isShow: undefined,
-  coverImage: undefined,
-  content: undefined,
-  industry: undefined,
-  adaptNo: undefined,
-  label: undefined,
-  innerAdvert: undefined,
-  remark: undefined,
-}
-const data = reactive<PageData<ProgramForm, ProgramQuery>>({
-  form: {...initFormData},
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    programNo: undefined,
-    title: undefined,
-    describe: undefined,
-    category: undefined,
-    isShow: undefined,
-    coverImage: undefined,
-    content: undefined,
-    industry: undefined,
-    adaptNo: undefined,
-    label: undefined,
-    innerAdvert: undefined,
-    platformCode: undefined,
-    params: {
-    }
-  },
-  rules: {
-    content: [
-      { required: true, message: "方案内容不能为空", trigger: "blur" }
-    ],
-    label: [
-      { required: true, message: "标签不能为空", trigger: "blur" }
-    ],
-    innerAdvert: [
-      { required: true, message: "内部广告内容不能为空", trigger: "blur" }
-    ],
-    remark: [
-      { required: true, message: "备注不能为空", trigger: "blur" }
-    ],
-  }
+  params: {}
 });
 
-const { queryParams, form, rules } = toRefs(data);
+/** 获取采购分类列表 */
+const getPurchaseCategoryList = async () => {
+  const res = await listPurchaseCategory({ isShow: 1 });
+  purchaseCategoryOptions.value = res.rows || [];
+};
+
+/** 获取分类名称 */
+const getCategoryName = (id: any) => {
+  if (!id) return '';
+  const category = purchaseCategoryOptions.value.find(item => String(item.id) === String(id));
+  return category ? category.categoryName : id;
+}
 
-/** 查询产品解决方案/项目方案列表 */
+/** 查询产品推荐列表 */
 const getList = async () => {
   loading.value = true;
   const res = await listProgram(queryParams.value);
@@ -230,18 +123,6 @@ const getList = async () => {
   loading.value = false;
 }
 
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-}
-
-/** 表单重置 */
-const reset = () => {
-  form.value = {...initFormData};
-  programFormRef.value?.resetFields();
-}
-
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
@@ -254,64 +135,31 @@ const resetQuery = () => {
   handleQuery();
 }
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: ProgramVO[]) => {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-}
-
 /** 新增按钮操作 */
 const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = "添加产品解决方案/项目方案";
+  router.push({ path: '/product/productProgram/form' });
 }
 
-/** 修改按钮操作 */
-const handleUpdate = async (row?: ProgramVO) => {
-  reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getProgram(_id);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = "修改产品解决方案/项目方案";
+/** 查看按钮操作 */
+const handleView = (row: ProgramVO) => {
+  router.push({ path: '/product/productProgram/form', query: { id: row.id, mode: 'view' } });
 }
 
-/** 提交按钮 */
-const submitForm = () => {
-  programFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      buttonLoading.value = true;
-      if (form.value.id) {
-        await updateProgram(form.value).finally(() =>  buttonLoading.value = false);
-      } else {
-        await addProgram(form.value).finally(() =>  buttonLoading.value = false);
-      }
-      proxy?.$modal.msgSuccess("操作成功");
-      dialog.visible = false;
-      await getList();
-    }
-  });
+/** 修改按钮操作 */
+const handleUpdate = (row: ProgramVO) => {
+  router.push({ path: '/product/productProgram/form', query: { id: row.id } });
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: ProgramVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除产品解决方案/项目方案编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delProgram(_ids);
+const handleDelete = async (row: ProgramVO) => {
+  await proxy?.$modal.confirm('是否确认删除该产品推荐?').finally(() => loading.value = false);
+  await delProgram(row.id);
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
 }
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('product/program/export', {
-    ...queryParams.value
-  }, `program_${new Date().getTime()}.xlsx`)
-}
-
-onMounted(() => {
+onMounted(async () => {
+  await getPurchaseCategoryList();
   getList();
 });
 </script>

+ 443 - 0
src/views/product/specialLink/index.vue

@@ -0,0 +1,443 @@
+<template>
+  <div class="special-link-page">
+    <!-- 特价商品搜索列表 -->
+    <div class="section-card">
+      <div class="section-title">特价商品搜索列表</div>
+      <div class="search-bar">
+        <el-input v-model="productQuery.productNo" placeholder="请输入商品编号" style="width: 200px" clearable @keyup.enter="handleProductSearch" />
+        <el-button type="primary" @click="handleProductSearch">搜索</el-button>
+        <div class="flex-grow"></div>
+        <el-button icon="Refresh" circle @click="handleProductSearch" />
+      </div>
+      <el-table v-loading="productLoading" :data="productList" border>
+        <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
+        <el-table-column label="商品图片" align="center" width="100">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.productImage || scope.row.productImageUrl"
+              :src="scope.row.productImageUrl || scope.row.productImage"
+              :preview-src-list="[scope.row.productImageUrl || scope.row.productImage]"
+              fit="cover"
+              style="width: 60px; height: 60px"
+            />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="商品名称" align="center" prop="itemName" min-width="180" show-overflow-tooltip />
+        <el-table-column label="商品描述" align="center" prop="remark" min-width="150" show-overflow-tooltip>
+          <template #default="scope">
+            {{ scope.row.remark || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="商品总数" align="center" prop="totalInventory" width="100">
+          <template #default="scope">
+            {{ scope.row.totalInventory ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="剩余商品数" align="center" width="110">
+          <template #default="scope">
+            {{ scope.row.totalInventory ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="商品优惠价格" align="center" width="120">
+          <template #default="scope">
+            {{ scope.row.minSellingPrice ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="商品原价格" align="center" width="110">
+          <template #default="scope">
+            {{ scope.row.marketPrice ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="80">
+          <template #default="scope">
+            <el-link type="primary" :underline="false" @click="handleConfirmProduct(scope.row)">确认</el-link>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="productTotal > 0"
+        v-model:page="productQuery.pageNum"
+        v-model:limit="productQuery.pageSize"
+        :total="productTotal"
+        @pagination="getProductList"
+      />
+    </div>
+
+    <!-- 特价商品管理信息列表 -->
+    <div class="section-card">
+      <div class="section-title">特价商品管理信息列表</div>
+      <div class="search-bar">
+        <el-input v-model="specialQuery.productId" placeholder="请输入商品编号" style="width: 200px" clearable @keyup.enter="handleSpecialSearch" />
+        <el-input v-model="specialQuery.itemName" placeholder="请输入商品名称" style="width: 200px" clearable @keyup.enter="handleSpecialSearch" />
+        <el-button type="primary" icon="Upload" @click="handleOpenImport">导入</el-button>
+        <el-button type="primary" @click="handleGenerateLink">生成链接</el-button>
+        <el-button type="primary" @click="handleSpecialSearch">搜索</el-button>
+        <el-button @click="handleSpecialReset">重置</el-button>
+        <div class="flex-grow"></div>
+        <el-button icon="Refresh" circle @click="getSpecialList" />
+      </div>
+      <el-table v-loading="specialLoading" :data="specialList" border>
+        <el-table-column label="序号" type="index" align="center" width="60" />
+        <el-table-column label="商品编号" align="center" prop="productId" width="120" />
+        <el-table-column label="商品图片" align="center" width="100">
+          <template #default="scope">
+            <el-image
+              v-if="scope.row.productImage"
+              :src="scope.row.productImage"
+              :preview-src-list="[scope.row.productImage]"
+              fit="cover"
+              style="width: 60px; height: 60px"
+            />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="商品名称" align="center" prop="itemName" min-width="180" show-overflow-tooltip />
+        <el-table-column label="商品描述" align="center" prop="remark" min-width="150" show-overflow-tooltip>
+          <template #default="scope">
+            {{ scope.row.remark || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="库存数" align="center" prop="totalInventory" width="80">
+          <template #default="scope">
+            {{ scope.row.totalInventory ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="平台价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.minSellingPrice ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="市场价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.marketPrice ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="80">
+          <template #default="scope">
+            <span :class="scope.row.status === '0' ? 'status-show' : 'status-hide'">
+              {{ scope.row.status === '0' ? '显示' : '不显示' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="120">
+          <template #default="scope">
+            <el-link type="primary" :underline="false" @click="handleEdit(scope.row)">编辑</el-link>
+            <el-link type="danger" :underline="false" @click="handleDelete(scope.row)" class="ml-2">删除</el-link>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="specialTotal > 0"
+        v-model:page="specialQuery.pageNum"
+        v-model:limit="specialQuery.pageSize"
+        :total="specialTotal"
+        @pagination="getSpecialList"
+      />
+    </div>
+
+    <!-- 导入商品弹窗 -->
+    <el-dialog v-model="importDialog.visible" title="导入商品" width="500px" append-to-body>
+      <el-form ref="importFormRef" :model="importForm" :rules="importRules" label-width="80px">
+        <el-form-item label="商品编号" prop="productNos" required>
+          <el-input
+            v-model="importForm.productNos"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入商品编号,多个商品编号请用逗号隔开,例如:000118163,000118162"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="handleImportSubmit">确 认</el-button>
+        <el-button @click="importDialog.visible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 编辑弹窗 -->
+    <el-dialog v-model="editDialog.visible" title="编辑特价商品" width="500px" append-to-body>
+      <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="80px">
+        <el-form-item label="商品编号">
+          <el-input v-model="editForm.productId" disabled />
+        </el-form-item>
+        <el-form-item label="特殊价格" prop="specialPrice">
+          <el-input-number v-model="editForm.specialPrice" :min="0" :precision="2" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="editForm.status">
+            <el-radio value="0">显示</el-radio>
+            <el-radio value="1">不显示</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="editForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="handleEditSubmit">确 认</el-button>
+        <el-button @click="editDialog.visible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="SpecialLink" lang="ts">
+import { listBase } from '@/api/pmsProduct/base';
+import { BaseVO, BaseQuery } from '@/api/pmsProduct/base/types';
+import { listSpecialLink, addSpecialLink, updateSpecialLink, delSpecialLink } from '@/api/pmsProduct/specialLink';
+import { ProductSpecialLinkVO, ProductSpecialLinkQuery, ProductSpecialLinkForm } from '@/api/pmsProduct/specialLink/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 商品搜索列表
+const productLoading = ref(false);
+const productList = ref<BaseVO[]>([]);
+const productTotal = ref(0);
+const productQuery = reactive<BaseQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  productNo: undefined
+});
+
+// 特价商品管理列表
+const specialLoading = ref(false);
+const specialList = ref<ProductSpecialLinkVO[]>([]);
+const specialTotal = ref(0);
+const specialQuery = reactive<ProductSpecialLinkQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  productId: undefined,
+  itemName: undefined
+});
+
+// 导入弹窗
+const importFormRef = ref<ElFormInstance>();
+const importDialog = reactive({
+  visible: false
+});
+const importForm = reactive({
+  productNos: ''
+});
+const importRules = reactive({
+  productNos: [{ required: true, message: '请输入商品编号', trigger: 'blur' }]
+});
+
+// 编辑弹窗
+const editFormRef = ref<ElFormInstance>();
+const editDialog = reactive({
+  visible: false
+});
+const editForm = reactive<ProductSpecialLinkForm>({
+  id: undefined,
+  productId: undefined,
+  specialPrice: undefined,
+  status: '0',
+  remark: ''
+});
+const editRules = reactive({
+  specialPrice: [{ required: true, message: '请输入特殊价格', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+});
+
+/** 获取商品列表 */
+const getProductList = async () => {
+  productLoading.value = true;
+  try {
+    const res = await listBase(productQuery);
+    productList.value = res.rows || [];
+    productTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('获取商品列表失败:', error);
+  } finally {
+    productLoading.value = false;
+  }
+};
+
+/** 商品搜索 */
+const handleProductSearch = () => {
+  productQuery.pageNum = 1;
+  getProductList();
+};
+
+/** 确认添加商品到特价列表 */
+const handleConfirmProduct = async (row: BaseVO) => {
+  try {
+    await proxy?.$modal.confirm('确认将该商品添加到特价商品列表吗?');
+    await addSpecialLink({
+      productId: row.id,
+      specialPrice: row.minSellingPrice,
+      status: '0'
+    });
+    proxy?.$modal.msgSuccess('添加成功');
+    getSpecialList();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      console.error('添加失败:', error);
+    }
+  }
+};
+
+/** 获取特价商品列表 */
+const getSpecialList = async () => {
+  specialLoading.value = true;
+  try {
+    const res = await listSpecialLink(specialQuery);
+    specialList.value = res.rows || [];
+    specialTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('获取特价商品列表失败:', error);
+  } finally {
+    specialLoading.value = false;
+  }
+};
+
+/** 特价商品搜索 */
+const handleSpecialSearch = () => {
+  specialQuery.pageNum = 1;
+  getSpecialList();
+};
+
+/** 特价商品重置 */
+const handleSpecialReset = () => {
+  specialQuery.productId = undefined;
+  specialQuery.itemName = undefined;
+  handleSpecialSearch();
+};
+
+/** 打开导入弹窗 */
+const handleOpenImport = () => {
+  importForm.productNos = '';
+  importDialog.visible = true;
+};
+
+/** 导入提交 */
+const handleImportSubmit = async () => {
+  await importFormRef.value?.validate();
+  try {
+    const productNos = importForm.productNos.split(',').map(no => no.trim()).filter(no => no);
+    if (productNos.length === 0) {
+      proxy?.$modal.msgError('请输入有效的商品编号');
+      return;
+    }
+    // 批量添加
+    for (const productNo of productNos) {
+      await addSpecialLink({
+        productId: productNo,
+        status: '0'
+      });
+    }
+    proxy?.$modal.msgSuccess('导入成功');
+    importDialog.visible = false;
+    getSpecialList();
+  } catch (error) {
+    console.error('导入失败:', error);
+    proxy?.$modal.msgError('导入失败');
+  }
+};
+
+/** 生成链接 */
+const handleGenerateLink = () => {
+  proxy?.$modal.msgWarning('功能开发中');
+};
+
+/** 编辑 */
+const handleEdit = (row: ProductSpecialLinkVO) => {
+  editForm.id = row.id;
+  editForm.productId = row.productId;
+  editForm.specialPrice = row.specialPrice;
+  editForm.status = row.status;
+  editForm.remark = row.remark;
+  editDialog.visible = true;
+};
+
+/** 编辑提交 */
+const handleEditSubmit = async () => {
+  await editFormRef.value?.validate();
+  try {
+    await updateSpecialLink(editForm);
+    proxy?.$modal.msgSuccess('修改成功');
+    editDialog.visible = false;
+    getSpecialList();
+  } catch (error) {
+    console.error('修改失败:', error);
+    proxy?.$modal.msgError('修改失败');
+  }
+};
+
+/** 删除 */
+const handleDelete = async (row: ProductSpecialLinkVO) => {
+  try {
+    await proxy?.$modal.confirm('确认删除该特价商品吗?');
+    await delSpecialLink(row.id);
+    proxy?.$modal.msgSuccess('删除成功');
+    getSpecialList();
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      console.error('删除失败:', error);
+    }
+  }
+};
+
+onMounted(() => {
+  getProductList();
+  getSpecialList();
+});
+</script>
+
+<style scoped lang="scss">
+.special-link-page {
+  padding: 15px;
+  background: #f5f5f5;
+  min-height: calc(100vh - 84px);
+}
+
+.section-card {
+  background: #fff;
+  border-radius: 4px;
+  padding: 20px;
+  margin-bottom: 15px;
+}
+
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 15px;
+  padding-left: 10px;
+  border-left: 3px solid #409eff;
+}
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 15px;
+  flex-wrap: wrap;
+}
+
+.flex-grow {
+  flex: 1;
+}
+
+.status-show {
+  color: #409eff;
+}
+
+.status-hide {
+  color: #f56c6c;
+}
+
+.ml-2 {
+  margin-left: 10px;
+}
+
+:deep(.el-table) {
+  .el-image {
+    border-radius: 4px;
+  }
+}
+
+:deep(.el-dialog__footer) {
+  text-align: right;
+}
+</style>

+ 514 - 0
src/views/product/topics/form.vue

@@ -0,0 +1,514 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex items-center">
+          <el-button link @click="handleBack">
+            <el-icon><ArrowLeft /></el-icon>
+            <span class="ml-1">返回</span>
+          </el-button>
+          <el-divider direction="vertical" />
+          <span class="text-lg font-medium">{{ pageTitle }}</span>
+        </div>
+      </template>
+
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" :disabled="isViewMode">
+        <el-row :gutter="20">
+          <!-- 专题标题 -->
+          <el-col :span="8">
+            <el-form-item label="专题标题" prop="title">
+              <el-input v-model="form.title" placeholder="请输入专题标题" />
+            </el-form-item>
+          </el-col>
+          <!-- 副标题 -->
+          <el-col :span="8">
+            <el-form-item label="副标题" prop="subtitle">
+              <el-input v-model="form.subtitle" placeholder="请输入副标题" />
+            </el-form-item>
+          </el-col>
+          <!-- 推文类型 -->
+          <el-col :span="8">
+            <el-form-item label="推文类型" prop="tweetType">
+              <el-select v-model="form.tweetType" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 推文类别 -->
+          <el-col :span="8">
+            <el-form-item label="推文类别" prop="tweetCategory">
+              <el-select v-model="form.tweetCategory" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 适配场景 -->
+          <el-col :span="8">
+            <el-form-item label="适配场景" prop="adaptNo">
+              <el-select v-model="form.adaptNo" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in sceneOptions"
+                  :key="item.id"
+                  :label="item.sceneName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 适配行业 -->
+          <el-col :span="8">
+            <el-form-item label="适配行业" prop="adaptIndustry">
+              <el-select v-model="form.adaptIndustry" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in industryOptions"
+                  :key="item.id"
+                  :label="item.industryCategoryName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 价格区间 -->
+          <el-col :span="8">
+            <el-form-item label="价格区间" prop="price">
+              <el-input v-model="form.price" placeholder="请输入价格" />
+            </el-form-item>
+          </el-col>
+          <!-- 标签 -->
+          <el-col :span="8">
+            <el-form-item label="标签" prop="lable">
+              <el-select v-model="form.lable" placeholder="请选择" clearable style="width: 100%">
+                <el-option
+                  v-for="item in tagOptions"
+                  :key="item.id"
+                  :label="item.tagName"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <!-- 是否显示 -->
+          <el-col :span="8">
+            <el-form-item label="是否显示" prop="isShow">
+              <el-switch
+                v-model="form.isShow"
+                :active-value="1"
+                :inactive-value="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 相关推荐 -->
+          <el-col :span="8">
+            <el-form-item label="相关推荐" prop="isRelevant">
+              <el-switch
+                v-model="form.isRelevant"
+                :active-value="1"
+                :inactive-value="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 封面图 -->
+          <el-col :span="24">
+            <el-form-item label="封面图" prop="coverImage">
+              <div class="flex items-center gap-2">
+                <el-button @click="openCoverUpload" :disabled="isViewMode">
+                  <el-icon><Upload /></el-icon>
+                  <span class="ml-1">选择上传文件</span>
+                </el-button>
+                <el-button @click="openCoverSelector" :disabled="isViewMode">
+                  <el-icon><Picture /></el-icon>
+                  <span class="ml-1">从图片库选择</span>
+                </el-button>
+                <div v-if="form.coverImage" class="ml-4 flex items-center">
+                  <el-image
+                    :src="coverImageUrl"
+                    style="width: 60px; height: 60px"
+                    fit="cover"
+                    :preview-src-list="[coverImageUrl]"
+                  />
+                  <el-button v-if="!isViewMode" link type="danger" class="ml-2" @click="clearCoverImage">
+                    <el-icon><Delete /></el-icon>
+                  </el-button>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 上传方案 -->
+          <el-col :span="24">
+            <el-form-item label="上传方案" prop="uploadScheme">
+              <div class="flex items-center gap-2">
+                <el-button @click="openSchemeSelector" :disabled="isViewMode">
+                  <el-icon><Upload /></el-icon>
+                  <span class="ml-1">选择文件</span>
+                </el-button>
+                <span v-if="form.uploadScheme" class="ml-4 text-gray-600">{{ schemeFileName }}</span>
+                <el-button v-if="form.uploadScheme && !isViewMode" link type="danger" class="ml-2" @click="clearScheme">
+                  <el-icon><Delete /></el-icon>
+                </el-button>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 描述 -->
+          <el-col :span="24">
+            <el-form-item label="描述" prop="topicsDescribe">
+              <el-input
+                v-model="form.topicsDescribe"
+                type="textarea"
+                :rows="5"
+                placeholder="请输入描述内容"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 专题内容 -->
+          <el-col :span="24">
+            <el-form-item label="专题内容" prop="content">
+              <editor v-model="form.content" :min-height="300" :disabled="isViewMode" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="!isViewMode">
+          <el-col :span="24">
+            <el-form-item>
+              <el-button type="primary" :loading="submitLoading" @click="handleSubmit">提 交</el-button>
+              <el-button @click="handleBack">取 消</el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+
+    <!-- 封面图上传对话框 -->
+    <el-dialog v-model="coverUploadVisible" title="上传封面图" width="500px" append-to-body>
+      <el-upload
+        ref="coverUploadRef"
+        class="upload-demo"
+        drag
+        action="#"
+        :auto-upload="false"
+        :limit="1"
+        accept="image/*"
+        :on-change="handleCoverUploadChange"
+      >
+        <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip>
+          <div class="el-upload__tip">只能上传图片文件</div>
+        </template>
+      </el-upload>
+      <template #footer>
+        <el-button @click="coverUploadVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmCoverUpload">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 封面图选择器 -->
+    <FileSelector
+      v-model="coverSelectorVisible"
+      title="选择封面图"
+      :allowed-types="[1]"
+      :multiple="false"
+      :allow-upload="true"
+      @confirm="handleCoverSelected"
+    />
+
+    <!-- 方案文件选择器 -->
+    <FileSelector
+      v-model="schemeSelectorVisible"
+      title="选择方案文件"
+      :allowed-types="[4]"
+      :multiple="false"
+      :allow-upload="true"
+      @confirm="handleSchemeSelected"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="TopicsForm">
+import { ArrowLeft, Upload, Picture, Delete, UploadFilled } from '@element-plus/icons-vue';
+import { getTopics, addTopics, updateTopics } from '@/api/product/topics';
+import { listPurchaseCategory } from '@/api/globalSetting/purchaseCategory';
+import { listScene } from '@/api/globalSetting/scene';
+import { listIndustryCategory } from '@/api/customer/industryCategory';
+import { listCustomerTag } from '@/api/customer/customerTag';
+import { TopicsForm } from '@/api/product/topics/types';
+import FileSelector from '@/components/FileSelector/index.vue';
+import { img } from '@/utils/common';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const route = useRoute();
+
+// 表单引用
+const formRef = ref<ElFormInstance>();
+const coverUploadRef = ref();
+
+// 加载状态
+const submitLoading = ref(false);
+
+// 页面模式:view-查看 / edit-编辑 / add-新增
+const isViewMode = computed(() => route.query.mode === 'view');
+const pageTitle = computed(() => {
+  if (isViewMode.value) return '查看专题';
+  return form.value.id ? '编辑专题' : '新增专题';
+});
+
+// 下拉选项数据
+const purchaseCategoryOptions = ref<any[]>([]);
+const sceneOptions = ref<any[]>([]);
+const industryOptions = ref<any[]>([]);
+const tagOptions = ref<any[]>([]);
+
+// 对话框状态
+const coverUploadVisible = ref(false);
+const coverSelectorVisible = ref(false);
+const schemeSelectorVisible = ref(false);
+
+// 临时上传文件
+const tempCoverFile = ref<any>(null);
+
+// 方案文件名
+const schemeFileName = ref('');
+
+// 表单数据
+const initFormData: TopicsForm = {
+  id: undefined,
+  procurementTopicsId: undefined,
+  title: undefined,
+  subtitle: undefined,
+  tweetType: undefined,
+  tweetCategory: undefined,
+  adaptNo: undefined,
+  adaptIndustry: undefined,
+  price: undefined,
+  lable: undefined,
+  isShow: 1,
+  isRelevant: 0,
+  coverImage: undefined,
+  uploadScheme: undefined,
+  topicsDescribe: undefined,
+  content: undefined,
+  isHome: undefined,
+  releaseTime: undefined,
+  exclusiveClient: undefined,
+  praise: undefined,
+  releaseStatus: undefined,
+  productCount: undefined,
+  status: undefined,
+  remark: undefined
+};
+
+const form = ref<TopicsForm>({ ...initFormData });
+
+// 表单验证规则
+const rules = reactive({
+  title: [{ required: true, message: '专题标题不能为空', trigger: 'blur' }],
+  tweetType: [{ required: true, message: '推文类型不能为空', trigger: 'change' }],
+  tweetCategory: [{ required: true, message: '推文类别不能为空', trigger: 'change' }]
+});
+
+// 封面图URL计算
+const coverImageUrl = computed(() => {
+  if (form.value.coverImage) {
+    return img(form.value.coverImage);
+  }
+  return '';
+});
+
+/** 获取采购分类列表 */
+const getPurchaseCategoryList = async () => {
+  const res = await listPurchaseCategory({ isShow: 1 });
+  purchaseCategoryOptions.value = res.rows || [];
+};
+
+/** 获取适配场景列表 */
+const getSceneList = async () => {
+  const res = await listScene({ isShow: 1 });
+  sceneOptions.value = res.rows || [];
+};
+
+/** 获取行业列表 */
+const getIndustryList = async () => {
+  const res = await listIndustryCategory({ isShow: 1 });
+  industryOptions.value = res.rows || [];
+};
+
+/** 获取标签列表 */
+const getTagList = async () => {
+  const res = await listCustomerTag({ isShow: 1 });
+  tagOptions.value = res.rows || [];
+};
+
+/** 获取详情数据 */
+const getDetail = async (id: string | number) => {
+  const res = await getTopics(id);
+  Object.assign(form.value, res.data);
+  // 提取方案文件名
+  if (form.value.uploadScheme) {
+    const parts = form.value.uploadScheme.split('/');
+    schemeFileName.value = parts[parts.length - 1] || '已上传文件';
+  }
+};
+
+/** 返回列表 */
+const handleBack = () => {
+  router.back();
+};
+
+/** 打开封面图上传对话框 */
+const openCoverUpload = () => {
+  tempCoverFile.value = null;
+  coverUploadVisible.value = true;
+};
+
+/** 打开封面图选择器 */
+const openCoverSelector = () => {
+  coverSelectorVisible.value = true;
+};
+
+/** 打开方案文件选择器 */
+const openSchemeSelector = () => {
+  schemeSelectorVisible.value = true;
+};
+
+/** 处理封面图上传文件变化 */
+const handleCoverUploadChange = (file: any) => {
+  tempCoverFile.value = file;
+};
+
+/** 确认上传封面图 */
+const confirmCoverUpload = () => {
+  if (tempCoverFile.value) {
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      form.value.coverImage = e.target?.result as string;
+    };
+    reader.readAsDataURL(tempCoverFile.value.raw);
+    coverUploadVisible.value = false;
+  }
+};
+
+/** 处理封面图选择 */
+const handleCoverSelected = (files: any[]) => {
+  if (files && files.length > 0) {
+    form.value.coverImage = files[0].path || files[0].url;
+  }
+};
+
+/** 清除封面图 */
+const clearCoverImage = () => {
+  form.value.coverImage = undefined;
+};
+
+/** 处理方案文件选择 */
+const handleSchemeSelected = (files: any[]) => {
+  if (files && files.length > 0) {
+    form.value.uploadScheme = files[0].path || files[0].url;
+    schemeFileName.value = files[0].name || files[0].originalName || '已选择文件';
+  }
+};
+
+/** 清除方案文件 */
+const clearScheme = () => {
+  form.value.uploadScheme = undefined;
+  schemeFileName.value = '';
+};
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  await formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      submitLoading.value = true;
+      try {
+        if (form.value.id) {
+          await updateTopics(form.value);
+        } else {
+          await addTopics(form.value);
+        }
+        proxy?.$modal.msgSuccess('操作成功');
+        router.back();
+      } finally {
+        submitLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 初始化 */
+onMounted(async () => {
+  // 加载下拉选项
+  await Promise.all([
+    getPurchaseCategoryList(),
+    getSceneList(),
+    getIndustryList(),
+    getTagList()
+  ]);
+
+  // 如果有id参数,加载详情
+  const id = route.query.id;
+  if (id) {
+    await getDetail(id as string);
+  }
+});
+</script>
+
+<style scoped>
+.flex {
+  display: flex;
+}
+.items-center {
+  align-items: center;
+}
+.gap-2 {
+  gap: 8px;
+}
+.ml-1 {
+  margin-left: 4px;
+}
+.ml-2 {
+  margin-left: 8px;
+}
+.ml-4 {
+  margin-left: 16px;
+}
+.text-lg {
+  font-size: 18px;
+}
+.font-medium {
+  font-weight: 500;
+}
+.text-gray-600 {
+  color: #666;
+}
+</style>

+ 102 - 333
src/views/product/topics/index.vue

@@ -4,64 +4,28 @@
       <div v-show="showSearch" class="mb-[10px]">
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="采购主题编号" prop="procurementTopicsId">
-              <el-input v-model="queryParams.procurementTopicsId" placeholder="请输入采购主题编号" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="标题" prop="title">
-              <el-input v-model="queryParams.title" placeholder="请输入标题" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="专题标题" prop="title">
+              <el-input v-model="queryParams.title" placeholder="请输入专题标题" clearable style="width: 240px" @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="推文类型" prop="tweetType">
+              <el-select v-model="queryParams.tweetType" placeholder="请选择" clearable style="width: 200px">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
             </el-form-item>
             <el-form-item label="推文类别" prop="tweetCategory">
-              <el-input v-model="queryParams.tweetCategory" placeholder="请输入推文类别" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否显示" prop="isShow">
-              <el-input v-model="queryParams.isShow" placeholder="请输入是否显示" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="发布时间" prop="releaseTime">
-              <el-date-picker clearable
-                v-model="queryParams.releaseTime"
-                type="date"
-                value-format="YYYY-MM-DD"
-                placeholder="请选择发布时间"
-              />
-            </el-form-item>
-            <el-form-item label="专属客户" prop="exclusiveClient">
-              <el-input v-model="queryParams.exclusiveClient" placeholder="请输入专属客户" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="副标题" prop="subtitle">
-              <el-input v-model="queryParams.subtitle" placeholder="请输入副标题" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="点赞数" prop="praise">
-              <el-input v-model="queryParams.praise" placeholder="请输入点赞数" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="适配编号" prop="adaptNo">
-              <el-input v-model="queryParams.adaptNo" placeholder="请输入适配编号" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="价格" prop="price">
-              <el-input v-model="queryParams.price" placeholder="请输入价格" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="标签" prop="lable">
-              <el-input v-model="queryParams.lable" placeholder="请输入标签" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="产品数量" prop="productCount">
-              <el-input v-model="queryParams.productCount" placeholder="请输入产品数量" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否首页展示" prop="isHome">
-              <el-input v-model="queryParams.isHome" placeholder="请输入是否首页展示" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="上传方案" prop="uploadScheme">
-              <el-input v-model="queryParams.uploadScheme" placeholder="请输入上传方案" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否相关" prop="isRelevant">
-              <el-input v-model="queryParams.isRelevant" placeholder="请输入是否相关" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="适配行业" prop="adaptIndustry">
-              <el-input v-model="queryParams.adaptIndustry" placeholder="请输入适配行业" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="描述" prop="topicsDescribe">
-              <el-input v-model="queryParams.topicsDescribe" placeholder="请输入描述" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="平台标识" prop="platformCode">
-              <el-input v-model="queryParams.platformCode" placeholder="请输入平台标识" clearable @keyup.enter="handleQuery" />
+              <el-select v-model="queryParams.tweetCategory" placeholder="请选择" clearable style="width: 200px">
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
             </el-form-item>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -74,268 +38,113 @@
 
     <el-card shadow="never">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:topics:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:topics:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:topics:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:topics:export']">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
+        <div class="flex justify-between items-center">
+          <span class="text-lg font-medium">采购指南信息列表</span>
+          <div>
+            <el-button type="primary" icon="Plus" @click="handleAdd" >新增</el-button>
+            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" style="display: inline-block; margin-left: 8px;"></right-toolbar>
+          </div>
+        </div>
       </template>
 
       <el-table v-loading="loading" border :data="topicsList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="主键ID,自增" align="center" prop="id" v-if="true" />
-        <el-table-column label="采购主题编号" align="center" prop="procurementTopicsId" />
-        <el-table-column label="标题" align="center" prop="title" />
-        <el-table-column label="推文类型" align="center" prop="tweetType" />
-        <el-table-column label="推文类别" align="center" prop="tweetCategory" />
-        <el-table-column label="是否显示" align="center" prop="isShow" />
-        <el-table-column label="发布时间" align="center" prop="releaseTime" width="180">
+        <el-table-column label="编号" align="center" prop="procurementTopicsId" width="100" />
+        <el-table-column label="标题" align="center" prop="title" min-width="180" show-overflow-tooltip>
           <template #default="scope">
-            <span>{{ parseTime(scope.row.releaseTime, '{y}-{m}-{d}') }}</span>
+            <el-button link type="primary" @click="handleView(scope.row)">{{ scope.row.title }}</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="推文类型" align="center" prop="tweetType" width="120">
+          <template #default="scope">
+            <span>{{ getCategoryName(scope.row.tweetType) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="推文类别" align="center" prop="tweetCategory" width="120">
+          <template #default="scope">
+            <span>{{ getCategoryName(scope.row.tweetCategory) }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="专属客户" align="center" prop="exclusiveClient" />
-        <el-table-column label="内容" align="center" prop="content" />
-        <el-table-column label="封面图片路径或URL" align="center" prop="coverImageUrl" width="100">
+        <el-table-column label="是否显示" align="center" prop="isShow" width="100">
           <template #default="scope">
-            <image-preview :src="scope.row.coverImageUrl" :width="50" :height="50"/>
+            <span :style="{ color: scope.row.isShow === 1 ? '#409eff' : '#f56c6c' }">
+              {{ scope.row.isShow === 1 ? '显示' : '不显示' }}
+            </span>
           </template>
         </el-table-column>
-        <el-table-column label="副标题" align="center" prop="subtitle" />
-        <el-table-column label="点赞数" align="center" prop="praise" />
-        <el-table-column label="发布状态" align="center" prop="releaseStatus" />
-        <el-table-column label="适配编号" align="center" prop="adaptNo" />
-        <el-table-column label="价格" align="center" prop="price" />
-        <el-table-column label="标签" align="center" prop="lable" />
-        <el-table-column label="产品数量" align="center" prop="productCount" />
-        <el-table-column label="是否首页展示" align="center" prop="isHome" />
-        <el-table-column label="上传方案" align="center" prop="uploadScheme" />
-        <el-table-column label="是否相关" align="center" prop="isRelevant" />
-        <el-table-column label="适配行业" align="center" prop="adaptIndustry" />
-        <el-table-column label="描述" align="center" prop="topicsDescribe" />
-        <el-table-column label="状态" align="center" prop="status" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="是否推荐" align="center" prop="isRelevant" width="100">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:topics:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:topics:remove']"></el-button>
-            </el-tooltip>
+            <span :style="{ color: scope.row.isRelevant === 1 ? '#409eff' : '#f56c6c' }">
+              {{ scope.row.isRelevant === 1 ? '推荐' : '不推荐' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="首页推荐" align="center" prop="isHome" width="100">
+          <template #default="scope">
+            <span :style="{ color: scope.row.isHome === 1 ? '#409eff' : '#f56c6c' }">
+              {{ scope.row.isHome === 1 ? '推荐' : '不推荐' }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="发布时间" align="center" prop="releaseTime" width="120">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.releaseTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="专属客户" align="center" prop="exclusiveClient" min-width="120" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" width="120" fixed="right">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
+            <el-button link type="primary" @click="handleUpdate(scope.row)" >编辑</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)" >删除</el-button>
           </template>
         </el-table-column>
       </el-table>
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
-    <!-- 添加或修改采购主题对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-      <el-form ref="topicsFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="采购主题编号" prop="procurementTopicsId">
-          <el-input v-model="form.procurementTopicsId" placeholder="请输入采购主题编号" />
-        </el-form-item>
-        <el-form-item label="标题" prop="title">
-          <el-input v-model="form.title" placeholder="请输入标题" />
-        </el-form-item>
-        <el-form-item label="推文类别" prop="tweetCategory">
-          <el-input v-model="form.tweetCategory" placeholder="请输入推文类别" />
-        </el-form-item>
-        <el-form-item label="是否显示" prop="isShow">
-          <el-input v-model="form.isShow" placeholder="请输入是否显示" />
-        </el-form-item>
-        <el-form-item label="发布时间" prop="releaseTime">
-          <el-date-picker clearable
-            v-model="form.releaseTime"
-            type="datetime"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            placeholder="请选择发布时间">
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="专属客户" prop="exclusiveClient">
-          <el-input v-model="form.exclusiveClient" placeholder="请输入专属客户" />
-        </el-form-item>
-        <el-form-item label="内容">
-          <editor v-model="form.content" :min-height="192"/>
-        </el-form-item>
-        <el-form-item label="封面图片路径或URL" prop="coverImage">
-          <image-upload v-model="form.coverImage"/>
-        </el-form-item>
-        <el-form-item label="副标题" prop="subtitle">
-          <el-input v-model="form.subtitle" placeholder="请输入副标题" />
-        </el-form-item>
-        <el-form-item label="点赞数" prop="praise">
-          <el-input v-model="form.praise" placeholder="请输入点赞数" />
-        </el-form-item>
-        <el-form-item label="适配编号" prop="adaptNo">
-          <el-input v-model="form.adaptNo" placeholder="请输入适配编号" />
-        </el-form-item>
-        <el-form-item label="价格" prop="price">
-          <el-input v-model="form.price" placeholder="请输入价格" />
-        </el-form-item>
-        <el-form-item label="标签" prop="lable">
-          <el-input v-model="form.lable" placeholder="请输入标签" />
-        </el-form-item>
-        <el-form-item label="产品数量" prop="productCount">
-          <el-input v-model="form.productCount" placeholder="请输入产品数量" />
-        </el-form-item>
-        <el-form-item label="是否首页展示" prop="isHome">
-          <el-input v-model="form.isHome" placeholder="请输入是否首页展示" />
-        </el-form-item>
-        <el-form-item label="上传方案" prop="uploadScheme">
-          <el-input v-model="form.uploadScheme" placeholder="请输入上传方案" />
-        </el-form-item>
-        <el-form-item label="是否相关" prop="isRelevant">
-          <el-input v-model="form.isRelevant" placeholder="请输入是否相关" />
-        </el-form-item>
-        <el-form-item label="适配行业" prop="adaptIndustry">
-          <el-input v-model="form.adaptIndustry" placeholder="请输入适配行业" />
-        </el-form-item>
-        <el-form-item label="描述" prop="topicsDescribe">
-            <el-input v-model="form.topicsDescribe" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-        <el-form-item label="备注" prop="remark">
-            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script setup name="Topics" lang="ts">
-import { listTopics, getTopics, delTopics, addTopics, updateTopics } from '@/api/product/topics';
-import { TopicsVO, TopicsQuery, TopicsForm } from '@/api/product/topics/types';
+import { listTopics, delTopics } from '@/api/product/topics';
+import { listPurchaseCategory } from '@/api/globalSetting/purchaseCategory';
+import { TopicsVO, TopicsQuery } from '@/api/product/topics/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
 
 const topicsList = ref<TopicsVO[]>([]);
-const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
 const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 
 const queryFormRef = ref<ElFormInstance>();
-const topicsFormRef = ref<ElFormInstance>();
 
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
+// 下拉选项数据
+const purchaseCategoryOptions = ref<any[]>([]);
 
-const initFormData: TopicsForm = {
-  id: undefined,
-  procurementTopicsId: undefined,
+const queryParams = ref<TopicsQuery>({
+  pageNum: 1,
+  pageSize: 10,
   title: undefined,
   tweetType: undefined,
   tweetCategory: undefined,
-  isShow: undefined,
-  releaseTime: undefined,
-  exclusiveClient: undefined,
-  content: undefined,
-  coverImage: undefined,
-  subtitle: undefined,
-  praise: undefined,
-  releaseStatus: undefined,
-  adaptNo: undefined,
-  price: undefined,
-  lable: undefined,
-  productCount: undefined,
-  isHome: undefined,
-  uploadScheme: undefined,
-  isRelevant: undefined,
-  adaptIndustry: undefined,
-  topicsDescribe: undefined,
-  status: undefined,
-  remark: undefined,
-}
-const data = reactive<PageData<TopicsForm, TopicsQuery>>({
-  form: {...initFormData},
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    procurementTopicsId: undefined,
-    title: undefined,
-    tweetType: undefined,
-    tweetCategory: undefined,
-    isShow: undefined,
-    releaseTime: undefined,
-    exclusiveClient: undefined,
-    content: undefined,
-    coverImage: undefined,
-    subtitle: undefined,
-    praise: undefined,
-    releaseStatus: undefined,
-    adaptNo: undefined,
-    price: undefined,
-    lable: undefined,
-    productCount: undefined,
-    isHome: undefined,
-    uploadScheme: undefined,
-    isRelevant: undefined,
-    adaptIndustry: undefined,
-    topicsDescribe: undefined,
-    status: undefined,
-    platformCode: undefined,
-    params: {
-    }
-  },
-  rules: {
-    procurementTopicsId: [
-      { required: true, message: "采购主题编号不能为空", trigger: "blur" }
-    ],
-    title: [
-      { required: true, message: "标题不能为空", trigger: "blur" }
-    ],
-    tweetType: [
-      { required: true, message: "推文类型不能为空", trigger: "change" }
-    ],
-    tweetCategory: [
-      { required: true, message: "推文类别不能为空", trigger: "blur" }
-    ],
-    exclusiveClient: [
-      { required: true, message: "专属客户不能为空", trigger: "blur" }
-    ],
-    price: [
-      { required: true, message: "价格不能为空", trigger: "blur" }
-    ],
-    uploadScheme: [
-      { required: true, message: "上传方案不能为空", trigger: "blur" }
-    ],
-    adaptIndustry: [
-      { required: true, message: "适配行业不能为空", trigger: "blur" }
-    ],
-    topicsDescribe: [
-      { required: true, message: "描述不能为空", trigger: "blur" }
-    ],
-    status: [
-      { required: true, message: "状态不能为空", trigger: "change" }
-    ],
-    remark: [
-      { required: true, message: "备注不能为空", trigger: "blur" }
-    ],
-  }
+  params: {}
 });
 
-const { queryParams, form, rules } = toRefs(data);
+/** 获取采购分类列表 */
+const getPurchaseCategoryList = async () => {
+  const res = await listPurchaseCategory({ isShow: 1 });
+  purchaseCategoryOptions.value = res.rows || [];
+};
+
+/** 获取分类名称 */
+const getCategoryName = (id: any) => {
+  if (!id) return '';
+  const category = purchaseCategoryOptions.value.find(item => String(item.id) === String(id));
+  return category ? category.categoryName : id;
+}
 
 /** 查询采购主题列表 */
 const getList = async () => {
@@ -346,18 +155,6 @@ const getList = async () => {
   loading.value = false;
 }
 
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-}
-
-/** 表单重置 */
-const reset = () => {
-  form.value = {...initFormData};
-  topicsFormRef.value?.resetFields();
-}
-
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
@@ -373,61 +170,33 @@ const resetQuery = () => {
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: TopicsVO[]) => {
   ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
 }
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = "添加采购主题";
+  router.push({ path: '/product/topics/form' });
 }
 
-/** 修改按钮操作 */
-const handleUpdate = async (row?: TopicsVO) => {
-  reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getTopics(_id);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = "修改采购主题";
+/** 查看按钮操作 */
+const handleView = (row: TopicsVO) => {
+  router.push({ path: '/product/topics/form', query: { id: row.id, mode: 'view' } });
 }
 
-/** 提交按钮 */
-const submitForm = () => {
-  topicsFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      buttonLoading.value = true;
-      if (form.value.id) {
-        await updateTopics(form.value).finally(() =>  buttonLoading.value = false);
-      } else {
-        await addTopics(form.value).finally(() =>  buttonLoading.value = false);
-      }
-      proxy?.$modal.msgSuccess("操作成功");
-      dialog.visible = false;
-      await getList();
-    }
-  });
+/** 修改按钮操作 */
+const handleUpdate = (row: TopicsVO) => {
+  router.push({ path: '/product/topics/form', query: { id: row.id } });
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: TopicsVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除采购主题编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delTopics(_ids);
+const handleDelete = async (row: TopicsVO) => {
+  await proxy?.$modal.confirm('是否确认删除该采购指南?').finally(() => loading.value = false);
+  await delTopics(row.id);
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
 }
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('product/topics/export', {
-    ...queryParams.value
-  }, `topics_${new Date().getTime()}.xlsx`)
-}
-
-onMounted(() => {
+onMounted(async () => {
+  await getPurchaseCategoryList();
   getList();
 });
 </script>