林小张 2 сар өмнө
parent
commit
40f57d991b

+ 29 - 0
src/api/product/serviceCase.ts

@@ -0,0 +1,29 @@
+import request from '@/utils/request';
+
+// 服务案例
+const baseUrl = '/product/serviceCase';
+
+// 查询服务案例列表
+export function listServiceCase(params: any) {
+  return request.get(baseUrl + '/list', { params });
+}
+
+// 获取服务案例详情
+export function getServiceCase(id: number | string) {
+  return request.get(`${baseUrl}/${id}`);
+}
+
+// 新增服务案例
+export function addServiceCase(data: any) {
+  return request.post(baseUrl, data);
+}
+
+// 修改服务案例
+export function updateServiceCase(data: any) {
+  return request.put(baseUrl, data);
+}
+
+// 删除服务案例
+export function delServiceCase(id: number | string) {
+  return request.delete(`${baseUrl}/${id}`);
+}

+ 87 - 218
src/views/platform/decoration/case/index.vue

@@ -30,9 +30,6 @@
                   <h4 class="case-name">{{ item.title }}</h4>
                   <p class="case-desc">{{ item.description }}</p>
                 </div>
-                <div class="case-actions" v-if="false">
-                  <el-button type="primary" size="small" @click.stop="handleConfigProduct(item)">配置商品</el-button>
-                </div>
               </div>
             </div>
           </el-carousel-item>
@@ -97,7 +94,7 @@
         <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="programNo" width="120" />
+        <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>
@@ -108,14 +105,14 @@
         <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.isShow === '1' ? 'status-show' : 'status-hide'">
-              {{ scope.row.isShow === '1' ? '推荐' : '不推荐' }}
+            <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.isShow === '1'" class="action-link danger" @click="handleUnrecommend(scope.row)">不推荐</span>
+            <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>
@@ -132,61 +129,6 @@
       </template>
     </el-dialog>
 
-    <!-- 配置商品对话框 -->
-    <el-dialog v-model="productDialog.visible" :title="`配置商品 - ${productDialog.title}`" width="900px" append-to-body>
-      <div class="product-dialog-header">
-        <el-button type="primary" @click="handleAddProduct">新增商品</el-button>
-        <el-button icon="Refresh" circle size="small" @click="loadLinkedProducts" style="margin-left: auto" />
-      </div>
-      <el-table v-loading="productDialog.loading" :data="linkedProducts" border>
-        <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
-        <el-table-column label="商品名称" align="center" prop="productName" min-width="200" show-overflow-tooltip />
-        <el-table-column label="商品图片" align="center" width="100">
-          <template #default="scope">
-            <el-image v-if="scope.row.productImage" :src="scope.row.productImage" fit="cover" style="width: 60px; height: 60px" />
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" width="80">
-          <template #default="scope">
-            <span class="action-link danger" @click="handleRemoveLinked(scope.row)">删除</span>
-          </template>
-        </el-table-column>
-      </el-table>
-      <template #footer>
-        <el-button @click="productDialog.visible = false">关 闭</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 选择商品对话框 -->
-    <el-dialog v-model="productSelectDialog.visible" title="选择商品" width="900px" append-to-body>
-      <div class="search-bar">
-        <el-input v-model="productSelectDialog.keyword" placeholder="请输入商品编号或名称" clearable style="width: 280px" />
-        <el-button type="primary" @click="loadProductList">搜索</el-button>
-      </div>
-      <el-table v-loading="productSelectDialog.loading" :data="productList" border style="margin-top: 15px" @selection-change="handleProductSelectionChange">
-        <el-table-column type="selection" width="50" align="center" />
-        <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
-        <el-table-column label="商品名称" align="center" prop="itemName" min-width="180" show-overflow-tooltip />
-        <el-table-column label="商品图片" align="center" width="100">
-          <template #default="scope">
-            <el-image v-if="scope.row.productImage" :src="scope.row.productImage" fit="cover" style="width: 60px; height: 60px" />
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-      </el-table>
-      <pagination
-        v-show="productSelectDialog.total > 0"
-        v-model:page="productSelectDialog.pageNum"
-        v-model:limit="productSelectDialog.pageSize"
-        :total="productSelectDialog.total"
-        @pagination="loadProductList"
-      />
-      <template #footer>
-        <el-button type="primary" @click="confirmSelectProducts">确 定</el-button>
-        <el-button @click="productSelectDialog.visible = false">取 消</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
@@ -195,13 +137,13 @@ 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 { listProgram, updateProgram, listProgramLink, addProgramLink, delProgramLink } from '@/api/product/program';
-import { listProduct } from '@/api/product/base';
+import { listServiceCase } from '@/api/product/serviceCase';
+import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
 
-// 分类标识
-const CATEGORY = 'case';
 // 标题配置ID(项目案例用id=8)
 const TITLE_ID = 8;
+// 推荐位编号
+const RECOMMEND_NO = 'decoration_case';
 
 // 标题配置
 const headerConfig = ref({
@@ -273,6 +215,10 @@ const handleLinkClick = () => {
   }
 };
 
+// 推荐位
+const recommendId = ref<number | null>(null);
+const linkedCaseIds = ref<Set<string>>(new Set());
+
 // 项目案例展示列表
 const caseList = ref<any[]>([]);
 
@@ -285,17 +231,48 @@ const caseGroups = computed(() => {
   return groups;
 });
 
+// 获取推荐位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 loadCaseList = async () => {
   try {
-    const res: any = await listProgram({ category: CATEGORY, isShow: '1', pageSize: 100 });
-    caseList.value = (res.rows || []).map((item: any) => ({
-      id: item.id,
-      programNo: item.programNo,
-      title: item.title,
-      description: item.describe || '',
-      imageUrl: item.coverImage || item.coverImageUrl
-    }));
+    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 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 = [];
@@ -324,19 +301,28 @@ const handleOpenSelect = () => {
 
 const loadSelectList = async () => {
   try {
-    const params: any = { category: CATEGORY, pageNum: selectDialog.pageNum, pageSize: selectDialog.pageSize };
+    const params: any = { pageNum: selectDialog.pageNum, pageSize: selectDialog.pageSize };
     if (selectDialog.keyword) {
-      // 用title做模糊查询
-      params.title = selectDialog.keyword;
+      // 用caseTitle做模糊查询
+      params.caseTitle = selectDialog.keyword;
     }
-    const res: any = await listProgram(params);
+    const res: any = await listServiceCase(params);
     selectList.value = (res.rows || []).map((item: any) => ({
       id: item.id,
-      programNo: item.programNo,
-      title: item.title,
-      imageUrl: item.coverImage || item.coverImageUrl,
-      isShow: item.isShow
+      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);
@@ -353,8 +339,13 @@ const resetSelectQuery = () => {
 // 推荐案例
 const handleRecommend = async (row: any) => {
   try {
-    await updateProgram({ id: row.id, isShow: '1' });
-    row.isShow = '1';
+    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) {
@@ -365,8 +356,11 @@ const handleRecommend = async (row: any) => {
 // 取消推荐
 const handleUnrecommend = async (row: any) => {
   try {
-    await updateProgram({ id: row.id, isShow: '0' });
-    row.isShow = '0';
+    if (row.linkId) {
+      await delRecommendLink(row.linkId);
+    }
+    row.isLinked = false;
+    linkedCaseIds.value.delete(String(row.id));
     await loadCaseList();
     ElMessage.success('已取消推荐');
   } catch (error) {
@@ -377,9 +371,12 @@ const handleUnrecommend = async (row: any) => {
 // 从展示区隐藏案例
 const handleHideCase = async (item: any) => {
   try {
-    await updateProgram({ id: item.id, isShow: '0' });
-    await loadCaseList();
-    ElMessage.success('已移除');
+    if (item.linkId) {
+      await delRecommendLink(item.linkId);
+      linkedCaseIds.value.delete(String(item.id));
+      await loadCaseList();
+      ElMessage.success('已移除');
+    }
   } catch (error) {
     ElMessage.error('移除失败');
   }
@@ -387,129 +384,8 @@ const handleHideCase = async (item: any) => {
 
 onMounted(() => {
   loadHeaderConfig();
-  loadCaseList();
-});
-
-// 配置商品相关
-const productDialog = reactive({
-  visible: false,
-  loading: false,
-  programId: null as number | null,
-  title: ''
-});
-const linkedProducts = ref<any[]>([]);
-
-// 打开配置商品弹框
-const handleConfigProduct = (item: any) => {
-  productDialog.programId = item.id;
-  productDialog.title = item.title;
-  productDialog.visible = true;
-  loadLinkedProducts();
-};
-
-// 加载已关联的商品
-const loadLinkedProducts = async () => {
-  if (!productDialog.programId) return;
-  productDialog.loading = true;
-  try {
-    const res: any = await listProgramLink({ programId: productDialog.programId, pageSize: 100 });
-    linkedProducts.value = res.rows || [];
-  } catch (error) {
-    console.error('加载关联商品失败', error);
-    linkedProducts.value = [];
-  } finally {
-    productDialog.loading = false;
-  }
-};
-
-// 删除关联商品
-const handleRemoveLinked = (row: any) => {
-  ElMessageBox.confirm('是否确认删除该商品?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(async () => {
-    await delProgramLink(row.id);
-    ElMessage.success('删除成功');
-    loadLinkedProducts();
-  }).catch(() => {});
-};
-
-// 选择商品弹框
-const productSelectDialog = reactive({
-  visible: false,
-  loading: false,
-  keyword: '',
-  pageNum: 1,
-  pageSize: 10,
-  total: 0
+  loadRecommendId().then(() => loadCaseList());
 });
-const productList = ref<any[]>([]);
-const selectedProducts = ref<any[]>([]);
-
-// 新增商品
-const handleAddProduct = () => {
-  productSelectDialog.keyword = '';
-  productSelectDialog.pageNum = 1;
-  productSelectDialog.visible = true;
-  loadProductList();
-};
-
-// 加载商品列表
-const loadProductList = async () => {
-  productSelectDialog.loading = true;
-  try {
-    const res: any = await listProduct({
-      keyword: productSelectDialog.keyword,
-      pageNum: productSelectDialog.pageNum,
-      pageSize: productSelectDialog.pageSize
-    });
-    productList.value = res.rows || [];
-    productSelectDialog.total = res.total || 0;
-  } catch (error) {
-    console.error('加载商品列表失败', error);
-    productList.value = [];
-  } finally {
-    productSelectDialog.loading = false;
-  }
-};
-
-// 选择变化
-const handleProductSelectionChange = (selection: any[]) => {
-  selectedProducts.value = selection;
-};
-
-// 确认选择商品
-const confirmSelectProducts = async () => {
-  if (selectedProducts.value.length === 0) {
-    ElMessage.warning('请选择商品');
-    return;
-  }
-
-  // 检查是否有重复商品
-  const existingProductIds = linkedProducts.value.map((item: any) => String(item.productId));
-  const duplicates = selectedProducts.value.filter((p: any) => existingProductIds.includes(String(p.id)));
-
-  if (duplicates.length > 0) {
-    const duplicateNames = duplicates.map((p: any) => p.itemName || p.productNo).join('、');
-    ElMessage.warning(`商品 ${duplicateNames} 已存在,请勿重复添加`);
-    return;
-  }
-
-  try {
-    for (const product of selectedProducts.value) {
-      await addProgramLink({
-        programId: productDialog.programId,
-        productId: product.id
-      });
-    }
-    ElMessage.success('添加成功');
-    productSelectDialog.visible = false;
-    loadLinkedProducts();
-  } catch (error) {
-    ElMessage.error('添加失败');
-  }
-};
 </script>
 
 <style scoped lang="scss">
@@ -699,11 +575,4 @@ const confirmSelectProducts = async () => {
   color: #ccc;
   font-size: 20px;
 }
-
-.product-dialog-header {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin-bottom: 15px;
-}
 </style>

+ 115 - 28
src/views/platform/decoration/flashSale/index.vue

@@ -179,11 +179,14 @@ import { listFloorAdvertManage, getFloorAdvertManage, addFloorAdvertManage, upda
 import { getFloorTitle, addFloorTitle, updateFloorTitle } from '@/api/system/floorTitle';
 import { listByIds } from '@/api/system/oss';
 import { listBrand } from '@/api/product/brand';
+import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
 
 // 广告位置:4=商品轮播/大图(品牌闪购大图)
 const ADVERT_POSITION = 4;
 // 标题配置ID(品牌闪购用id=4)
 const TITLE_ID = 4;
+// 推荐位编号
+const RECOMMEND_NO = 'decoration_brand_flash';
 
 const searchKeyword = ref('');
 const defaultBannerUrl = 'https://via.placeholder.com/200x300/1a237e/FFFFFF?text=品牌广告';
@@ -320,19 +323,55 @@ const saveHeaderConfig = async () => {
   } catch (error) { ElMessage.error('保存失败'); }
 };
 
+// 推荐位
+const recommendId = ref<number | null>(null);
+const linkedBrandIds = ref<Set<string>>(new Set());
+
 // 品牌列表
 const brandList = ref<any[]>([]);
 
+// 获取推荐位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 loadBrandList = async () => {
   try {
-    const res: any = await listBrand({ pageNum: 1, pageSize: 10, isShow: 1 });
-    brandList.value = (res.rows || []).map((item: any) => ({
-      id: item.id,
-      title: item.brandName,
-      description: item.brandDescribe || item.brandName,
-      logoUrl: item.brandBigImageUrl || item.brandLogo
-    }));
+    if (!recommendId.value) await loadRecommendId();
+    if (!recommendId.value) {
+      brandList.value = [];
+      linkedBrandIds.value = new Set();
+      return;
+    }
+    const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
+    const links = (linkRes.rows || []).sort((a: any, b: any) => (a.sort || 0) - (b.sort || 0));
+    linkedBrandIds.value = new Set(links.map((l: any) => String(l.brandId)));
+    if (links.length === 0) {
+      brandList.value = [];
+      return;
+    }
+    const brandIds = links.map((link: any) => link.brandId);
+    const brandRes: any = await listBrand({ ids: brandIds.join(','), pageSize: 100 });
+    const brandMap = new Map((brandRes.rows || []).map((b: any) => [String(b.id), b]));
+    brandList.value = links.map((link: any) => {
+      const brand: any = brandMap.get(String(link.brandId)) || {};
+      return {
+        id: brand.id || link.brandId,
+        linkId: link.id,
+        title: brand.brandName || `品牌${link.brandId}`,
+        description: brand.brandDescribe || brand.brandName || '',
+        logoUrl: brand.brandBigImageUrl || brand.brandLogo,
+        sort: link.sort || 0
+      };
+    });
   } catch (error) {
     console.error('加载品牌列表失败', error);
     brandList.value = [];
@@ -364,8 +403,18 @@ const loadSearchBrands = async () => {
       id: item.id,
       brandNo: item.brandNo,
       brandName: item.brandName,
-      coverImage: item.brandBigImageUrl || item.brandLogo
+      coverImage: item.brandBigImageUrl || item.brandLogo,
+      isLinked: linkedBrandIds.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.brandId), l.id]));
+      selectDialog.list.forEach((b: any) => {
+        b.linkId = linkMap.get(String(b.id)) || null;
+      });
+    }
     selectDialog.total = res.total || 0;
   } catch (error) {
     console.error('搜索品牌失败', error);
@@ -382,38 +431,76 @@ const handleSearch = () => {
 };
 
 // 确认选择品牌
-const handleConfirmBrand = (row: any) => {
-  // 检查是否已存在
-  if (brandList.value.find((b) => String(b.id) === String(row.id))) {
-    ElMessage.warning('该品牌已添加');
-    return;
+const handleConfirmBrand = async (row: any) => {
+  try {
+    if (!recommendId.value) {
+      ElMessage.error('推荐位不存在');
+      return;
+    }
+    if (linkedBrandIds.value.has(String(row.id))) {
+      ElMessage.warning('该品牌已添加');
+      return;
+    }
+    if (brandList.value.length >= 10) {
+      ElMessage.warning('最多只能添加10个品牌');
+      return;
+    }
+    const maxSort = brandList.value.length > 0 ? Math.max(...brandList.value.map(b => b.sort || 0)) : 0;
+    await addRecommendLink({ recommendId: recommendId.value, brandId: row.id, sort: maxSort + 1 });
+    linkedBrandIds.value.add(String(row.id));
+    await loadBrandList();
+    ElMessage.success('添加成功');
+  } catch (error) {
+    ElMessage.error('添加失败');
   }
-  brandList.value.push({
-    id: row.id,
-    title: row.brandName,
-    description: row.brandName,
-    logoUrl: row.coverImage
-  });
-  ElMessage.success('添加成功');
 };
 
 // 品牌操作
-const handleRemoveBrand = (index: number) => { brandList.value.splice(index, 1); ElMessage.success('已移除'); };
+const handleRemoveBrand = async (index: number) => {
+  try {
+    const item = brandList.value[index];
+    if (item.linkId) {
+      await delRecommendLink(item.linkId);
+      linkedBrandIds.value.delete(String(item.id));
+      await loadBrandList();
+      ElMessage.success('已移除');
+    }
+  } catch (error) {
+    ElMessage.error('移除失败');
+  }
+};
 
 // 排序对话框
-const sortDialog = reactive({ visible: false, index: 0, sortValue: 0 });
+const sortDialog = reactive({ visible: false, index: 0, sortValue: 0, linkId: null as number | null });
 const handleSort = (index: number) => {
+  const item = brandList.value[index];
   sortDialog.index = index;
-  sortDialog.sortValue = index;
+  sortDialog.sortValue = item.sort || index;
+  sortDialog.linkId = item.linkId;
   sortDialog.visible = true;
 };
-const saveSortConfig = () => {
-  // 这里可以保存排序值到后端,目前只是前端展示
-  sortDialog.visible = false;
-  ElMessage.success('排序已保存');
+const saveSortConfig = async () => {
+  try {
+    if (!sortDialog.linkId) {
+      ElMessage.error('无法保存排序');
+      return;
+    }
+    await delRecommendLink(sortDialog.linkId);
+    const item = brandList.value[sortDialog.index];
+    await addRecommendLink({
+      recommendId: recommendId.value,
+      brandId: item.id,
+      sort: sortDialog.sortValue
+    });
+    await loadBrandList();
+    sortDialog.visible = false;
+    ElMessage.success('排序已保存');
+  } catch (error) {
+    ElMessage.error('保存失败');
+  }
 };
 
-onMounted(() => { loadHeaderConfig(); loadBannerConfig(); loadBrandList(); });
+onMounted(() => { loadHeaderConfig(); loadBannerConfig(); loadRecommendId().then(() => loadBrandList()); });
 </script>
 
 

+ 83 - 25
src/views/platform/decoration/guide/index.vue

@@ -110,14 +110,14 @@
         <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.isShow === '1' ? 'status-show' : 'status-hide'">
-              {{ scope.row.isShow === '1' ? '推荐' : '不推荐' }}
+            <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.isShow === '1'" class="action-link danger" @click="handleUnrecommend(scope.row)">不推荐</span>
+            <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>
@@ -197,13 +197,16 @@ import { ref, reactive, 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 { listProgram, updateProgram, listProgramLink, addProgramLink, delProgramLink } from '@/api/product/program';
+import { listProgram, listProgramLink, addProgramLink, delProgramLink } from '@/api/product/program';
 import { listProduct } from '@/api/product/base';
+import { listRecommend, listRecommendLink, addRecommendLink, delRecommendLink } from '@/api/product/recommend';
 
 // 分类标识
 const CATEGORY = 'guide';
 // 标题配置ID(采购指南用id=6)
 const TITLE_ID = 6;
+// 推荐位编号
+const RECOMMEND_NO = 'decoration_guide';
 
 // 标题配置
 const headerConfig = ref({
@@ -275,32 +278,70 @@ const handleLinkClick = () => {
   }
 };
 
-// 采购指南展示列表(category = 'guide' 且 isShow = '1')
+// 推荐位
+const recommendId = ref<number | null>(null);
+const linkedProgramIds = ref<Set<string>>(new Set());
+
+// 采购指南展示列表
 const guideList = ref<any[]>([]);
 
+// 获取推荐位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 loadGuideList = async () => {
   try {
-    const res: any = await listProgram({ category: CATEGORY, isShow: '1', pageSize: 100 });
-    guideList.value = (res.rows || []).map((item: any) => ({
-      id: item.id,
-      programNo: item.programNo,
-      title: item.title,
-      subtitle: item.describe || '',
-      imageUrl: item.coverImageUrl || item.coverImage
-    }));
+    if (!recommendId.value) await loadRecommendId();
+    if (!recommendId.value) {
+      guideList.value = [];
+      linkedProgramIds.value = new Set();
+      return;
+    }
+    const linkRes: any = await listRecommendLink({ recommendId: recommendId.value, pageSize: 100 });
+    const links = linkRes.rows || [];
+    linkedProgramIds.value = new Set(links.map((l: any) => String(l.programId)));
+    if (links.length === 0) {
+      guideList.value = [];
+      return;
+    }
+    const programIds = links.map((link: any) => link.programId);
+    const programRes: any = await listProgram({ ids: programIds.join(','), category: CATEGORY, pageSize: 100 });
+    const programMap = new Map((programRes.rows || []).map((p: any) => [String(p.id), p]));
+    guideList.value = links.map((link: any) => {
+      const program: any = programMap.get(String(link.programId)) || {};
+      return {
+        id: program.id || link.programId,
+        linkId: link.id,
+        programNo: program.programNo || '',
+        title: program.title || `方案${link.programId}`,
+        subtitle: program.describe || '',
+        imageUrl: program.coverImageUrl || program.coverImage
+      };
+    });
   } catch (error) {
     console.error('加载采购指南列表失败', error);
     guideList.value = [];
   }
 };
 
-// 从展示区隐藏指南
+// 从展示区移除指南
 const handleHideGuide = async (item: any) => {
   try {
-    await updateProgram({ id: item.id, isShow: '0' });
-    await loadGuideList();
-    ElMessage.success('已移除');
+    if (item.linkId) {
+      await delRecommendLink(item.linkId);
+      linkedProgramIds.value.delete(String(item.id));
+      await loadGuideList();
+      ElMessage.success('已移除');
+    }
   } catch (error) {
     ElMessage.error('移除失败');
   }
@@ -343,8 +384,17 @@ const loadSelectList = async () => {
       programNo: item.programNo,
       title: item.title,
       imageUrl: item.coverImageUrl || item.coverImage,
-      isShow: item.isShow
+      isLinked: linkedProgramIds.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.programId), l.id]));
+      selectList.value.forEach((p: any) => {
+        p.linkId = linkMap.get(String(p.id)) || null;
+      });
+    }
     selectDialog.total = res.total || 0;
   } catch (error) {
     console.error('加载方案列表失败', error);
@@ -359,11 +409,16 @@ const resetSelectQuery = () => {
   loadSelectList();
 };
 
-// 推荐(设置 isShow = '1')
+// 推荐
 const handleRecommend = async (row: any) => {
   try {
-    await updateProgram({ id: row.id, isShow: '1' });
-    row.isShow = '1';
+    if (!recommendId.value) {
+      ElMessage.error('推荐位不存在');
+      return;
+    }
+    await addRecommendLink({ recommendId: recommendId.value, programId: row.id });
+    row.isLinked = true;
+    linkedProgramIds.value.add(String(row.id));
     await loadGuideList();
     ElMessage.success('已推荐');
   } catch (error) {
@@ -371,11 +426,14 @@ const handleRecommend = async (row: any) => {
   }
 };
 
-// 取消推荐(设置 isShow = '0')
+// 取消推荐
 const handleUnrecommend = async (row: any) => {
   try {
-    await updateProgram({ id: row.id, isShow: '0' });
-    row.isShow = '0';
+    if (row.linkId) {
+      await delRecommendLink(row.linkId);
+    }
+    row.isLinked = false;
+    linkedProgramIds.value.delete(String(row.id));
     await loadGuideList();
     ElMessage.success('已取消推荐');
   } catch (error) {
@@ -385,7 +443,7 @@ const handleUnrecommend = async (row: any) => {
 
 onMounted(() => {
   loadHeaderConfig();
-  loadGuideList();
+  loadRecommendId().then(() => loadGuideList());
 });
 
 // 配置商品相关

+ 19 - 78
src/views/product/recommend/index.vue

@@ -10,18 +10,6 @@
             <el-form-item label="推荐名称" prop="recommendName">
               <el-input v-model="queryParams.recommendName" placeholder="请输入推荐名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="推荐位描述" prop="recommendDescribe">
-              <el-input v-model="queryParams.recommendDescribe" 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="推荐封面图片路径或URL" prop="recommendCoverphoto">
-              <el-input v-model="queryParams.recommendCoverphoto" placeholder="请输入推荐封面图片路径或URL" 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>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
               <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -32,41 +20,19 @@
     </transition>
 
     <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:recommend:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:recommend:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:recommend:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:recommend:export']">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
-      </template>
+      <div style="display: flex; justify-content: flex-end; margin-bottom: 16px;">
+        <el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
+      </div>
 
-      <el-table v-loading="loading" border :data="recommendList" @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="recommendNo" />
-        <el-table-column label="推荐名称" align="center" prop="recommendName" />
-        <el-table-column label="推荐位描述" align="center" prop="recommendDescribe" />
-        <el-table-column label="是否显示:1=显示,0=隐藏" align="center" prop="isShow" />
-        <el-table-column label="推荐封面图片路径或URL" align="center" prop="recommendCoverphoto" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table v-loading="loading" border :data="recommendList">
+        <el-table-column label="推荐位ID" align="center" min-width="120" prop="id" />
+        <el-table-column label="推荐编号" align="center" min-width="150" prop="recommendNo" />
+        <el-table-column label="推荐名称" align="center" min-width="150" prop="recommendName" />
+        <el-table-column label="推荐位描述" align="center" min-width="200" prop="recommendDescribe" />
+        <el-table-column label="操作" align="center" width="150">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:recommend:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:recommend:remove']"></el-button>
-            </el-tooltip>
+            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">编辑</el-button>
+            <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -75,7 +41,7 @@
     </el-card>
     <!-- 添加或修改产品推荐位配置对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-      <el-form ref="recommendFormRef" :model="form" :rules="rules" label-width="80px">
+      <el-form ref="recommendFormRef" :model="form" :rules="rules" label-width="100px">
         <el-form-item label="推荐编号" prop="recommendNo">
           <el-input v-model="form.recommendNo" placeholder="请输入推荐编号" />
         </el-form-item>
@@ -83,16 +49,10 @@
           <el-input v-model="form.recommendName" placeholder="请输入推荐名称" />
         </el-form-item>
         <el-form-item label="推荐位描述" prop="recommendDescribe">
-          <el-input v-model="form.recommendDescribe" 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="recommendCoverphoto">
-          <el-input v-model="form.recommendCoverphoto" placeholder="请输入推荐封面图片路径或URL" />
+          <el-input v-model="form.recommendDescribe" type="textarea" :rows="3" placeholder="请输入推荐位描述" />
         </el-form-item>
         <el-form-item label="备注" prop="remark">
-            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -115,9 +75,6 @@ const recommendList = ref<RecommendVO[]>([]);
 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>();
@@ -193,13 +150,6 @@ const resetQuery = () => {
   handleQuery();
 }
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: RecommendVO[]) => {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-}
-
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
@@ -208,10 +158,9 @@ const handleAdd = () => {
 }
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: RecommendVO) => {
+const handleUpdate = async (row: RecommendVO) => {
   reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getRecommend(_id);
+  const res = await getRecommend(row.id);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.title = "修改产品推荐位配置";
@@ -235,21 +184,13 @@ const submitForm = () => {
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: RecommendVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除产品推荐位配置编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delRecommend(_ids);
+const handleDelete = async (row: RecommendVO) => {
+  await proxy?.$modal.confirm('是否确认删除产品推荐位配置编号为"' + row.id + '"的数据项?').finally(() => loading.value = false);
+  await delRecommend(row.id);
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
 }
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('product/recommend/export', {
-    ...queryParams.value
-  }, `recommend_${new Date().getTime()}.xlsx`)
-}
-
 onMounted(() => {
   getList();
 });

+ 2 - 1
vite.config.ts

@@ -24,7 +24,8 @@ export default defineConfig(({ mode, command }) => {
       open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://127.0.0.1:8080',
+          target: 'http://localhost:8080',
+          // http://192.168.1.52:8080
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')