Sfoglia il codice sorgente

Merge branch 'master' of http://8.152.4.3:3000/yp_web/yoe-opm-web

hurx 3 mesi fa
parent
commit
6981301553
73 ha cambiato i file con 11969 aggiunte e 31 eliminazioni
  1. 1 0
      package.json
  2. 26 0
      src/api/diy/index.ts
  3. 4 0
      src/api/pmsProduct/base/types.ts
  4. 10 0
      src/api/product/site/types.ts
  5. 66 1
      src/api/product/siteProduct/index.ts
  6. 195 0
      src/api/product/siteProduct/types.ts
  7. BIN
      src/assets/images/figure.png
  8. BIN
      src/assets/images/pcdiy/floor1.png
  9. BIN
      src/assets/images/pcdiy/floor2.png
  10. BIN
      src/assets/images/pcdiy/layout2.png
  11. BIN
      src/assets/images/pcdiy/layout4.png
  12. BIN
      src/assets/images/pcdiy/style10-1.png
  13. BIN
      src/assets/images/pcdiy/style10-2.png
  14. BIN
      src/assets/images/pcdiy/style11-1.png
  15. BIN
      src/assets/images/pcdiy/style11-2.png
  16. BIN
      src/assets/images/pcdiy/style11-3.png
  17. BIN
      src/assets/images/pcdiy/style13-1.png
  18. BIN
      src/assets/images/pcdiy/style13-2.png
  19. BIN
      src/assets/images/pcdiy/titlle1.png
  20. BIN
      src/assets/images/pcdiy/titlle10.png
  21. BIN
      src/assets/images/pcdiy/titlle11.png
  22. BIN
      src/assets/images/pcdiy/titlle12.png
  23. BIN
      src/assets/images/pcdiy/titlle13.png
  24. BIN
      src/assets/images/pcdiy/titlle14.png
  25. BIN
      src/assets/images/pcdiy/titlle15.png
  26. BIN
      src/assets/images/pcdiy/titlle2.png
  27. BIN
      src/assets/images/pcdiy/titlle3.png
  28. BIN
      src/assets/images/pcdiy/titlle4.png
  29. BIN
      src/assets/images/pcdiy/titlle5.png
  30. BIN
      src/assets/images/pcdiy/titlle6.png
  31. BIN
      src/assets/images/pcdiy/titlle7.png
  32. BIN
      src/assets/images/pcdiy/titlle8.png
  33. BIN
      src/assets/images/pcdiy/titlle9.png
  34. 71 0
      src/assets/styles/common.scss
  35. 5 1
      src/components/ImageUpload/index.vue
  36. 45 0
      src/components/ImagesForm/index.vue
  37. 1223 0
      src/components/LinkSelector/index.vue
  38. 2 0
      src/components/Pagination/index.vue
  39. 170 0
      src/components/WebLinkInput/index.vue
  40. 1 1
      src/components/upload-image/index.vue
  41. 6 0
      src/router/index.ts
  42. 558 0
      src/store/modules/pcdiy.ts
  43. 458 0
      src/views/diy/pcEdit.vue
  44. 226 0
      src/views/diy/pcEdit/advert-edit.vue
  45. 242 0
      src/views/diy/pcEdit/article-edit.vue
  46. 254 0
      src/views/diy/pcEdit/brand-edit.vue
  47. 187 0
      src/views/diy/pcEdit/carousel-edit.vue
  48. 631 0
      src/views/diy/pcEdit/discover-edit.vue
  49. 601 0
      src/views/diy/pcEdit/floor-edit.vue
  50. 536 0
      src/views/diy/pcEdit/goods-edit.vue
  51. 583 0
      src/views/diy/pcEdit/goodsList-edit.vue
  52. 640 0
      src/views/diy/pcEdit/head-edit.vue
  53. 168 0
      src/views/diy/pcEdit/imageCube-edit.vue
  54. 226 0
      src/views/diy/pcEdit/navigation-edit.vue
  55. 281 0
      src/views/diy/pcEdit/textTitle-edit.vue
  56. 209 0
      src/views/diy/pcList.vue
  57. 173 0
      src/views/diy/pcPages/advert.vue
  58. 183 0
      src/views/diy/pcPages/article.vue
  59. 168 0
      src/views/diy/pcPages/brand.vue
  60. 102 0
      src/views/diy/pcPages/carousel.vue
  61. 449 0
      src/views/diy/pcPages/discover.vue
  62. 252 0
      src/views/diy/pcPages/floor.vue
  63. 268 0
      src/views/diy/pcPages/goods.vue
  64. 284 0
      src/views/diy/pcPages/goodsList.vue
  65. 919 0
      src/views/diy/pcPages/head.vue
  66. 93 0
      src/views/diy/pcPages/imageCube.vue
  67. 151 0
      src/views/diy/pcPages/navigation.vue
  68. 457 0
      src/views/diy/pcPages/textTitle.vue
  69. 7 4
      src/views/platform/customerOperation/vipSite/index.vue
  70. 127 19
      src/views/platform/customerOperation/vipSite/productConfig.vue
  71. 10 1
      src/views/platform/customerOperation/vipSite/siteConfig.vue
  72. 693 0
      src/views/platform/customerOperation/vipSite/styleDesign.vue
  73. 8 4
      src/views/platform/industrial/brandFloor/index.vue

+ 1 - 0
package.json

@@ -46,6 +46,7 @@
     "vue-json-pretty": "2.4.0",
     "vue-router": "4.5.0",
     "vue-types": "6.0.0",
+    "vuedraggable": "^4.1.0",
     "vxe-table": "4.13.7"
   },
   "devDependencies": {

+ 26 - 0
src/api/diy/index.ts

@@ -44,3 +44,29 @@ export function editDiy(data: any) {
     data: data
   });
 }
+
+// pc自定义页面分页列表
+export function pcDiyList(query: any) {
+  return request({
+    url: '/mall/diyPcPage/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// pc新增自定义页面
+export function pcAddDiy(data: any) {
+  return request({
+    url: '/mall/diyPcPage',
+    method: 'post',
+    data: data
+  });
+}
+
+// pc删除自定义页面
+export function pcDelDiy(id: any) {
+  return request({
+    url: '/mall/diyPcPage/' + id,
+    method: 'delete'
+  });
+}

+ 4 - 0
src/api/pmsProduct/base/types.ts

@@ -729,6 +729,10 @@ export interface BaseQuery extends PageQuery {
    * 日期范围参数
    */
   params?: any;
+  /**
+   * 指定商品
+   */
+  ids?: any;
 }
 /**
  * 状态数量统计视图对象

+ 10 - 0
src/api/product/site/types.ts

@@ -98,6 +98,11 @@ export interface SiteVO {
    * 状态(0正常 1停用)
    */
   status: string;
+
+  /**
+   * 是否diy(0不diy 1diy)
+   */
+  isDiy: number;
 }
 
 export interface SiteForm extends BaseEntity {
@@ -200,6 +205,11 @@ export interface SiteForm extends BaseEntity {
    * 状态(0正常 1停用)
    */
   status?: string;
+
+  /**
+   * 是否diy(0不diy 1diy)
+   */
+  isDiy?: number;
 }
 
 export interface SiteQuery extends PageQuery {

+ 66 - 1
src/api/product/siteProduct/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { SiteProductVO, SiteProductForm, SiteProductQuery } from '@/api/product/siteProduct/types';
+import { SiteProductVO, SiteProductForm, SiteProductQuery, SiteProductPageQuery, PcProductVO, ClientSiteProductExportVO, ClientSiteProductImportVO } from '@/api/product/siteProduct/types';
 
 /**
  * 查询客户站点产品配置列表
@@ -61,3 +61,68 @@ export const delSiteProduct = (id: string | number | Array<string | number>) =>
     method: 'delete'
   });
 };
+/**
+ * 编辑站点产品的协议价
+ * @param data
+ */
+export const updateSiteProductAgreementPrice = (data: SiteProductForm) => {
+  return request({
+    url: '/product/siteProduct/updateAgreementPrice',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 获取站点下的产品数据(分页)
+ * @param query
+ * @returns {*}
+ */
+export const getSiteProductPage = (query?: SiteProductPageQuery & PageQuery) => {
+  return request({
+    url: '/product/siteProduct/getSiteProductPage',
+    method: 'get',
+    params: query
+  }) as AxiosPromise<{ rows: PcProductVO[]; total: number }>;
+};
+
+/**
+ * 导出客户站点产品数据
+ * @param query
+ */
+export const exportSiteProductData = (query?: SiteProductPageQuery) => {
+  return request({
+    url: '/product/siteProduct/exportData',
+    method: 'post',
+    data: query,
+    responseType: 'blob'
+  });
+};
+
+/**
+ * 导入客户站点产品配置数据
+ * @param file 导入文件
+ */
+export const importSiteProductData = (file: File) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: '/product/siteProduct/importData',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 获取导入模板
+ */
+export const getSiteProductImportTemplate = () => {
+  return request({
+    url: '/product/siteProduct/importTemplate',
+    method: 'post',
+    responseType: 'blob'
+  });
+};

+ 195 - 0
src/api/product/siteProduct/types.ts

@@ -213,3 +213,198 @@ export interface SiteProductQuery extends PageQuery {
    */
   params?: any;
 }
+
+/**
+ * 站点产品分页查询参数
+ */
+export interface SiteProductPageQuery {
+  /**
+   * 站点 ID
+   */
+  siteId?: string | number;
+}
+
+/**
+ * 产品基础信息 VO(来自 Elasticsearch)
+ */
+export interface PcProductVO {
+  /**
+   * 产品 ID
+   */
+  id: string | number;
+  
+  /**
+   * 产品编号
+   */
+  productNo?: string;
+  
+  /**
+   * 产品名称
+   */
+  itemName?: string;
+  
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+  
+  /**
+   * 品牌名称
+   */
+  brandName?: string;
+  
+  /**
+   * 单位
+   */
+  unitName?: string;
+  
+  /**
+   * 市场价
+   */
+  marketPrice?: number | string;
+  
+  /**
+   * 会员价
+   */
+  memberPrice?: number | string;
+  
+  /**
+   * 最低售价
+   */
+  minSellingPrice?: number | string;
+  
+  /**
+   * 采购价
+   */
+  purchasingPrice?: number | string;
+}
+
+/**
+ * 客户站点产品导出 VO
+ */
+export interface ClientSiteProductExportVO {
+  /**
+   * 序号
+   */
+  serialNumber?: number;
+  
+  /**
+   * 产品编号
+   */
+  productNo?: string;
+  
+  /**
+   * 产品名称
+   */
+  itemName?: string;
+  
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+  
+  /**
+   * 品牌名称
+   */
+  brandName?: string;
+  
+  /**
+   * 单位
+   */
+  unitName?: string;
+  
+  /**
+   * 市场价
+   */
+  marketPrice?: number | string;
+  
+  /**
+   * 平台价
+   */
+  platformPrice?: number | string;
+  
+  /**
+   * 最低售价
+   */
+  minSellingPrice?: number | string;
+  
+  /**
+   * 采购价
+   */
+  purchasingPrice?: number | string;
+  
+  /**
+   * 协议价
+   */
+  agreementPrice?: number | string;
+}
+
+/**
+ * 客户站点产品导入 VO
+ */
+export interface ClientSiteProductImportVO {
+  /**
+   * 站点 ID
+   */
+  siteId?: string | number;
+  
+  /**
+   * 客户编号
+   */
+  clientNo?: string;
+  
+  /**
+   * 客户 ID
+   */
+  clientId?: string | number;
+  
+  /**
+   * 产品编号
+   */
+  productNo?: string;
+  
+  /**
+   * 产品 ID
+   */
+  productId?: string | number;
+  
+  /**
+   * 是否在中心展示
+   */
+  centerView?: number;
+  
+  /**
+   * 展示方案 1 可见性
+   */
+  zsfa1View?: number;
+  
+  /**
+   * 展示方案 2 可见性
+   */
+  zsfa2View?: number;
+  
+  /**
+   * 展示方案 3 可见性
+   */
+  zsfa3View?: number;
+  
+  /**
+   * 展示方案 4 可见性
+   */
+  zsfa4View?: number;
+  
+  /**
+   * 协议价格
+   */
+  agreementPrice?: number | string;
+  
+  /**
+   * 状态
+   */
+  status?: string;
+  
+  /**
+   * 备注
+   */
+  remark?: string;
+}

BIN
src/assets/images/figure.png


BIN
src/assets/images/pcdiy/floor1.png


BIN
src/assets/images/pcdiy/floor2.png


BIN
src/assets/images/pcdiy/layout2.png


BIN
src/assets/images/pcdiy/layout4.png


BIN
src/assets/images/pcdiy/style10-1.png


BIN
src/assets/images/pcdiy/style10-2.png


BIN
src/assets/images/pcdiy/style11-1.png


BIN
src/assets/images/pcdiy/style11-2.png


BIN
src/assets/images/pcdiy/style11-3.png


BIN
src/assets/images/pcdiy/style13-1.png


BIN
src/assets/images/pcdiy/style13-2.png


BIN
src/assets/images/pcdiy/titlle1.png


BIN
src/assets/images/pcdiy/titlle10.png


BIN
src/assets/images/pcdiy/titlle11.png


BIN
src/assets/images/pcdiy/titlle12.png


BIN
src/assets/images/pcdiy/titlle13.png


BIN
src/assets/images/pcdiy/titlle14.png


BIN
src/assets/images/pcdiy/titlle15.png


BIN
src/assets/images/pcdiy/titlle2.png


BIN
src/assets/images/pcdiy/titlle3.png


BIN
src/assets/images/pcdiy/titlle4.png


BIN
src/assets/images/pcdiy/titlle5.png


BIN
src/assets/images/pcdiy/titlle6.png


BIN
src/assets/images/pcdiy/titlle7.png


BIN
src/assets/images/pcdiy/titlle8.png


BIN
src/assets/images/pcdiy/titlle9.png


+ 71 - 0
src/assets/styles/common.scss

@@ -1 +1,72 @@
 
+/* 布局flex */
+.flex-row-between {
+  display: -webkit-flex;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.flex-row-around {
+  display: -webkit-flex;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+}
+.flex-row-center {
+  display: -webkit-flex;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.flex-row-start {
+  display: -webkit-flex;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+}
+.flex-row-end {
+  display: -webkit-flex;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+.flex-column-between {
+  display: -webkit-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+.flex-column-center {
+  display: -webkit-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.flex-1 {
+  flex: 1;
+}
+.flex-wrap {
+  flex-wrap: wrap;
+}
+.flex-start {
+  align-items: flex-start;
+}
+.inline-block {
+  display: inline-block;
+}
+
+.ellipsis {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.ellipsis2 {
+ display: -webkit-box;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  /* 添加标准属性 */
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}

+ 5 - 1
src/components/ImageUpload/index.vue

@@ -99,9 +99,13 @@ watch(
       let list: OssVO[] = [];
       if (Array.isArray(val)) {
         list = val as OssVO[];
+      } else if (typeof val === 'string' && val.startsWith('http')) {
+        // 如果存的是 URL 直接回显,无需调接口
+        fileList.value = [{ name: val, url: val }];
+        return;
       } else {
         const res = await listByIds(val);
-        list = res.data;
+        list = res.data || [];
       }
       // 然后将数组转为对象数组
       fileList.value = list.map((item) => {

+ 45 - 0
src/components/ImagesForm/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <div>
+    <el-dialog v-model="dialogShow" title="缩放模式设置" width="600px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="缩放模式">
+          <el-radio-group v-model="formData.imgType">
+            <el-radio :value="1">拉伸</el-radio>
+            <el-radio :value="2">缩放</el-radio>
+            <el-radio :value="3">填充</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="close">取消</el-button>
+        <el-button type="primary" @click="confirm">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+const type = ref<any>(null);
+const dialogShow = ref<any>(false);
+const formData = ref<any>({});
+
+const onOpen = (res: any, res2: any) => {
+  type.value = res2;
+  formData.value = res;
+  dialogShow.value = true;
+};
+
+const emit = defineEmits(['confirmCallBack']);
+
+const confirm = () => {
+  emit('confirmCallBack', formData.value, type.value);
+  dialogShow.value = false;
+};
+const close = () => {
+  dialogShow.value = false;
+};
+
+defineExpose({
+  onOpen
+});
+</script>

+ 1223 - 0
src/components/LinkSelector/index.vue

@@ -0,0 +1,1223 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="选择链接" width="65%" @close="handleClose">
+    <div class="link-selector-container">
+      <!-- 左侧树状结构导航 -->
+      <div class="tree-container">
+        <el-tree ref="treeRef" :data="treeData" node-key="id" :props="defaultProps" @node-click="handleNodeClick" default-expand-all></el-tree>
+      </div>
+
+      <!-- 右侧链接内容 -->
+      <div class="links-container">
+        <div v-if="selectedCategory" class="category-title">{{ selectedCategory.title || selectedCategory.name }}</div>
+        <div v-else class="category-title">基础链接</div>
+
+        <!-- 链接区域 -->
+        <div class="link-list-container">
+          <!-- 商品链接选择区域 -->
+          <div v-if="isProductCategoryComputed" class="product-selection-container">
+            <!-- 搜索框 -->
+            <div class="product-search">
+              <el-input v-model="productSearchKeyword" placeholder="请输入商品名称关键词" clearable @keyup.enter="handleProductSearch" />
+              <el-button type="primary" @click="handleProductSearch">搜索</el-button>
+            </div>
+
+            <!-- 商品列表 -->
+            <div class="product-list">
+              <el-table
+                :data="productList"
+                :style="{ width: '100%' }"
+                :row-class-name="({ row }) => (selectedProduct?.id === row.id ? 'product-row-active' : '')"
+                @row-click="handleProductSelect"
+                class="custom-product-table"
+              >
+                <el-table-column width="80">
+                  <template #default="{ row }">
+                    <el-radio :model-value="selectedProduct?.id" :label="row.id" @change="() => handleProductSelect(row)" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="ID" prop="id" width="180" />
+                <el-table-column label="图片" width="120">
+                  <template #default="{ row }">
+                    <el-image
+                      :src="row.image || ''"
+                      style="width: 80px; height: 60px; object-fit: cover; display: block"
+                      :preview-src-list="row.image ? [row.image] : []"
+                    >
+                      <template #error>
+                        <div class="image-slot">
+                          <el-icon><Picture /></el-icon>
+                        </div>
+                      </template>
+                    </el-image>
+                  </template>
+                </el-table-column>
+                <el-table-column prop="name" label="商品名称">
+                  <template #default="{ row }">
+                    <div class="product-name">{{ row.name }}</div>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </div>
+
+          <!-- 文章链接选择区域 -->
+          <div v-else-if="isArticleCategoryComputed" class="article-selection-container">
+            <!-- 搜索框 -->
+            <div class="product-search">
+              <el-input v-model="articleSearchKeyword" placeholder="请输入文章标题关键词" clearable @keyup.enter="handleArticleSearch" />
+              <el-button type="primary" @click="handleArticleSearch">搜索</el-button>
+            </div>
+
+            <!-- 文章列表 -->
+            <div class="article-list">
+              <el-table
+                :data="articleList"
+                :style="{ width: '100%' }"
+                :row-class-name="({ row }) => (selectedArticle?.id === row.id ? 'article-row-active' : '')"
+                @row-click="handleArticleSelect"
+                class="custom-article-table"
+              >
+                <el-table-column width="80">
+                  <template #default="{ row }">
+                    <el-radio :model-value="selectedArticle?.id" :label="row.id" @change="() => handleArticleSelect(row)" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="ID" prop="id" width="180" />
+                <el-table-column prop="title" label="文章标题">
+                  <template #default="{ row }">
+                    <div class="article-title">{{ row.title }}</div>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </div>
+
+          <!-- 微页面链接选择区域 -->
+          <div v-else-if="isMicropageCategoryComputed" class="micropage-selection-container">
+            <!-- 搜索框 -->
+            <div class="product-search">
+              <el-input v-model="micropageSearchKeyword" placeholder="请输入微页面名称关键词" clearable @keyup.enter="handleMicropageSearch" />
+              <el-button type="primary" @click="handleMicropageSearch">搜索</el-button>
+            </div>
+
+            <!-- 微页面列表 -->
+            <div class="micropage-list">
+              <el-table
+                :data="micropageList"
+                :style="{ width: '100%' }"
+                :row-class-name="({ row }) => (selectedMicropage?.id === row.id ? 'micropage-row-active' : '')"
+                @row-click="handleMicropageSelect"
+                class="custom-micropage-table"
+              >
+                <el-table-column width="80">
+                  <template #default="{ row }">
+                    <el-radio :model-value="selectedMicropage?.id" :label="row.id" @change="() => handleMicropageSelect(row)" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="ID" prop="id" width="180" />
+                <el-table-column prop="name" label="标题名称名称">
+                  <template #default="{ row }">
+                    <div class="micropage-name">{{ row.name }}</div>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </div>
+
+          <!-- 自定义链接输入区域 -->
+          <div v-else-if="isCustomLinkCategory" class="custom-link-container">
+            <el-form label-position="top">
+              <el-form-item label="跳转路径">
+                <el-input
+                  v-model="customLinkPath"
+                  placeholder="请输入跳转路径"
+                  clearable
+                  @input="() => emit('update:modelValue', customLinkPath.trim())"
+                >
+                  <template #prefix>
+                    <el-icon><Aim /></el-icon>
+                  </template>
+                </el-input>
+                <div class="custom-link-tips">
+                  <el-icon size="14"><Warning /></el-icon>
+                  <span>请确保链接格式正确,支持相对路径和绝对路径</span>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <!-- 预设链接展示区域 -->
+          <div v-else class="links-grid">
+            <el-button
+              v-for="link in currentLinks"
+              :key="link.id"
+              size="small"
+              :class="['link-button', { active: selectedLink && selectedLink.id === link.id }]"
+              :type="selectedLink && selectedLink.id === link.id ? 'primary' : 'default'"
+              :title="link.url"
+              @click="handleLinkClick(link)"
+            >
+              {{ link.name }}
+            </el-button>
+          </div>
+
+          <!-- 链接预览 -->
+          <div v-if="selectedLink || (isCustomLinkCategory && customLinkPath)" class="link-preview">
+            <span class="preview-label">当前选择:</span>
+            <span v-if="selectedLink" class="preview-name">{{ selectedLink.name }}</span>
+            <span v-else class="preview-name">自定义链接</span>
+            <span class="preview-path">{{ selectedLink ? selectedLink.url : customLinkPath }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <template #footer>
+      <el-button @click="handleCancel">取消</el-button>
+      <el-button type="primary" @click="handleConfirm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
+import type { TreeNode } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import { Aim, Warning, Picture } from '@element-plus/icons-vue';
+import { getCategoryTree } from '@/api/mall/pageCategory/api';
+import { listPageLink } from '@/api/mall/pageLink/api';
+import { pcDiyList } from '@/api/diy/index';
+import { listBase } from '@/api/pmsProduct/base';
+import { listServiceCase } from '@/api/product/serviceCase';
+
+interface LinkItem {
+  id: string;
+  name: string;
+  url: string;
+}
+
+// 商品数据接口
+interface ProductItem {
+  id: number;
+  name: string;
+  image: string;
+}
+
+// 文章数据接口
+interface ArticleItem {
+  id: number;
+  title: string;
+}
+
+// 微页面数据接口
+interface MicropageItem {
+  id: number;
+  name: string;
+}
+
+// Props
+interface Props {
+  visible: boolean;
+  modelValue?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  visible: false,
+  modelValue: ''
+});
+
+// Emits
+interface Emits {
+  'update:visible': [value: boolean];
+  'update:modelValue': [value: string];
+  'confirm': [value: string, link: LinkItem];
+  'cancel': [];
+  'close': [];
+  'reset': [];
+}
+
+const emit = defineEmits<Emits>();
+
+// 响应式数据
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => emit('update:visible', value)
+});
+
+const treeRef = ref<InstanceType<(typeof import('element-plus'))['ElTree']>>();
+const selectedCategory = ref<any>();
+const selectedLink = ref<LinkItem>();
+const customLinkPath = ref('');
+const selectedProduct = ref<ProductItem>();
+const productList = ref<ProductItem[]>([]);
+const productSearchKeyword = ref('');
+const isProductCategory = ref(false);
+
+// 文章相关响应式数据
+const selectedArticle = ref<ArticleItem>();
+const articleList = ref<ArticleItem[]>([]);
+const articleSearchKeyword = ref('');
+const isArticleCategory = ref(false);
+
+// 微页面相关响应式数据
+// 获取DIY上下文
+const selectedMicropage = ref<MicropageItem>();
+const micropageList = ref<any[]>([]);
+const micropageSearchKeyword = ref('');
+const isMicropageCategory = ref(false);
+
+// 动态树数据
+const treeData = ref<any[]>([]);
+// 分类对应的链接数据
+const categoryLinksMap = ref<Map<string | number, LinkItem[]>>(new Map());
+
+// 加载分类数据
+const loadCategoryData = async () => {
+  try {
+    const res = await getCategoryTree();
+    if (res.data) {
+      // 只保留非顶级的分类
+      const filteredItems = res.data.find((item) => item.id === 0).children || [];
+      // 剩余的节点转换为树结构
+      const convertToTreeFormat = (items: any[]) => {
+        return items.map((item) => ({
+          id: item.id,
+          title: item.name,
+          originalData: item,
+          children: item.children ? convertToTreeFormat(item.children) : []
+        }));
+      };
+      treeData.value = convertToTreeFormat(filteredItems);
+      // 获取所有分类的 ID 列表
+      const allCategoryIds = getAllCategoryIds(treeData.value);
+
+      // 并行请求所有分类的链接数据
+      const linkPromises = allCategoryIds.map((id) => loadLinksForCategory(id));
+      const allLinks = await Promise.all(linkPromises);
+
+      // 将结果合并到 categoryLinksMap
+      allLinks.forEach((links, index) => {
+        const categoryId = allCategoryIds[index];
+        if (links && links.length > 0) {
+          categoryLinksMap.value.set(categoryId, links);
+        }
+      });
+      return true;
+    }
+    return false;
+  } catch (error) {
+    console.error('获取分类树失败:', error);
+    ElMessage.error('获取分类树失败');
+    return false;
+  }
+};
+
+// 递归获取所有分类 ID
+const getAllCategoryIds = (nodes: any[]): number[] => {
+  const ids: number[] = [];
+  nodes.forEach((node) => {
+    ids.push(node.id);
+    if (node.children && node.children.length > 0) {
+      ids.push(...getAllCategoryIds(node.children));
+    }
+  });
+  return ids;
+};
+
+// 加载指定分类的链接数据
+const loadLinksForCategory = async (categoryId: number): Promise<LinkItem[]> => {
+  try {
+    const res = await listPageLink({
+      cateId: categoryId,
+      pageNum: 1,
+      pageSize: 100,
+      createTime: undefined
+    });
+    if (res.rows) {
+      return res.rows.map((item: any) => ({
+        id: item.linkKey,
+        name: item.name,
+        url: item.url
+      }));
+    }
+    return [];
+  } catch (error) {
+    console.error(`获取分类 ${categoryId} 链接失败:`, error);
+    return [];
+  }
+};
+
+// 默认树配置
+const defaultProps = {
+  children: 'children',
+  label: 'title'
+};
+
+// 当前显示的链接列表
+const currentLinks = computed(() => {
+  if (!selectedCategory.value || !selectedCategory.value.id) {
+    return [];
+  }
+
+  // 从缓存中获取链接数据
+  return categoryLinksMap.value.get(selectedCategory.value.id) || [];
+});
+
+// 是否为自定义链接分类
+const isCustomLinkCategory = computed(() => {
+  return selectedCategory.value?.originalData?.type === '4' || selectedCategory.value?.name === '自定义链接';
+});
+
+// 是否为商品分类
+const isProductCategoryComputed = computed(() => {
+  return selectedCategory.value?.name === '商品' || selectedCategory.value?.title === '商品';
+});
+
+// 是否为文章分类
+const isArticleCategoryComputed = computed(() => {
+  return selectedCategory.value?.name === '文章' || selectedCategory.value?.title === '文章';
+});
+
+// 是否为微页面分类
+const isMicropageCategoryComputed = computed(() => {
+  return selectedCategory.value?.name === '微页面' || selectedCategory.value?.title === '微页面';
+});
+
+// 处理节点点击
+async function handleNodeClick(node: TreeNode) {
+  const treeNode = node as any;
+  if (treeNode.id) {
+    selectedCategory.value = treeNode;
+    selectedLink.value = undefined;
+    selectedProduct.value = undefined;
+    selectedArticle.value = undefined;
+    selectedMicropage.value = undefined;
+
+    // 设置当前分类类型
+    isProductCategory.value = isProductCategoryComputed.value;
+    isArticleCategory.value = isArticleCategoryComputed.value;
+    isMicropageCategory.value = isMicropageCategoryComputed.value;
+
+    if (isProductCategory.value) {
+      // 如果是商品分类,清空搜索关键词并加载商品列表
+      productSearchKeyword.value = '';
+      await loadProductList();
+      emit('update:modelValue', '');
+    } else if (isArticleCategory.value) {
+      // 如果是文章分类,清空搜索关键词并加载文章列表
+      articleSearchKeyword.value = '';
+      await loadArticleList();
+      emit('update:modelValue', '');
+    } else if (isMicropageCategory.value) {
+      // 如果是微页面分类,清空搜索关键词并加载微页面列表
+      micropageSearchKeyword.value = '';
+      await loadMicropageList();
+      emit('update:modelValue', '');
+    } else if (isCustomLinkCategory.value) {
+      // 如果是自定义链接分类,清空选择
+      customLinkPath.value = '';
+      emit('update:modelValue', '');
+    } else {
+      // 获取当前分类的链接
+      const links = currentLinks.value;
+      if (links.length > 0) {
+        // 自动选择第一个链接
+        selectedLink.value = links[0];
+        emit('update:modelValue', links[0].url);
+      } else {
+        emit('update:modelValue', '');
+      }
+    }
+  }
+}
+
+// 加载商品列表
+async function loadProductList(keyword: string = '') {
+  try {
+    // 这里模拟获取商品数据,实际项目中应该调用API
+    const queryParams: any = {
+      pageNum: 1,
+      pageSize: 100,
+      itemName: keyword
+    };
+    const res = await listBase(queryParams);
+    if (res.rows) {
+      productList.value = res.rows.map((item: any) => ({
+        id: Number(item.id),
+        name: item.itemName,
+        image: item.productImage
+      }));
+    }
+  } catch (error) {
+    console.error('加载商品列表失败:', error);
+    ElMessage.error('加载商品列表失败');
+    productList.value = [];
+  }
+}
+
+// 搜索商品
+async function handleProductSearch() {
+  await loadProductList(productSearchKeyword.value);
+}
+
+// 选择商品
+function handleProductSelect(product: ProductItem) {
+  selectedProduct.value = product;
+  // 构建商品详情链接:pages/goods/detail/detail?id=商品Id
+  const productUrl = `/pages/goods/detail/detail?id=${product.id}`;
+  emit('update:modelValue', productUrl);
+}
+
+// 加载文章列表
+async function loadArticleList(keyword: string = '') {
+  try {
+    const queryParams = {
+      pageNum: 1,
+      pageSize: 100,
+      caseTitle: keyword
+    };
+
+    const res = await listServiceCase(queryParams);
+    if (res.data) {
+      articleList.value = res.data.map((item: any) => ({
+        id: item.id,
+        title: item.caseTitle
+      }));
+    }
+  } catch (error) {
+    console.error('加载文章列表失败:', error);
+    ElMessage.error('加载文章列表失败');
+    articleList.value = [];
+  }
+}
+
+// 搜索文章
+async function handleArticleSearch() {
+  await loadArticleList(articleSearchKeyword.value);
+}
+
+// 选择文章
+function handleArticleSelect(article: ArticleItem) {
+  selectedArticle.value = article;
+  // 构建文章详情链接:pages/article/detail/detail?id=文章Id
+  const articleUrl = `/pages/article/detail/detail?id=${article.id}`;
+  emit('update:modelValue', articleUrl);
+}
+
+// 加载微页面列表
+async function loadMicropageList(keyword: string = '') {
+  try {
+    // 这里模拟获取微页面数据,实际项目中应该调用API
+    // 假设的查询参数结构
+    const queryParams = {
+      pageNum: 1,
+      pageSize: 100,
+      title: keyword
+    };
+    let res = undefined;
+    res = await pcDiyList(queryParams);
+    if (res.rows) {
+      micropageList.value = res.list.map((item: any) => ({
+        id: item.id,
+        name: item.name
+      }));
+    }
+  } catch (error) {
+    console.error('加载微页面列表失败:', error);
+    ElMessage.error('加载微页面列表失败');
+    micropageList.value = [];
+  }
+}
+
+// 搜索微页面
+async function handleMicropageSearch() {
+  await loadMicropageList(micropageSearchKeyword.value);
+}
+
+// 选择微页面
+function handleMicropageSelect(micropage: MicropageItem) {
+  selectedMicropage.value = micropage;
+  // 构建微页面链接:pages/micropage/detail/detail?id=微页面Id
+  const micropageUrl = `/pages/diy/index?id=${micropage.id}`;
+  emit('update:modelValue', micropageUrl);
+}
+
+// 处理链接点击
+function handleLinkClick(link: LinkItem) {
+  selectedLink.value = link;
+  // 触发临时选择事件,让父组件可以实时获取选择的链接
+  emit('update:modelValue', link.url);
+}
+
+// 处理确定
+function handleConfirm() {
+  if (isProductCategory.value) {
+    if (!selectedProduct.value) {
+      ElMessage({
+        message: '请选择商品',
+        type: 'warning',
+        duration: 2000
+      });
+      return;
+    }
+    dialogVisible.value = false;
+    ElMessage({
+      message: '商品设置成功',
+      type: 'success',
+      duration: 2000
+    });
+  } else if (isArticleCategory.value) {
+    if (!selectedArticle.value) {
+      ElMessage({
+        message: '请选择文章',
+        type: 'warning',
+        duration: 2000
+      });
+      return;
+    }
+    dialogVisible.value = false;
+    ElMessage({
+      message: '文章设置成功',
+      type: 'success',
+      duration: 2000
+    });
+  } else if (isMicropageCategory.value) {
+    if (!selectedMicropage.value) {
+      ElMessage({
+        message: '请选择微页面',
+        type: 'warning',
+        duration: 2000
+      });
+      return;
+    }
+    dialogVisible.value = false;
+    ElMessage({
+      message: '微页面设置成功',
+      type: 'success',
+      duration: 2000
+    });
+  } else if (isCustomLinkCategory.value) {
+    // 自定义链接处理
+    if (!customLinkPath.value.trim()) {
+      ElMessage({
+        message: '请输入自定义链接地址',
+        type: 'warning',
+        duration: 2000
+      });
+      return;
+    }
+
+    // 验证链接格式
+    const path = customLinkPath.value.trim();
+    // 创建临时的自定义链接项
+    const customLink: LinkItem = {
+      id: `custom-${Date.now()}`,
+      name: '自定义链接',
+      url: path
+    };
+
+    emit('update:modelValue', path);
+    emit('confirm', path, customLink);
+    dialogVisible.value = false;
+    ElMessage({
+      message: '自定义链接设置成功',
+      type: 'success',
+      duration: 2000
+    });
+  } else if (selectedLink.value) {
+    // 普通链接处理
+    emit('update:modelValue', selectedLink.value.url);
+    emit('confirm', selectedLink.value.url, selectedLink.value);
+    dialogVisible.value = false;
+    // 确认选择后,显示成功提示
+    ElMessage({
+      message: '链接选择成功',
+      type: 'success',
+      duration: 2000
+    });
+  } else {
+    // 如果没有选择链接,提示用户
+    ElMessage({
+      message: '请选择一个链接',
+      type: 'warning',
+      duration: 2000
+    });
+  }
+}
+
+// 处理取消
+function handleCancel() {
+  emit('cancel');
+  dialogVisible.value = false;
+}
+
+// 打开对话框的方法,用于外部调用
+const open = () => {
+  dialogVisible.value = true;
+  // 如果数据还没加载完成,则重新加载
+  if (treeData.value.length === 0) {
+    loadCategoryData();
+  }
+};
+
+// 处理关闭
+function handleClose() {
+  emit('close');
+  // 对话框关闭时重置所有状态,不更新modelValue
+  selectedCategory.value = undefined;
+  selectedLink.value = undefined;
+  selectedProduct.value = undefined;
+  selectedArticle.value = undefined;
+  selectedMicropage.value = undefined;
+  if (treeRef.value) {
+    treeRef.value.setCurrentKey(null);
+  }
+}
+
+// 重置状态
+function resetState() {
+  selectedCategory.value = undefined;
+  selectedLink.value = undefined;
+  customLinkPath.value = '';
+  selectedProduct.value = undefined;
+  selectedArticle.value = undefined;
+  selectedMicropage.value = undefined;
+  productSearchKeyword.value = '';
+  articleSearchKeyword.value = '';
+  micropageSearchKeyword.value = '';
+
+  // 重置树的当前节点
+  if (treeRef.value) {
+    treeRef.value.setCurrentKey(null);
+  }
+}
+
+// 监听dialogVisible变化,当对话框打开时初始化状态
+watch(
+  () => dialogVisible.value,
+  async (newValue) => {
+    if (newValue) {
+      nextTick(async () => {
+        // 重置自定义链接输入
+        customLinkPath.value = '';
+        // 清空缓存
+        categoryLinksMap.value.clear();
+
+        // 加载分类数据
+        await loadCategoryData();
+
+        if (props.modelValue) {
+          // 如果有传入的modelValue,则尝试匹配
+        }
+
+        // 如果没有匹配的链接或没有传入modelValue,则设置默认选中
+        if (!selectedCategory.value && treeData.value.length > 0) {
+          // 找到第一个有子分类的节点
+          const findFirstNodeWithLinks = (nodes: any[]) => {
+            for (const node of nodes) {
+              if (node.children && node.children.length > 0) {
+                return node.children[0];
+              }
+            }
+            return null;
+          };
+
+          const firstNode = findFirstNodeWithLinks(treeData.value);
+          if (firstNode) {
+            selectedCategory.value = firstNode;
+            if (treeRef.value && firstNode.id) {
+              treeRef.value.setCurrentKey(firstNode.id);
+              // 触发节点点击,加载链接数据
+              await handleNodeClick(firstNode);
+            }
+          }
+        }
+      });
+    }
+  }
+);
+
+// 键盘导航处理
+function handleKeyDown(event: KeyboardEvent) {
+  // 只有当对话框可见时才处理键盘事件
+  if (!dialogVisible.value || !selectedLink.value) return;
+
+  const links = currentLinks.value;
+  if (links.length === 0) return;
+
+  const currentIndex = links.findIndex((link) => link.id === selectedLink.value?.id);
+
+  switch (event.key) {
+    case 'ArrowRight':
+      // 向右箭头:选择下一个链接
+      event.preventDefault();
+      if (currentIndex < links.length - 1) {
+        handleLinkClick(links[currentIndex + 1]);
+      } else if (links.length > 0) {
+        handleLinkClick(links[0]);
+      }
+      break;
+    case 'ArrowLeft':
+      // 向左箭头:选择上一个链接
+      event.preventDefault();
+      if (currentIndex > 0) {
+        handleLinkClick(links[currentIndex - 1]);
+      } else if (links.length > 0) {
+        handleLinkClick(links[links.length - 1]);
+      }
+      break;
+    case 'Enter':
+      // 回车键:确认选择
+      event.preventDefault();
+      handleConfirm();
+      break;
+    case 'Escape':
+      // ESC键:取消
+      event.preventDefault();
+      handleCancel();
+      break;
+  }
+}
+
+// 重置方法
+defineExpose({
+  reset: () => {
+    resetState();
+    emit('reset');
+    emit('update:modelValue', '');
+  },
+  open,
+  resetState
+});
+
+// 组件挂载时添加键盘事件监听
+onMounted(() => {
+  document.addEventListener('keydown', handleKeyDown);
+});
+
+// 组件卸载时移除键盘事件监听
+onUnmounted(() => {
+  document.removeEventListener('keydown', handleKeyDown);
+});
+</script>
+
+<style scoped>
+/* 主容器样式 */
+.link-selector-container {
+  display: flex;
+  height: 800px;
+  gap: 20px;
+  animation: fadeIn 0.3s ease-in-out;
+}
+
+/* 商品链接选择区域样式 */
+.product-selection-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.product-search {
+  margin-bottom: 16px;
+}
+
+.product-search .el-input {
+  width: 30%;
+  margin-right: 8px;
+}
+
+.product-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+/* Element UI表格样式调整 */
+.product-list .el-table {
+  --el-table-header-bg-color: #1890ff;
+  --el-table-header-text-color: #ffffff;
+  --el-table-border-color: #e4e7ed;
+}
+
+.product-list .el-table :deep(.el-table__header th) {
+  font-weight: 500;
+  text-align: left;
+  padding: 12px;
+}
+
+.product-list .el-table :deep(.el-table__body td) {
+  padding: 12px;
+}
+
+.product-list .el-table :deep(.el-table__row:hover) {
+  background-color: #f5f7fa;
+}
+
+/* 选中行样式 */
+.product-row-active {
+  background-color: #ecf5ff !important;
+}
+
+/* 商品名称样式 */
+.product-name {
+  font-size: 14px;
+  line-height: 20px;
+  word-break: break-word;
+  max-width: 300px;
+}
+
+/* 左侧树容器 */
+.tree-container {
+  width: 200px;
+  border-right: 1px solid #e4e7ed;
+  overflow-y: auto;
+  background-color: #fafafa;
+  transition: all 0.3s ease;
+}
+
+/* 右侧链接容器 */
+.links-container {
+  flex: 1;
+  overflow-y: auto;
+  padding: 15px;
+  background-color: #ffffff;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: column;
+  transition: all 0.3s ease;
+}
+
+/* 分类标题 */
+.category-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 20px;
+  color: #303133;
+  padding-bottom: 8px;
+  border-bottom: 2px solid #409eff;
+  animation: slideInLeft 0.3s ease-out;
+}
+
+/* 链接网格 */
+.links-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-bottom: 20px;
+}
+
+/* 自定义链接容器 */
+.custom-link-container {
+  animation: fadeInUp 0.3s ease-out;
+}
+
+/* 自定义链接提示 */
+.custom-link-tips {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: flex-start;
+  gap: 4px;
+  word-break: break-all;
+  flex-wrap: wrap;
+  width: 100%;
+}
+
+.custom-link-tips .el-icon {
+  color: #e6a23c;
+}
+
+/* 链接按钮 */
+.link-button {
+  margin-bottom: 8px;
+  transition: all 0.3s ease;
+  min-width: 120px;
+  animation: fadeInUp 0.3s ease-out;
+}
+
+.link-button:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.link-button.active {
+  background-color: #409eff;
+  color: white;
+  animation: pulse 0.5s ease-in-out;
+}
+
+/* 链接预览区域 */
+.link-preview {
+  margin-top: auto;
+  padding: 12px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  border-left: 4px solid #409eff;
+  font-size: 14px;
+  animation: slideInUp 0.4s ease-out;
+  transition: all 0.3s ease;
+}
+
+.link-preview:hover {
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.preview-label {
+  color: #606266;
+  font-weight: 500;
+}
+
+.preview-name {
+  color: #409eff;
+  font-weight: bold;
+  margin: 0 8px;
+}
+
+.preview-path {
+  color: #909399;
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+  word-break: break-all;
+}
+
+/* 滚动条样式 */
+.tree-container::-webkit-scrollbar,
+.links-container::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+.tree-container::-webkit-scrollbar-thumb,
+.links-container::-webkit-scrollbar-thumb {
+  background-color: #dcdfe6;
+  border-radius: 3px;
+  transition: background-color 0.3s;
+}
+
+.tree-container::-webkit-scrollbar-thumb:hover,
+.links-container::-webkit-scrollbar-thumb:hover {
+  background-color: #c0c4cc;
+}
+
+.tree-container::-webkit-scrollbar-track,
+.links-container::-webkit-scrollbar-track {
+  background-color: #f5f7fa;
+}
+
+/* Element Plus 树样式覆盖 */
+:deep(.el-tree-node) {
+  padding: 2px 0;
+  transition: all 0.2s ease;
+}
+
+:deep(.el-tree-node__content) {
+  transition: all 0.3s ease;
+  padding: 8px 12px;
+  margin: 0;
+}
+
+:deep(.el-tree-node__content:hover) {
+  background-color: #ecf5ff;
+  transform: translateX(2px);
+}
+
+:deep(.el-tree-node.is-current > .el-tree-node__content) {
+  background-color: #ecf5ff;
+  color: #409eff;
+  font-weight: 500;
+  border-right: 3px solid #409eff;
+}
+
+:deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) {
+  color: #409eff;
+}
+
+/* 自定义表格样式 */
+:deep(.custom-product-table .el-table__header th) {
+  background-color: #188fff54 !important;
+}
+
+/* 移除单选框后面的点 */
+:deep(.el-radio__label) {
+  display: none;
+}
+
+/* 确保单选框样式正确 */
+:deep(.el-radio) {
+  margin-right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 图片加载失败时的样式 */
+:deep(.image-slot) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 80px;
+  height: 60px;
+  background-color: #f5f7fa;
+  color: #909399;
+}
+
+/* 动画定义 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes slideInLeft {
+  from {
+    opacity: 0;
+    transform: translateX(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.05);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .link-selector-container {
+    height: 350px;
+  }
+
+  .tree-container {
+    width: 180px;
+  }
+}
+
+@media (max-width: 768px) {
+  .link-selector-container {
+    height: 300px;
+    gap: 10px;
+  }
+
+  .tree-container {
+    width: 160px;
+  }
+
+  .links-container {
+    padding: 10px;
+  }
+
+  .link-button {
+    min-width: 100px;
+    padding: 8px 12px;
+    font-size: 13px;
+  }
+
+  .category-title {
+    font-size: 14px;
+    margin-bottom: 15px;
+  }
+
+  .link-preview {
+    font-size: 12px;
+    padding: 8px;
+  }
+}
+
+@media (max-width: 480px) {
+  .link-selector-container {
+    flex-direction: column;
+    height: 450px;
+  }
+
+  .tree-container {
+    width: 100%;
+    height: 120px;
+    border-right: none;
+    border-bottom: 1px solid #e4e7ed;
+  }
+
+  .links-container {
+    flex: 1;
+  }
+
+  .links-grid {
+    gap: 8px;
+  }
+
+  .link-button {
+    min-width: calc(50% - 4px);
+    margin-bottom: 0;
+  }
+}
+
+/* 加载动画 */
+:deep(.el-loading-spinner) {
+  margin-top: -20px;
+}
+
+/* 确保对话框在小屏幕上也能良好显示 */
+:deep(.el-dialog__wrapper) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+:deep(.el-dialog) {
+  margin: 0 !important;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+}
+
+:deep(.el-dialog__body) {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
+}
+
+/* 当选择链接时的反馈 */
+:deep(.el-button--primary.is-active) {
+  background-color: #337ecc;
+  border-color: #337ecc;
+}
+
+/* 确保选中行样式正确 */
+.product-row-active {
+  background-color: #e6f7ff !important;
+}
+</style>

+ 2 - 0
src/components/Pagination/index.vue

@@ -1,5 +1,6 @@
 <template>
   <div :class="{ hidden: hidden }" class="pagination-container">
+    <slot name="slotDiv"></slot>
     <!-- 游标分页模式 -->
     <div v-if="cursorMode" class="cursor-pagination">
       <span class="pagination-info">每页 {{ pageSize }} 条</span>
@@ -108,6 +109,7 @@ function handleNextPage() {
 
 <style lang="scss" scoped>
 .pagination-container {
+  position: relative;
   .el-pagination {
     float: v-bind(float);
   }

+ 170 - 0
src/components/WebLinkInput/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="web-link-input-wrapper">
+    <el-input
+      v-model="localValue"
+      :placeholder="placeholder"
+      :disabled="disabled"
+      clearable
+      :style="inputStyle"
+      @change="handleInputChange"
+      @keydown="handleKeydown"
+      @focus="
+        (e: FocusEvent) => {
+          if (e.target) (e.target as HTMLInputElement).select();
+        }
+      "
+    >
+      <template #append>
+        <el-button type="primary" size="small" :disabled="disabled" @click="handleSelectLink"> 选择 </el-button>
+      </template>
+    </el-input>
+    <LinkSelector
+      ref="linkSelectorRef"
+      v-model:visible="selectorVisible"
+      v-model="localValue"
+      @confirm="handleLinkConfirm"
+      @close="handleSelectorClose"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import LinkSelector from '@/components/LinkSelector/index.vue';
+
+// 定义属性
+interface Props {
+  modelValue?: string;
+  placeholder?: string;
+  disabled?: boolean;
+  inputStyle?: Record<string, any>;
+}
+
+// 定义事件
+interface Emits {
+  (e: 'update:modelValue', value: string): void;
+  (e: 'change', value: string): void;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: '',
+  placeholder: '请输入链接或点击选择',
+  disabled: false,
+  inputStyle: () => ({})
+});
+
+const emit = defineEmits<Emits>();
+
+// 响应式数据
+const linkSelectorRef = ref();
+const selectorVisible = ref(false);
+const localValue = ref(props.modelValue);
+
+// 监听props变化
+watch(
+  () => props.modelValue,
+  (newValue) => {
+    if (newValue !== localValue.value) {
+      localValue.value = newValue;
+    }
+  },
+  { immediate: true }
+);
+
+// 监听localValue变化,实现双向绑定
+watch(localValue, (newValue) => {
+  if (newValue !== props.modelValue) {
+    emit('update:modelValue', newValue);
+  }
+});
+
+// 处理输入框变化
+const handleInputChange = () => {
+  emit('change', localValue.value);
+};
+
+// 打开链接选择器
+const handleSelectLink = () => {
+  // 优先使用组件暴露的open方法
+  if (linkSelectorRef.value && linkSelectorRef.value.open) {
+    linkSelectorRef.value.open();
+  } else {
+    // 兼容方案
+    selectorVisible.value = true;
+  }
+};
+
+// 处理键盘事件
+const handleKeydown = (event: KeyboardEvent) => {
+  // 当输入框获取焦点时,按回车键可以打开选择器
+  if (event.code === 'Enter' && !props.disabled) {
+    handleSelectLink();
+    event.preventDefault();
+  }
+};
+
+// 处理链接选择确认
+const handleLinkConfirm = (value: string, linkItem?: any) => {
+  localValue.value = value;
+  emit('update:modelValue', value);
+  emit('change', value);
+  selectorVisible.value = false;
+};
+
+// 处理选择器关闭
+const handleSelectorClose = () => {
+  selectorVisible.value = false;
+};
+
+// 暴露方法给父组件
+defineExpose({
+  openSelector: () => {
+    handleSelectLink();
+  },
+  focusInput: () => {
+    // 可以在后续实现输入框聚焦功能
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.web-link-input-wrapper {
+  position: relative;
+
+  .el-input {
+    width: 100%;
+  }
+
+  .el-input-group__append {
+    padding: 0 15px;
+    background-color: transparent;
+    border-left: none;
+    box-shadow: none;
+  }
+
+  .el-button {
+    transition: all 0.3s ease;
+
+    &:hover:not(:disabled) {
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(40, 180, 133, 0.3);
+    }
+
+    &:active:not(:disabled) {
+      transform: translateY(0);
+    }
+  }
+
+  // 适配不同状态下的样式
+  .el-input.is-disabled {
+    .el-input-group__append {
+      background-color: #f5f7fa;
+    }
+  }
+
+  // 添加一些间距和布局优化
+  display: inline-flex;
+  flex-direction: column;
+  gap: 4px;
+}
+</style>

+ 1 - 1
src/components/upload-image/index.vue

@@ -57,7 +57,7 @@
     </div>
     <FileSelector
       v-model="logoSelectorVisible"
-      title="选择品牌Logo"
+      title="选择图片"
       :allowed-types="[1]"
       :multiple="false"
       :allow-upload="true"

+ 6 - 0
src/router/index.ts

@@ -194,6 +194,12 @@ export const constantRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/platform/customerOperation/vipSite/productConfig.vue'),
         name: 'VipProductConfig',
         meta: { title: '商品配置', activeMenu: '/customerOperation/vipSite', noCache: true }
+      },
+      {
+        path: 'vipSite/styleDesign',
+        component: () => import('@/views/platform/customerOperation/vipSite/styleDesign.vue'),
+        name: 'VipStyleDesign',
+        meta: { title: '样式设置', activeMenu: '/customerOperation/vipSite', noCache: true }
       }
     ]
   },

+ 558 - 0
src/store/modules/pcdiy.ts

@@ -0,0 +1,558 @@
+import { defineStore } from 'pinia';
+import { toRaw } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { cloneDeep } from 'lodash-es';
+import { fa } from 'element-plus/es/locale/index.mjs';
+
+const usePcdiyStore = defineStore('pcdiy', {
+  state: () => {
+    return {
+      componentList: [],
+      uniqueIdCounter: 0,
+      currentIndex: -99, // 当前正在编辑的组件下标
+      currentKey: '',
+      editTab: 'content', // 编辑页面
+      predefineColors: [
+        '#F4391c',
+        '#ff4500',
+        '#ff8c00',
+        '#FFD009',
+        '#ffd700',
+        '#19C650',
+        '#90ee90',
+        '#00ced1',
+        '#1e90ff',
+        '#c71585',
+        '#FF407E',
+        '#CFAF70',
+        '#A253FF',
+        'rgba(255, 69, 0, 0.68)',
+        'rgb(255, 120, 0)',
+        'hsl(181, 100%, 37%)',
+        'hsla(209, 100%, 56%, 0.73)',
+        '#c7158577'
+      ],
+      global: {
+        title: '页面',
+        // 公共模板属性,所有组件都继承,无需重复定义,组件内部根据业务自行调用
+        template: {
+          textColor: '#303133', // 文字颜色
+          pageStartBgColor: '', // 组件底部背景颜色(开始)
+          pageEndBgColor: '', // 组件底部背景颜色(结束)
+          pageGradientAngle: 'to bottom', // 渐变角度,从上到下(to bottom)、从左到右(to right)
+
+          componentStartBgColor: '', // 组件背景颜色(开始)
+          componentEndBgColor: '', // 组件背景颜色(结束)
+          componentGradientAngle: 'to bottom', // 渐变角度,从上到下(to bottom)、从左到右(to right)
+
+          topRounded: 0, // 组件上圆角
+          bottomRounded: 0, // 组件下圆角
+
+          padding: {
+            top: 0, // 上边距
+            bottom: 0, // 下边距
+            both: 0 // 左右边距
+          },
+          isHidden: false // 是否隐藏该组件 true:是,false:否,增加问号说明:勾选后该组件将隐藏,适用于你不希望看到该组件字段又不希望删除的情况;
+        }
+      }
+    };
+  },
+  getters: {
+    editComponent: (state) => {
+      if (state.currentIndex == -99) {
+        return state.global;
+      } else {
+        return state.componentList[state.currentIndex];
+      }
+    }
+  },
+  actions: {
+    // 添加组件
+    addComponent(item: any, key: any) {
+      if (this.componentList.length == 0) {
+        this.uniqueIdCounter = 0;
+      }
+      this.uniqueIdCounter++;
+      const template = cloneDeep(this.global.template);
+      const newItem = {
+        ...item,
+        ...template,
+        itemKey: this.uniqueIdCounter,
+        ignore: []
+      };
+      // 头部组件
+      if (item.id == 1) {
+        newItem.ignore = ['pageBgColor', 'componentBgUrl', 'componentBgColor', 'componentBgColor', 'topRounded', 'bottomRounded', 'marginBoth'];
+        newItem.settings = 1;
+        newItem.carousel = 1;
+        newItem.topStyle = 1;
+        newItem.logo = '';
+        newItem.code = '';
+        newItem.classifyShow = true;
+        newItem.topType = 1;
+        newItem.topNav = [
+          {
+            title: '导航名称',
+            url: '',
+            id: Date.now()
+          }
+        ];
+        newItem.toplabel = [
+          {
+            title: '标签名称',
+            url: '',
+            id: Date.now()
+          }
+        ];
+
+        newItem.leftStyle = 1;
+        newItem.leftBackground = '#ffffff';
+        newItem.leftColor1 = '#101828';
+        newItem.leftColor2 = '#364153';
+
+        newItem.carouselStyle = false;
+        newItem.carouselType = 0;
+        newItem.carouselRadius = 10;
+        newItem.carouselInterval = 3000;
+
+        newItem.centreType = 1;
+        newItem.carouselList = [
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            show: true,
+            id: Date.now()
+          }
+        ];
+        newItem.advertNum = 0;
+        newItem.advertList = [
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now(),
+            show: false
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now(),
+            show: false
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now(),
+            show: false
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now(),
+            show: false
+          }
+        ];
+        newItem.realType = 1;
+        newItem.realDataType = 1;
+        newItem.realNumber = 5;
+        newItem.realIds = [];
+        newItem.navlList = [
+          {
+            imageUrl: '',
+            title: '导航名称',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+        newItem.rightRadius = 5;
+      } else if (item.id == 2) {
+        // 文本标题
+        newItem.styleType = 1;
+
+        newItem.title = '标题名称';
+        newItem.titleUrl = '';
+        newItem.titleAlign = 'left';
+        newItem.titleSize = 18;
+        newItem.titleColor = '#101828';
+        newItem.titleWeight = 'bold';
+
+        newItem.subtitle = '副标题';
+        newItem.subtitleSize = 16;
+        newItem.subtitleColor = '#b7bcd2';
+
+        newItem.more = '更多';
+        newItem.moreShow = true;
+        newItem.moreSize = 14;
+        newItem.moreColor = '#b7bcd2';
+        newItem.moreUrl = '';
+
+        newItem.pageStartBgColor = '#ffffff';
+        newItem.padding.top = 10;
+        newItem.padding.bottom = 10;
+        newItem.padding.both = 20;
+      } else if (item.id == 3) {
+        //图文导航
+        newItem.styleType = 1;
+        newItem.number = 3;
+        newItem.count = 1;
+        newItem.navlList = [
+          {
+            imageUrl: '',
+            title: '标题名称',
+            subtitle: '副标题名称',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+
+        newItem.titleSize = 16;
+        newItem.titleColor = '#101828';
+        newItem.titleWeight = 'bold';
+        newItem.subtitleSize = 12;
+        newItem.subtitleColor = '#364153';
+        newItem.imageRadius = 10;
+        newItem.componentStartBgColor = '#ffffff';
+        newItem.topRounded = 10;
+        newItem.bottomRounded = 10;
+      } else if (item.id == 4) {
+        // 图片魔方
+        newItem.number = 1;
+        newItem.imageHeight = 150;
+        newItem.imagelList = [
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+        newItem.gap = 10;
+        newItem.imageTopRounded = 10;
+        newItem.imageBottomRoundedRounded = 10;
+      } else if (item.id == 6) {
+        //轮播图
+        newItem.imageHeight = 200;
+        newItem.imagelList = [
+          {
+            imageUrl: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+        newItem.interval = 3000;
+        newItem.styleType = 1;
+        newItem.position = 2;
+        newItem.imageRadius = 10;
+        newItem.ignore = ['componentBgColor', 'topRounded', 'bottomRounded'];
+      } else if (item.id == 7) {
+        //文章咨询
+        newItem.dataType = 1;
+        newItem.dataNumber = 4;
+        newItem.dataIds = [];
+        newItem.border = 1;
+        newItem.borderColor = '#ffffff';
+        newItem.backgroundColor = '#ffffff';
+        newItem.boxTopRounded = 10;
+        newItem.boxBottomRounded = 10;
+      } else if (item.id == 8) {
+        //品牌组件
+        newItem.settings = 1;
+        newItem.imageUrl = '';
+        newItem.url = '';
+        newItem.imgType = 1;
+        newItem.brandList = [
+          {
+            imageUrl: '',
+            title: '品牌名称',
+            subtitle: '品牌介绍',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+        newItem.titleSize = 14;
+        newItem.titleColor = '#101828';
+        newItem.titleWeight = 'bold';
+        newItem.subtitleSize = 12;
+        newItem.subtitleColor = '#364153';
+        newItem.imageRadius = 10;
+        newItem.boxRadius = 5;
+      } else if (item.id == 9) {
+        //图文广告
+        newItem.styleType = 1;
+        newItem.number = 3;
+        newItem.count = 1;
+        newItem.navlList = [
+          {
+            imageUrl: '',
+            title: '标题名称',
+            subtitle: '副标题名称',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+
+        newItem.titleSize = 16;
+        newItem.titleColor = '#101828';
+        newItem.titleWeight = 'bold';
+        newItem.subtitleSize = 12;
+        newItem.subtitleColor = '#364153';
+        newItem.imageRadius = 10;
+        newItem.componentStartBgColor = '#ffffff';
+        newItem.topRounded = 10;
+        newItem.bottomRounded = 10;
+      } else if (item.id == 10) {
+        //楼层组件
+        newItem.styleType = 1;
+        newItem.imageUrl = '';
+        newItem.imgType = 1;
+        newItem.imageRadius = 10;
+        newItem.goodsIds = [];
+        newItem.goodsShow = [1, 2, 3];
+        newItem.btnShow = true;
+        newItem.btnStyle = 1;
+        newItem.btnText = '购买';
+        newItem.btnColor = '#ffffff';
+        newItem.btnbackgroundColor = '#E7000B';
+        newItem.moreTitle = '更多';
+        newItem.moreUrl = '';
+        newItem.moreColor = '#E7000B';
+        newItem.moreShow = true;
+        newItem.brandList = [
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+      } else if (item.id == 11) {
+        // 商品组件
+        newItem.styleType = 1;
+        newItem.btnShow = true;
+        newItem.btnStyle = 1;
+        newItem.btnText = '购买';
+        newItem.goodsShow = [1, 2, 3];
+        newItem.goodsType = 1;
+        newItem.goodsIds = [];
+        newItem.goodsClassify = '';
+        newItem.topCategoryId = '';
+        newItem.mediumCategoryId = '';
+        newItem.bottomCategoryId = '';
+        newItem.goodsNumber = 5;
+        newItem.goodsSort = 1;
+        newItem.goodsBrand = '';
+
+        newItem.goodsbackgroundColor = '#ffffff';
+        newItem.goodsTitleType = 1;
+        newItem.goodsTitleColor = '#101828';
+        newItem.imageRadius = 10;
+        newItem.goodstopRounded = 10;
+        newItem.goodsbottomRounded = 10;
+        newItem.priceColor = '#E7000B';
+        newItem.btnColor = '#ffffff';
+        newItem.btnbackgroundColor = '#E7000B';
+      } else if (item.id == 12) {
+        //多商品组件
+        newItem.btnShow = true;
+        newItem.btnStyle = 1;
+        newItem.btnText = '购买';
+        newItem.goodsShow = [1, 2, 3];
+        newItem.goodsNumber = 5;
+        newItem.goodsSort = 1;
+
+        newItem.goodsbackgroundColor = '#ffffff';
+        newItem.goodsTitleType = 1;
+        newItem.goodsTitleColor = '#101828';
+        newItem.imageRadius = 10;
+        newItem.goodstopRounded = 10;
+        newItem.goodsbottomRounded = 10;
+        newItem.priceColor = '#E7000B';
+        newItem.btnColor = '#ffffff';
+        newItem.btnbackgroundColor = '#E7000B';
+
+        newItem.tabColor1 = '#333333';
+        newItem.tabColor2 = '#ffffff';
+        newItem.tabbackgroundColor1 = '#ffffff';
+        newItem.tabbackgroundColor2 = '#E7000B';
+        newItem.tabRadius = 10;
+        newItem.tabIndex = 0;
+        newItem.tabList = [
+          {
+            title: '选项卡',
+            goodsType: 1,
+            goodsIds: [],
+            goodsClassify: '',
+            topCategoryId: '',
+            mediumCategoryId: '',
+            bottomCategoryId: '',
+            id: Date.now()
+          }
+        ];
+      } else if (item.id == 13) {
+        // 发现组件
+        newItem.settings = 1;
+        newItem.title = '标题';
+        newItem.titleUrl = '';
+        newItem.titleSize = 18;
+        newItem.titleColor = '#101828';
+        newItem.titleWeight = 'bold';
+        newItem.subtitle = '副标题';
+        newItem.subtitleSize = 14;
+        newItem.subtitleColor = '#364153';
+        newItem.imgType = 1;
+
+        newItem.tabList = [
+          {
+            title: '导航名称',
+            url: '',
+            id: Date.now()
+          }
+        ];
+
+        newItem.imageUrl = '';
+        newItem.imageRadius = 10;
+        newItem.boxRadius = 5;
+        newItem.boxColor = '#E7000B';
+
+        newItem.planList = [
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+
+        newItem.detectList = [
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          },
+          {
+            imageUrl: '',
+            title: '',
+            subtitle: '',
+            url: '',
+            imgType: 1,
+            id: Date.now()
+          }
+        ];
+
+        newItem.labelList = [
+          {
+            title: '标签名称',
+            url: '',
+            id: Date.now()
+          }
+        ];
+        newItem.goodsIds = [];
+      }
+
+      this.componentList.push(newItem);
+      this.currentIndex = this.componentList.length - 1;
+      this.currentKey = newItem.itemKey;
+
+      console.log(this.componentList, '4564654');
+    },
+    //点击组件
+    onComponent(item: any, index: any) {
+      this.currentKey = item.itemKey;
+      this.currentIndex = index;
+    }
+  }
+});
+
+export default usePcdiyStore;

+ 458 - 0
src/views/diy/pcEdit.vue

@@ -0,0 +1,458 @@
+<template>
+  <div class="pcEdit">
+    <div class="pcEdit-pages">
+      <el-header class="flex items-center h-[50px] bg-primary px-[20px]">
+        <div class="text-white cursor-pointer flex items-center" @click="goBack">
+          <el-icon size="14">
+            <ArrowLeft />
+          </el-icon>
+          <span class="pl-[5px] text-[14px]">返回</span>
+        </div>
+        <div class="text-white ml-[10px] mr-[20px] flex items-center">
+          <span class="mr-[5px] text-[rgba(255,255,255,.5)]">|</span>
+          <span class="mr-[5px] text-[14px]">正在装修:{{ query.title || '页面名字' }}</span>
+        </div>
+        <div class="flex-1"></div>
+        <el-button @click="preview()">保存并预览</el-button>
+        <el-button @click="save()">保存</el-button>
+      </el-header>
+      <div class="full-container flex flex-row flex-1 bg-page">
+        <div class="component-list w-[192px]">
+          <!-- 组件列表区域 -->
+          <el-collapse v-model="activeNames" @change="handleChange">
+            <el-collapse-item v-for="(item, key) in collapse" :key="key" :title="item.name" :name="key">
+              <ul class="flex flex-row flex-wrap">
+                <li
+                  v-for="(compItem, compKey) in item.list"
+                  :key="compKey"
+                  class="w-2/4 text-center cursor-pointer h-[65px]"
+                  :title="compItem.name"
+                  @click="diyStore.addComponent(compItem, compKey)"
+                >
+                  <icon v-if="compItem.icon" :name="compItem.icon" size="20px" class="inline-block mt-[3px]" />
+                  <icon v-else name="iconfont iconkaifazujian" size="20px" class="inline-block mt-[3px]" />
+                  <span class="block text-[12px] truncate">{{ compItem.name }}</span>
+                </li>
+              </ul>
+            </el-collapse-item>
+          </el-collapse>
+        </div>
+        <div class="preview-wrap">
+          <!-- 组件编辑区域 -->
+          <div class="preview-pages shadow-lg">
+            <!-- @end="onDragEnd" -->
+            <draggable v-model="diyStore.componentList" item-key="itemKey" class="drag-area">
+              <template #item="{ element, index }">
+                <div @click="diyStore.onComponent(element, index)" class="component-bos">
+                  <div class="component-box" :style="{ borderWidth: diyStore.currentIndex == index ? '2px' : '0px' }"></div>
+                  <component :is="element.components" :key="element.itemKey" :index="index"></component>
+                </div>
+              </template>
+            </draggable>
+          </div>
+        </div>
+        <!-- 编辑组件属性区域 -->
+        <div class="edit-attribute-wrap w-[400px]">
+          <!-- 编辑组件属性区域 -->
+          <el-scrollbar>
+            <el-card class="box-card" shadow="never">
+              <template #header>
+                <div class="card-header flex justify-between items-center">
+                  <span class="title flex-1">{{ diyStore.currentIndex == -99 ? '页面设置' : diyStore.editComponent.name }}</span>
+                  <div class="tab-wrap flex rounded-[50px] bg-gray-100 text-[14px]">
+                    <span
+                      class="cursor-pointer rounded-[50px] py-[5px] px-[15px]"
+                      :class="{ 'bg-primary text-white': diyStore.editTab == 'content' }"
+                      @click="diyStore.editTab = 'content'"
+                      >内容</span
+                    >
+                    <span
+                      class="cursor-pointer rounded-[50px] py-[5px] px-[15px]"
+                      :class="{ 'bg-primary text-white': diyStore.editTab == 'style' }"
+                      @click="diyStore.editTab = 'style'"
+                      >样式</span
+                    >
+                  </div>
+                </div>
+              </template>
+
+              <div class="edit-component-wrap">
+                <component
+                  v-if="diyStore.currentKey"
+                  :is="diyStore.editComponent.edit"
+                  :key="diyStore.currentIndex"
+                  :value="diyStore.componentList[diyStore.currentIndex]"
+                >
+                  <template #style>
+                    <div class="edit-attr-item-wrap">
+                      <h3 class="mb-[10px]">组件样式</h3>
+                      <el-form label-width="90px" class="px-[10px]">
+                        <template v-if="diyStore.editComponent.ignore.indexOf('pageBgColor') == -1">
+                          <el-form-item label="底部背景">
+                            <el-color-picker v-model="diyStore.editComponent.pageStartBgColor" show-alpha :predefine="diyStore.predefineColors" />
+                            <icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
+                            <el-color-picker v-model="diyStore.editComponent.pageEndBgColor" show-alpha :predefine="diyStore.predefineColors" />
+                          </el-form-item>
+                          <div class="text-sm text-gray-400 ml-[90px] mb-[10px]">底部背景包含边距和圆角</div>
+                        </template>
+                        <el-form-item label="渐变角度" v-if="diyStore.editComponent.ignore.indexOf('pageBgColor') == -1">
+                          <el-radio-group v-model="diyStore.editComponent.pageGradientAngle">
+                            <el-radio value="to bottom">从上到下</el-radio>
+                            <el-radio value="to right">从左到右</el-radio>
+                          </el-radio-group>
+                        </el-form-item>
+                        <el-form-item label="组件背景色" v-if="diyStore.editComponent.ignore.indexOf('componentBgColor') == -1">
+                          <el-color-picker v-model="diyStore.editComponent.componentStartBgColor" show-alpha :predefine="diyStore.predefineColors" />
+                          <icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
+                          <el-color-picker v-model="diyStore.editComponent.componentEndBgColor" show-alpha :predefine="diyStore.predefineColors" />
+                        </el-form-item>
+                        <el-form-item label="渐变角度" v-if="diyStore.editComponent.ignore.indexOf('componentBgColor') == -1">
+                          <el-radio-group v-model="diyStore.editComponent.componentGradientAngle">
+                            <el-radio value="to bottom">从上到下</el-radio>
+                            <el-radio value="to right">从左到右</el-radio>
+                          </el-radio-group>
+                        </el-form-item>
+                        <el-form-item label="上边距" v-if="diyStore.editComponent.ignore.indexOf('marginTop') == -1">
+                          <el-slider
+                            v-model="diyStore.editComponent.padding.top"
+                            show-input
+                            size="small"
+                            :min="-100"
+                            class="ml-[10px] diy-nav-slider"
+                          />
+                        </el-form-item>
+                        <el-form-item label="下边距" v-if="diyStore.editComponent.ignore.indexOf('marginBottom') == -1">
+                          <el-slider
+                            v-model="diyStore.editComponent.padding.bottom"
+                            show-input
+                            size="small"
+                            class="ml-[10px] diy-nav-slider"
+                            :min="-100"
+                          />
+                        </el-form-item>
+                        <el-form-item label="左右边距" v-if="diyStore.editComponent.ignore.indexOf('marginBoth') == -1">
+                          <el-slider v-model="diyStore.editComponent.padding.both" show-input size="small" class="ml-[10px] diy-nav-slider" />
+                        </el-form-item>
+                        <el-form-item label="上圆角" v-if="diyStore.editComponent.ignore.indexOf('topRounded') == -1">
+                          <el-slider
+                            v-model="diyStore.editComponent.topRounded"
+                            show-input
+                            size="small"
+                            class="ml-[10px] diy-nav-slider"
+                            :max="100"
+                          />
+                        </el-form-item>
+                        <el-form-item label="下圆角" v-if="diyStore.editComponent.ignore.indexOf('bottomRounded') == -1">
+                          <el-slider
+                            v-model="diyStore.editComponent.bottomRounded"
+                            show-input
+                            size="small"
+                            class="ml-[10px] diy-nav-slider"
+                            :max="100"
+                          />
+                        </el-form-item>
+                      </el-form>
+                    </div>
+                  </template>
+                </component>
+              </div>
+            </el-card>
+          </el-scrollbar>
+          <!-- <div v-for="(item, index) in diyStore.componentList" :key="index">
+          <component v-if="item.itemKey == diyStore.currentKey" :is="item.edit" :value="item"> </component>
+        </div> -->
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import { pcAddDiy } from '@/api/diy/index';
+import icon from '@/components/icon/index.vue';
+import draggable from 'vuedraggable';
+
+import usePcdiyStore from '@/store/modules/pcdiy';
+const diyStore = usePcdiyStore();
+const route = useRoute();
+const query = route.query;
+// 头部组件
+import head from '@/views/diy/pcPages/head.vue';
+const headRef = shallowRef(head);
+import headEdit from '@/views/diy/pcEdit/head-edit.vue';
+const headEditRef = shallowRef(headEdit);
+//文本标题
+import textTitle from '@/views/diy/pcPages/textTitle.vue';
+const ctextTitleRef = shallowRef(textTitle);
+import textTitleEdit from '@/views/diy/pcEdit/textTitle-edit.vue';
+const textTitleEditRef = shallowRef(textTitleEdit);
+//图文导航
+import navigation from '@/views/diy/pcPages/navigation.vue';
+const navigationRef = shallowRef(navigation);
+import navigationEdit from '@/views/diy/pcEdit/navigation-edit.vue';
+const navigationEditRef = shallowRef(navigationEdit);
+//图片魔方
+import imageCube from '@/views/diy/pcPages/imageCube.vue';
+const imageCubeRef = shallowRef(imageCube);
+import imageCubeEdit from '@/views/diy/pcEdit/imageCube-edit.vue';
+const imageCubeEditRef = shallowRef(imageCubeEdit);
+//轮播图
+import carousel from '@/views/diy/pcPages/carousel.vue';
+const carouselRef = shallowRef(carousel);
+import carouselEdit from '@/views/diy/pcEdit/carousel-edit.vue';
+const carouselEditRef = shallowRef(carouselEdit);
+//文章咨询
+import article from '@/views/diy/pcPages/article.vue';
+const articleRef = shallowRef(article);
+import articleEdit from '@/views/diy/pcEdit/article-edit.vue';
+const articleEditRef = shallowRef(articleEdit);
+//品牌组件
+import brand from '@/views/diy/pcPages/brand.vue';
+const brandeRef = shallowRef(brand);
+import brandEdit from '@/views/diy/pcEdit/brand-edit.vue';
+const brandEditRef = shallowRef(brandEdit);
+//图文广告
+import advert from '@/views/diy/pcPages/advert.vue';
+const advertRef = shallowRef(advert);
+import advertEdit from '@/views/diy/pcEdit/advert-edit.vue';
+const advertEditRef = shallowRef(advertEdit);
+//楼层组件
+import floor from '@/views/diy/pcPages/floor.vue';
+const floorRef = shallowRef(floor);
+import floorEdit from '@/views/diy/pcEdit/floor-edit.vue';
+const floorEditRef = shallowRef(floorEdit);
+//商品组件
+import goods from '@/views/diy/pcPages/goods.vue';
+const goodsRef = shallowRef(goods);
+import goodsEdit from '@/views/diy/pcEdit/goods-edit.vue';
+const goodsEditRef = shallowRef(goodsEdit);
+//多商品组
+import goodsList from '@/views/diy/pcPages/goodsList.vue';
+const goodsListRef = shallowRef(goodsList);
+import goodsListEdit from '@/views/diy/pcEdit/goodsList-edit.vue';
+const goodsListEditRef = shallowRef(goodsListEdit);
+//发现组件
+import discover from '@/views/diy/pcPages/discover.vue';
+const discoverRef = shallowRef(discover);
+import discoverEdit from '@/views/diy/pcEdit/discover-edit.vue';
+const discoverEditRef = shallowRef(discoverEdit);
+
+const itemKey = ref<any>(0);
+//左边得组件
+const uniqueIdCounter = ref<any>(0);
+const activeNames = ref<any>([0]);
+const collapse = ref<any>([
+  {
+    name: '基础组件',
+    list: [
+      {
+        name: '头部组件',
+        icon: 'iconfont iconfuwenbenpc',
+        id: 1,
+        components: markRaw(headRef.value),
+        edit: markRaw(headEditRef.value)
+      },
+      {
+        name: '文本标题',
+        icon: 'iconfont iconbiaotipc',
+        id: 2,
+        components: markRaw(ctextTitleRef.value),
+        edit: markRaw(textTitleEditRef.value)
+      },
+      {
+        name: '图文导航',
+        icon: 'iconfont icontuwendaohangpc',
+        id: 3,
+        components: markRaw(navigationRef.value),
+        edit: markRaw(navigationEditRef.value)
+      },
+      {
+        name: '图片魔方',
+        icon: 'iconfont iconmofangpc',
+        id: 4,
+        components: markRaw(imageCubeRef.value),
+        edit: markRaw(imageCubeEditRef.value)
+      },
+      {
+        name: '活动魔方',
+        icon: 'iconfont iconmofangpc',
+        id: 5
+      },
+      {
+        name: '轮播图',
+        icon: 'iconfont icona-tupianzhanbopc302',
+        id: 6,
+        components: markRaw(carouselRef.value),
+        edit: markRaw(carouselEditRef.value)
+      },
+      {
+        name: '文章咨询',
+        icon: 'iconfont icongonggaopc',
+        id: 7,
+        components: markRaw(articleRef.value),
+        edit: markRaw(articleEditRef.value)
+      },
+      {
+        name: '品牌组件',
+        icon: 'iconfont iconmiaoshashangpin',
+        id: 8,
+        components: markRaw(brandeRef.value),
+        edit: markRaw(brandEditRef.value)
+      },
+      {
+        name: '图文广告',
+        icon: 'iconfont icontupiandaohangpc',
+        id: 9,
+        components: markRaw(advertRef.value),
+        edit: markRaw(advertEditRef.value)
+      },
+      {
+        name: '楼层组件',
+        icon: 'iconfont iconshangpinliebiaopc',
+        id: 10,
+        components: markRaw(floorRef.value),
+        edit: markRaw(floorEditRef.value)
+      },
+      {
+        name: '商品组件',
+        icon: 'iconfont icona-shangpintuijianpc30',
+        id: 11,
+        components: markRaw(goodsRef.value),
+        edit: markRaw(goodsEditRef.value)
+      },
+      {
+        name: '多商品组',
+        icon: 'iconfont iconduoshangpinzupc',
+        id: 12,
+        components: markRaw(goodsListRef.value),
+        edit: markRaw(goodsListEditRef.value)
+      },
+      {
+        name: '发现组件',
+        icon: 'iconfont iconrequpc',
+        id: 13,
+        components: markRaw(discoverRef.value),
+        edit: markRaw(discoverEditRef.value)
+      }
+    ]
+  }
+]);
+
+const componentList = ref<any>([
+  // {
+  //   components: ctextTitleRef
+  // }
+]);
+const handleChange = (val: string[]) => {};
+// 返回上一页
+const goBack = () => {};
+
+// 预览
+const preview = () => {};
+
+// 保存
+const save = () => {
+  const datas = {
+    name: query.title,
+    siteId: '',
+    clientId: '',
+    type: query.type,
+    remark: '',
+    previewPicUrls: '',
+    property: JSON.stringify(diyStore.componentList),
+    isHome: 1
+  };
+  const api = pcAddDiy;
+  api(datas)
+    .then((res: any) => {
+      if (res.code == 200) {
+      }
+    })
+    .catch(() => {});
+};
+</script>
+
+<style lang="scss" scoped>
+.pcEdit {
+  width: 100%;
+  overflow: auto;
+}
+.pcEdit-pages {
+  min-height: calc(100vh - 84px);
+  // min-width: 1900px;
+
+  .full-container {
+    height: calc(100vh - 134px);
+    background-color: #f2f2f2;
+
+    .component-list {
+      height: 100%;
+      background-color: #ffffff;
+      padding: 0 10px;
+    }
+
+    .component-list ul li {
+      &:not(.disabled):hover {
+        color: var(--el-color-primary);
+        background: var(--el-color-primary-light-9);
+      }
+    }
+
+    .preview-wrap {
+      flex: 1;
+      display: flex;
+      justify-content: center;
+
+      .preview-pages {
+        margin: 30px auto;
+        width: 1300px;
+        background: var(--el-bg-color-page);
+        overflow: auto;
+        height: calc(130vh - 194px);
+        zoom: 0.7;
+        // height: calc(100vh - 194px);
+
+        /* 为了兼容某些情况,可能还需要配合 display */
+        display: inline-block;
+      }
+
+      .component-bos {
+        position: relative;
+        .component-box {
+          position: absolute;
+          width: 100%;
+          height: 100%;
+          top: 0;
+          left: 0;
+          border: 2px solid var(--el-color-primary);
+          z-index: 2;
+          cursor: move;
+        }
+      }
+    }
+
+    //编辑组件属性区域
+    .edit-attribute-wrap {
+      background: var(--el-bg-color);
+    }
+
+    .edit-attribute-wrap .box-card {
+      border: none;
+    }
+
+    .edit-attr-item-wrap {
+      border-top: 2px solid var(--el-color-info-light-8);
+      padding-top: 20px;
+
+      &:first-of-type {
+        border-top: none;
+        padding-top: 0;
+      }
+    }
+  }
+
+  :deep(.el-header) {
+    height: 50px;
+  }
+}
+</style>

+ 226 - 0
src/views/diy/pcEdit/advert-edit.vue

@@ -0,0 +1,226 @@
+<template>
+  <div class="pc-edit">
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">导航模式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="展示风格">
+            <el-radio-group v-model="diyStore.editComponent.styleType">
+              <el-radio :value="1">固定显示</el-radio>
+              <el-radio :value="2">单行滑动</el-radio>
+              <el-radio :value="3">分页滑动</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="每行数量">
+            <el-radio-group v-model="diyStore.editComponent.number">
+              <el-radio :value="3">3个</el-radio>
+              <el-radio :value="4">4个</el-radio>
+              <el-radio :value="5">5个</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="每页行数" v-if="diyStore.editComponent.styleType == 3">
+            <el-radio-group v-model="diyStore.editComponent.count">
+              <el-radio :value="1">1个</el-radio>
+              <el-radio :value="2">2个</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>导航设置</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <el-form label-width="86px" class="px-[10px]">
+          <draggable v-model="diyStore.editComponent.navlList" item-key="id">
+            <template #item="{ element, index }">
+              <div class="edit-attr-box">
+                <el-icon @click="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                  <CircleCloseFilled />
+                </el-icon>
+                <el-form-item label="图片上传">
+                  <div class="flex-row-start">
+                    <upload-image v-model="element.imageUrl" :limit="1" />
+                    <div class="flex-column-between images-bos">
+                      <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                      <div class="flex-row-between images-box">
+                        <div>缩放模式</div>
+                        <div class="flex-row-start" @click="openImageType(element, index)">
+                          <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                            element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                          }}</span>
+                          <el-icon class="cursor-pointer">
+                            <ArrowRight />
+                          </el-icon>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </el-form-item>
+                <el-form-item label="标题名称">
+                  <el-input v-model="element.title" placeholder="请输入标签名称" :maxlength="10" show-word-limit />
+                </el-form-item>
+                <el-form-item label="副标题名称">
+                  <el-input v-model="element.subtitle" placeholder="请输入标签名称" />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </template>
+          </draggable>
+          <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增导航</el-button>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">样式设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.titleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字加粗">
+            <el-radio-group size="small" v-model="diyStore.editComponent.titleWeight" fill="#409eff">
+              <el-radio-button label="加粗" :value="'bold'" />
+              <el-radio-button label="不加粗" :value="'normal'" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.titleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.titleColor" />
+            <el-button @click="diyStore.editComponent.titleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">副标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.subtitleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.subtitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.subtitleColor" />
+            <el-button @click="diyStore.editComponent.subtitleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+//图片类型返回
+const confirmCallBack = (res: any, index: any) => {
+  diyStore.editComponent.navlList[index].imgType = res.imgType;
+};
+const onAdd = () => {
+  diyStore.editComponent.navlList.push({
+    imageUrl: '',
+    title: '标题名称',
+    subtitle: '副标题名称',
+    url: '',
+    imgType: 1,
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.navlList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+
+  // :deep(.el-radio){
+  //   margin-right: 10px;
+  // }
+}
+</style>

+ 242 - 0
src/views/diy/pcEdit/article-edit.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">文章数据</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="数据来源">
+            <el-radio-group v-model="diyStore.editComponent.dataType">
+              <el-radio :value="1">默认</el-radio>
+              <el-radio :value="2">手动</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文章数量" v-if="diyStore.editComponent.dataType == 1">
+            <el-slider size="small" v-model="diyStore.editComponent.dataNumber" show-input :min="1" :max="16" />
+          </el-form-item>
+          <el-form-item label="手动选择" v-else>
+            <div class="data-num" @click="openDialog">
+              <span v-if="diyStore.editComponent.dataIds.length == 0">请选择</span>
+              <span v-else>已选择{{ diyStore.editComponent.dataIds.length }}个</span>
+              <el-icon><ArrowRight /></el-icon>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">样式设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="边框">
+            <el-radio-group v-model="diyStore.editComponent.border">
+              <el-radio :value="1">默认</el-radio>
+              <el-radio :value="2">投影</el-radio>
+              <el-radio :value="3">描边</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="边框颜色" v-if="diyStore.editComponent.border != 1">
+            <span class="mr-[10px]">{{ diyStore.editComponent.borderColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.borderColor" />
+            <el-button @click="diyStore.editComponent.borderColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="文章背景">
+            <span class="mr-[10px]">{{ diyStore.editComponent.backgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.backgroundColor" />
+            <el-button @click="diyStore.editComponent.backgroundColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="上圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.boxTopRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="下圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.boxBottomRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择文章">
+      <div class="data-bos">
+        <el-input v-model="queryParams.caseTitle" placeholder="请输入服务标题" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button @click="handleQuery" :icon="Search" />
+          </template>
+        </el-input>
+        <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" />
+          <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>
+                </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>
+        <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { listServiceCase } from '@/api/product/serviceCase';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { Search } from '@element-plus/icons-vue';
+import type { TableInstance } from 'element-plus';
+const diyStore = usePcdiyStore();
+
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  caseTitle: '',
+  projectTypeId: undefined as number | undefined
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listServiceCase(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.dataIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+
+    console.log('result', result);
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+//打开弹窗
+const openDialog = () => {
+  showDialog.value = true;
+  getList();
+};
+
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.dataIds, tableData.value, multipleSelection.value);
+  diyStore.editComponent.dataIds = newIds;
+  showDialog.value = false;
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .data-num {
+      width: 100%;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--el-color-primary);
+      cursor: pointer;
+    }
+  }
+
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+}
+</style>

+ 254 - 0
src/views/diy/pcEdit/brand-edit.vue

@@ -0,0 +1,254 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">品牌设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="设置">
+            <el-radio-group v-model="diyStore.editComponent.settings" fill="#409eff">
+              <el-radio-button label="图片" :value="1" />
+              <el-radio-button label="品牌" :value="2" />
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <template v-if="diyStore.editComponent.settings == 1">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">图片设置</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <div class="edit-attr-box">
+              <el-form-item label="图片上传">
+                <div class="flex-row-start">
+                  <upload-image v-model="diyStore.editComponent.imageUrl" :limit="1" />
+                  <div class="flex-column-between images-bos">
+                    <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸230*340)</div>
+                    <div class="flex-row-between images-box">
+                      <div>缩放模式</div>
+                      <div class="flex-row-start" @click="openImageType({ imgType: diyStore.editComponent.imgType }, 1)">
+                        <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                          diyStore.editComponent.imgType == 1 ? '拉伸' : diyStore.editComponent.imgType == 2 ? '缩放' : '填充'
+                        }}</span>
+                        <el-icon class="cursor-pointer">
+                          <ArrowRight />
+                        </el-icon>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+              <el-form-item label="链接地址">
+                <el-input v-model="diyStore.editComponent.url" placeholder="请输入链接地址" />
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+      </template>
+      <template v-if="diyStore.editComponent.settings == 2">
+        <div class="edit-attr-item-wrap">
+          <div class="edit-attr-title flex-row-between">
+            <div>
+              <span>品牌设置</span>
+              <span class="title2">鼠标拖拽可以改变顺序</span>
+            </div>
+          </div>
+          <draggable v-model="diyStore.editComponent.brandList" item-key="id">
+            <template #item="{ element, index }">
+              <el-form label-width="90px" class="px-[10px]">
+                <div class="edit-attr-box">
+                  <el-icon @click="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                    <CircleCloseFilled />
+                  </el-icon>
+                  <el-form-item label="图片上传">
+                    <div class="flex-row-start">
+                      <upload-image v-model="element.imageUrl" :limit="1" />
+                      <div class="flex-column-between images-bos">
+                        <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                        <div class="flex-row-between images-box">
+                          <div>缩放模式</div>
+                          <div class="flex-row-start" @click="openImageType(element, 2)">
+                            <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                              element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                            }}</span>
+                            <el-icon class="cursor-pointer">
+                              <ArrowRight />
+                            </el-icon>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </el-form-item>
+                  <el-form-item label="品牌名称">
+                    <el-input v-model="element.title" placeholder="请输入品牌名称" :maxlength="10" show-word-limit />
+                  </el-form-item>
+                  <el-form-item label="副标题名称">
+                    <el-input v-model="element.subtitle" placeholder="请输入副标题内容" />
+                  </el-form-item>
+                  <el-form-item label="链接地址">
+                    <el-input v-model="element.url" placeholder="请输入链接地址" />
+                  </el-form-item>
+                </div>
+              </el-form>
+            </template>
+          </draggable>
+          <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增品牌</el-button>
+        </div>
+      </template>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">样式设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.titleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字加粗">
+            <el-radio-group size="small" v-model="diyStore.editComponent.titleWeight" fill="#409eff">
+              <el-radio-button label="加粗" :value="'bold'" />
+              <el-radio-button label="不加粗" :value="'normal'" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.titleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.titleColor" />
+            <el-button @click="diyStore.editComponent.titleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">副标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.subtitleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.subtitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.subtitleColor" />
+            <el-button @click="diyStore.editComponent.subtitleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+          <el-form-item label="模块圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.boxRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+// 图片类型返回
+const confirmCallBack = (res: any, type: any) => {
+  if (type == 1) {
+    diyStore.editComponent.imgType = res.imgType;
+  } else {
+    diyStore.editComponent.brandList.forEach((item: any) => {
+      if (item.id == res.id) {
+        item.imgType = res.imgType;
+      }
+    });
+  }
+};
+
+const onAdd = () => {
+  diyStore.editComponent.brandList.push({
+    imageUrl: '',
+    title: '品牌名称',
+    subtitle: '品牌介绍',
+    url: '',
+    imgType: 1,
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.brandList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+
+      .annotation3 {
+        font-size: 12px;
+        color: #666;
+        line-height: 14px;
+      }
+    }
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 187 - 0
src/views/diy/pcEdit/carousel-edit.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">魔方设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="图片高度">
+            <el-input-number v-model="diyStore.editComponent.imageHeight" :min="1" :max="300">
+              <template #suffix>
+                <span>px</span>
+              </template>
+            </el-input-number>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>魔方布局</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <draggable v-model="diyStore.editComponent.imagelList" item-key="id">
+          <template #item="{ element, index }">
+            <div class="edit-attr-box">
+              <el-icon @click="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                <CircleCloseFilled />
+              </el-icon>
+              <el-form-item label="图片上传">
+                <div class="flex-row-start">
+                  <upload-image v-model="element.imageUrl" :limit="1" />
+                  <div class="flex-column-between images-bos">
+                    <div class="annotation3">
+                      (建议上传尺寸相同图片,推荐尺寸{{ Math.floor(1200 / diyStore.editComponent.number) }}*{{ diyStore.editComponent.imageHeight }})
+                    </div>
+                    <div class="flex-row-between images-box">
+                      <div>缩放模式</div>
+                      <div class="flex-row-start" @click="openImageType(element, index)">
+                        <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                          element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                        }}</span>
+                        <el-icon class="cursor-pointer">
+                          <ArrowRight />
+                        </el-icon>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+            </div>
+          </template>
+        </draggable>
+        <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增轮播图</el-button>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片间隙</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="切换间隔ms" label-width="100px">
+            <el-slider size="small" v-model="diyStore.editComponent.interval" show-input :min="1000" :max="5000" />
+          </el-form-item>
+          <el-form-item label="样式">
+            <el-radio-group v-model="diyStore.editComponent.styleType">
+              <el-radio :value="1">直线</el-radio>
+              <el-radio :value="2">圆点</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="位置">
+            <el-radio-group v-model="diyStore.editComponent.position">
+              <el-radio :value="1">居左</el-radio>
+              <el-radio :value="2">居中</el-radio>
+              <el-radio :value="3">居右</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+//图片类型返回
+const confirmCallBack = (res: any, index: any) => {
+  diyStore.editComponent.imagelList[index].imgType = res.imgType;
+};
+
+const onAdd = () => {
+  diyStore.editComponent.imagelList.push({
+    imageUrl: '',
+    imgType: 1,
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.imagelList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+
+      .annotation3 {
+        font-size: 12px;
+        color: #666;
+        line-height: 14px;
+      }
+    }
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 631 - 0
src/views/diy/pcEdit/discover-edit.vue

@@ -0,0 +1,631 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">品牌设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="设置">
+            <el-radio-group v-model="diyStore.editComponent.settings" fill="#409eff">
+              <el-radio-button label="标题" :value="1" />
+              <el-radio-button label="导航" :value="2" />
+              <el-radio-button label="图片" :value="3" />
+              <el-radio-button label="方案" :value="4" />
+              <el-radio-button label="发现" :value="5" />
+              <el-radio-button label="标签" :value="6" />
+              <el-radio-button label="商品" :value="7" />
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <template v-if="diyStore.editComponent.settings == 1">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">标题内容</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <div class="edit-attr-box">
+              <el-form-item label="标题名称">
+                <el-input v-model="diyStore.editComponent.title" placeholder="请输入标题内容" />
+              </el-form-item>
+              <el-form-item label="链接地址">
+                <el-input v-model="diyStore.editComponent.titleUrl" placeholder="请输入链接地址" />
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+        <div class="edit-attr-item-wrap mt-[20px]">
+          <h3 class="mb-[10px]">副标题内容</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <div class="edit-attr-box">
+              <el-form-item label="标题名称">
+                <el-input v-model="diyStore.editComponent.subtitle" placeholder="请输入副标题内容" />
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+      </template>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 2">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>导航设置</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <draggable v-model="diyStore.editComponent.tabList" item-key="id">
+          <template #item="{ element, index }">
+            <el-form label-width="90px" class="px-[10px]">
+              <div class="edit-attr-box">
+                <el-icon @click="onDel(1, index)" color="#F56C6C" size="18px" class="circleClose">
+                  <CircleCloseFilled />
+                </el-icon>
+                <el-form-item label="导航名称">
+                  <el-input v-model="element.title" placeholder="请输入导航名称" :maxlength="5" show-word-limit />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </el-form>
+          </template>
+        </draggable>
+        <el-button @click="onAdd(1)" style="width: 100%; margin-top: 10px">新增导航</el-button>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 3">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <div class="edit-attr-box">
+            <el-form-item label="图片上传">
+              <div class="flex-row-start">
+                <upload-image v-model="diyStore.editComponent.imageUrl" :limit="1" />
+                <div class="flex-column-between images-bos">
+                  <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸230*340)</div>
+                  <div class="flex-row-between images-box">
+                    <div>缩放模式</div>
+                    <div class="flex-row-start" @click="openImageType({ imgType: diyStore.editComponent.imgType }, 1)">
+                      <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                        diyStore.editComponent.imgType == 1 ? '拉伸' : diyStore.editComponent.imgType == 2 ? '缩放' : '填充'
+                      }}</span>
+                      <el-icon class="cursor-pointer">
+                        <ArrowRight />
+                      </el-icon>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 4">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>方案设置</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <el-form label-width="86px" class="px-[10px]">
+          <draggable v-model="diyStore.editComponent.planList" item-key="id">
+            <template #item="{ element }">
+              <div class="edit-attr-box">
+                <el-form-item label="图片上传">
+                  <div class="flex-row-start">
+                    <upload-image v-model="element.imageUrl" :limit="1" />
+                    <div class="flex-column-between images-bos">
+                      <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                      <div class="flex-row-between images-box">
+                        <div>缩放模式</div>
+                        <div class="flex-row-start" @click="openImageType(element, 2)">
+                          <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                            element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                          }}</span>
+                          <el-icon class="cursor-pointer">
+                            <ArrowRight />
+                          </el-icon>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </el-form-item>
+                <el-form-item label="方案名称">
+                  <el-input v-model="element.title" placeholder="请输入方案名称" />
+                </el-form-item>
+                <el-form-item label="副标题名称">
+                  <el-input v-model="element.subtitle" placeholder="请输入标签名称" />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </template>
+          </draggable>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 5">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>发现</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <el-form label-width="86px" class="px-[10px]">
+          <draggable v-model="diyStore.editComponent.detectList" item-key="id">
+            <template #item="{ element }">
+              <div class="edit-attr-box">
+                <el-form-item label="图片上传">
+                  <div class="flex-row-start">
+                    <upload-image v-model="element.imageUrl" :limit="1" />
+                    <div class="flex-column-between images-bos">
+                      <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                      <div class="flex-row-between images-box">
+                        <div>缩放模式</div>
+                        <div class="flex-row-start" @click="openImageType(element, 3)">
+                          <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                            element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                          }}</span>
+                          <el-icon class="cursor-pointer">
+                            <ArrowRight />
+                          </el-icon>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </el-form-item>
+                <el-form-item label="发现名称">
+                  <el-input v-model="element.title" placeholder="请输入发现名称" />
+                </el-form-item>
+                <el-form-item label="副标题名称">
+                  <el-input v-model="element.subtitle" placeholder="请输入标签名称" />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </template>
+          </draggable>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 6">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>标签设置</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <draggable v-model="diyStore.editComponent.labelList" item-key="id">
+          <template #item="{ element, index }">
+            <el-form label-width="90px" class="px-[10px]">
+              <div class="edit-attr-box">
+                <el-icon @click="onDel(2, index)" color="#F56C6C" size="18px" class="circleClose">
+                  <CircleCloseFilled />
+                </el-icon>
+                <el-form-item label="导航名称">
+                  <el-input v-model="element.title" placeholder="请输入导航名称" :maxlength="5" show-word-limit />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </el-form>
+          </template>
+        </draggable>
+        <el-button @click="onAdd(2)" style="width: 100%; margin-top: 10px">新增标签</el-button>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.settings == 7">
+        <h3 class="mb-[10px]">选择商品</h3>
+        <el-form-item label="商品">
+          <div class="data-num" @click="openDialog">
+            <span v-if="diyStore.editComponent.goodsIds.length == 0">请选择</span>
+            <span v-else>已选择{{ diyStore.editComponent.goodsIds.length }}个</span>
+            <el-icon><ArrowRight /></el-icon>
+          </div>
+        </el-form-item>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">标题样式设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.titleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字加粗">
+            <el-radio-group size="small" v-model="diyStore.editComponent.titleWeight" fill="#409eff">
+              <el-radio-button label="加粗" :value="'bold'" />
+              <el-radio-button label="不加粗" :value="'normal'" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.titleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.titleColor" />
+            <el-button @click="diyStore.editComponent.titleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">副标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.subtitleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.subtitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.subtitleColor" />
+            <el-button @click="diyStore.editComponent.subtitleColor = '#364153'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">其它样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+          <el-form-item label="模块圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.boxRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+          <el-form-item label="模块主题">
+            <span class="mr-[10px]">{{ diyStore.editComponent.boxColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.boxColor" />
+            <el-button @click="diyStore.editComponent.boxColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择商品" width="1400">
+      <div class="dialog-bos">
+        <el-input v-model="queryParams.itemName" placeholder="请输入商品名称" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button :icon="Search" @click="handleQuery" />
+          </template>
+        </el-input>
+        <div class="flex">
+          <div class="tree-bos">
+            <el-tree :data="categoryOptions" :props="defaultProps" @node-click="handleNodeClick" :highlight-current="true" />
+          </div>
+          <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" />
+            <el-table-column label="商品图片" align="center" prop="productImage" width="100">
+              <template #default="scope">
+                <image-preview :src="scope.row.productImage" :width="60" :height="60" />
+              </template>
+            </el-table-column>
+            <el-table-column label="商品信息" align="center" minWidth="250" show-overflow-tooltip>
+              <template #default="scope">
+                <div class="text-left">
+                  <div>{{ scope.row.itemName }}</div>
+                  <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="SKU价格" align="center" width="180">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">市场价:</span>
+                    <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">会员价:</span>
+                    <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">最低价:</span>
+                    <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="成本情况" align="center" width="150">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">采购价:</span>
+                    <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">暂估毛利率:</span>
+                    <span>{{ scope.row.tempGrossMargin || '0.0000' }}%</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+import { categoryTree, listBase } from '@/api/pmsProduct/base';
+import type { TableInstance } from 'element-plus';
+import { Search } from '@element-plus/icons-vue';
+import { el } from 'element-plus/es/locale/index.mjs';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  itemName: '',
+  topCategoryId: '',
+  mediumCategoryId: '',
+  bottomCategoryId: ''
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+const categoryOptions = ref<any>([]);
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+};
+
+onMounted(() => {
+  getCategoryTree();
+});
+
+const onAdd = (res: any) => {
+  if (res == 1) {
+    //顶部导航
+    diyStore.editComponent.tabList.push({
+      title: '',
+      url: '',
+      id: Date.now()
+    });
+  } else if (res == 2) {
+    //标签
+    diyStore.editComponent.labelList.push({
+      title: '',
+      url: '',
+      id: Date.now()
+    });
+  }
+};
+
+const onDel = (res: any, index: any) => {
+  if (res == 1) {
+    //导航
+    diyStore.editComponent.tabList.splice(index, 1);
+  } else if (res == 2) {
+    //标签
+    diyStore.editComponent.labelList.splice(index, 1);
+  }
+};
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+
+// 图片类型返回
+const confirmCallBack = (res: any, type: any) => {
+  let list = [];
+  if (type == 1) diyStore.editComponent.imgType = res.imgType;
+  if (type == 2) list = diyStore.editComponent.planList;
+  if (type == 3) list = diyStore.editComponent.detectList;
+  list.forEach((item: any) => {
+    if (item.id == res.id) {
+      item.imgType = res.imgType;
+    }
+  });
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listBase(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.goodsIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 查询分类树 */
+const getCategoryTree = async () => {
+  categoryOptions.value = [];
+  const res = await categoryTree();
+  const list = res.data || [];
+  categoryOptions.value = [...list];
+  categoryOptions.value.unshift({
+    id: '',
+    label: '全部'
+  });
+};
+
+//打开弹窗
+const openDialog = () => {
+  showDialog.value = true;
+  getList();
+};
+
+const handleNodeClick = (data: any) => {
+  queryParams.topCategoryId = '';
+  queryParams.mediumCategoryId = '';
+  queryParams.bottomCategoryId = '';
+  if (data.parentId == 0) {
+    queryParams.topCategoryId = data.id;
+  } else if (data.children) {
+    queryParams.mediumCategoryId = data.id;
+  } else {
+    queryParams.bottomCategoryId = data.id;
+  }
+  handleQuery();
+};
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.goodsIds, tableData.value, multipleSelection.value);
+  if (newIds.length < 5) {
+    diyStore.editComponent.goodsIds = newIds;
+    showDialog.value = false;
+  } else {
+    ElMessage.error('最多只能选择4个商品');
+  }
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .data-num {
+      width: 100%;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--el-color-primary);
+      cursor: pointer;
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .annotation {
+    // position: absolute;
+    // bottom: 0;
+    // left: 0;
+    font-size: 12px;
+    color: #666;
+  }
+
+  .annotation2 {
+    font-size: 12px;
+    color: #666;
+    margin-left: 10px;
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 601 - 0
src/views/diy/pcEdit/floor-edit.vue

@@ -0,0 +1,601 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">风格设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="风格选择" class="flex">
+            <span class="text-primary flex-1 cursor-pointer" @click="openStyle">风格{{ diyStore.editComponent.styleType }}</span>
+            <el-icon @click="openStyle" class="cursor-pointer">
+              <ArrowRight />
+            </el-icon>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <div class="edit-attr-box">
+            <el-form-item label="图片上传">
+              <div class="flex-row-start">
+                <upload-image v-model="diyStore.editComponent.imageUrl" :limit="1" />
+                <div class="flex-column-between images-bos">
+                  <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸230*560)</div>
+                  <div class="flex-row-between images-box">
+                    <div>缩放模式</div>
+                    <div class="flex-row-start" @click="openImageType({ imgType: diyStore.editComponent.imgType }, 1)">
+                      <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                        diyStore.editComponent.imgType == 1 ? '拉伸' : diyStore.editComponent.imgType == 2 ? '缩放' : '填充'
+                      }}</span>
+                      <el-icon class="cursor-pointer">
+                        <ArrowRight />
+                      </el-icon>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-form-item>
+            <el-form-item label="链接地址">
+              <el-input v-model="diyStore.editComponent.imageUrl" placeholder="请输入链接地址" />
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap mt-[20px]">
+        <h3 class="mb-[10px]">商品设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="选择商品">
+            <div class="data-num" @click="openDialog">
+              <span v-if="diyStore.editComponent.goodsIds.length == 0">请选择</span>
+              <span v-else>已选择{{ diyStore.editComponent.goodsIds.length }}个</span>
+              <el-icon><ArrowRight /></el-icon>
+            </div>
+          </el-form-item>
+          <el-form-item label="显示内容">
+            <el-checkbox-group v-model="diyStore.editComponent.goodsShow">
+              <el-checkbox label="商品名称" :value="1" />
+              <el-checkbox label="销售价格" :value="2" />
+              <el-checkbox label="划线价格" :value="3" />
+            </el-checkbox-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">购买按钮</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="是否显示">
+            <el-switch v-model="diyStore.editComponent.btnShow" />
+          </el-form-item>
+          <el-form-item label="样式">
+            <div class="flex-row-start">
+              <div
+                @click="diyStore.editComponent.btnStyle = 1"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 1 }"
+              >
+                <div class="btn1">购买</div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 2"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 2 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <el-icon size="14"><Plus /></el-icon>
+                </div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 3"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 3 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <icon name="iconfont icongouwuche" size="14px" />
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item label="按钮文字">
+            <el-input v-model="diyStore.editComponent.btnText" placeholder="请输入按钮文字" :maxlength="4" show-word-limit />
+          </el-form-item>
+        </el-form>
+      </div>
+      <template v-if="diyStore.editComponent.styleType == 2">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">"更多"按钮</h3>
+          <el-form label-width="90px" class="px-[10px]">
+            <el-form-item label="按钮文字" class="flex">
+              <el-input v-model="diyStore.editComponent.moreTitle" placeholder="请输入按钮文字" />
+            </el-form-item>
+            <el-form-item label="链接地址" class="flex">
+              <el-input v-model="diyStore.editComponent.moreUrl" placeholder="请输入链接地址" />
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="edit-attr-item-wrap">
+          <div class="edit-attr-title flex-row-between">
+            <div>
+              <span>品牌设置</span>
+              <span class="title2">鼠标拖拽可以改变顺序</span>
+            </div>
+          </div>
+          <draggable v-model="diyStore.editComponent.brandList" item-key="id">
+            <template #item="{ element, index }">
+              <el-form label-width="90px" class="px-[10px]">
+                <div class="edit-attr-box">
+                  <el-icon @click="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                    <CircleCloseFilled />
+                  </el-icon>
+                  <el-form-item label="图片上传">
+                    <div class="flex-row-start">
+                      <upload-image v-model="element.imageUrl" :limit="1" />
+                      <div class="flex-column-between images-bos">
+                        <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                        <div class="flex-row-between images-box">
+                          <div>缩放模式</div>
+                          <div class="flex-row-start" @click="openImageType(element, 2)">
+                            <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                              element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                            }}</span>
+                            <el-icon class="cursor-pointer">
+                              <ArrowRight />
+                            </el-icon>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </el-form-item>
+                </div>
+              </el-form>
+            </template>
+          </draggable>
+          <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增品牌</el-button>
+        </div>
+      </template>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品按钮</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="按钮颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnColor" />
+            <el-button @click="diyStore.editComponent.btnColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="背景颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnbackgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnbackgroundColor" />
+            <el-button @click="diyStore.editComponent.btnbackgroundColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.styleType == 2">
+        <h3 class="mb-[10px]">"更多"按钮样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.moreColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.moreColor" />
+            <el-button @click="diyStore.editComponent.moreColor = '#b7bcd2'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="是否显示">
+            <el-switch v-model="diyStore.editComponent.moreShow" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <!-- 风格弹窗 -->
+    <el-dialog v-model="styleDialog" title="风格选择">
+      <div class="data-bos">
+        <template v-for="(item, index) in styleList" :key="index">
+          <div
+            :class="{ 'border-primary': styleId == item.id }"
+            @click="changeTitleStyle(item)"
+            class="data-list flex items-center justify-center overflow-hidden w-[200px] h-[100px] mr-[12px] mb-[12px] cursor-pointer border bg-[#eee]"
+          >
+            <img :src="item.img" />
+          </div>
+        </template>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="styleDialog = false">取消</el-button>
+          <el-button type="primary" @click="confirmTitleStyle">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择商品" width="1400">
+      <div class="dialog-bos">
+        <el-input v-model="queryParams.itemName" placeholder="请输入商品名称" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button :icon="Search" @click="handleQuery" />
+          </template>
+        </el-input>
+        <div class="flex">
+          <div class="tree-bos">
+            <el-tree :data="categoryOptions" :props="defaultProps" @node-click="handleNodeClick" :highlight-current="true" />
+          </div>
+          <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" />
+            <el-table-column label="商品图片" align="center" prop="productImage" width="100">
+              <template #default="scope">
+                <image-preview :src="scope.row.productImage" :width="60" :height="60" />
+              </template>
+            </el-table-column>
+            <el-table-column label="商品信息" align="center" minWidth="250" show-overflow-tooltip>
+              <template #default="scope">
+                <div class="text-left">
+                  <div>{{ scope.row.itemName }}</div>
+                  <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="SKU价格" align="center" width="180">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">市场价:</span>
+                    <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">会员价:</span>
+                    <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">最低价:</span>
+                    <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="成本情况" align="center" width="150">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">采购价:</span>
+                    <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">暂估毛利率:</span>
+                    <span>{{ scope.row.tempGrossMargin || '0.0000' }}%</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script setup lang="ts">
+import usePcdiyStore from '@/store/modules/pcdiy';
+import floor1 from '@/assets/images/pcdiy/floor1.png';
+import floor2 from '@/assets/images/pcdiy/floor2.png';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+import type { TableInstance } from 'element-plus';
+import { Search } from '@element-plus/icons-vue';
+import { categoryTree, listBase } from '@/api/pmsProduct/base';
+import draggable from 'vuedraggable';
+const styleList = [
+  {
+    img: floor1,
+    id: 1
+  },
+  {
+    img: floor2,
+    id: 2
+  }
+];
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+const styleDialog = ref(false);
+const styleId = ref<any>(1);
+
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const Brandloading = ref(false);
+const BrandList = ref<any>([]);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  itemName: '',
+  topCategoryId: '',
+  mediumCategoryId: '',
+  bottomCategoryId: ''
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+const categoryOptions = ref<any>([]);
+const categoryOptions1 = ref<any>([]);
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+};
+
+onMounted(() => {
+  getCategoryTree();
+});
+
+//打开风格弹窗
+const openStyle = () => {
+  styleDialog.value = true;
+  styleId.value = diyStore.editComponent.styleType;
+};
+
+//选择弹窗
+const changeTitleStyle = (item: any) => {
+  styleId.value = item.id;
+};
+
+//确定弹窗
+const confirmTitleStyle = () => {
+  diyStore.editComponent.styleType = styleId.value;
+  styleDialog.value = false;
+};
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+
+// 图片类型返回
+const confirmCallBack = (res: any, type: any) => {
+  if (type == 1) {
+    diyStore.editComponent.imgType = res.imgType;
+  } else {
+    diyStore.editComponent.brandList.forEach((item: any) => {
+      if (item.id == res.id) {
+        item.imgType = res.imgType;
+      }
+    });
+  }
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listBase(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.goodsIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 查询分类树 */
+const getCategoryTree = async () => {
+  categoryOptions.value = [];
+  const res = await categoryTree();
+  const list = res.data || [];
+  categoryOptions.value = [...list];
+  categoryOptions.value.unshift({
+    id: '',
+    label: '全部'
+  });
+};
+
+//打开弹窗
+const openDialog = () => {
+  showDialog.value = true;
+  getList();
+};
+
+const handleNodeClick = (data: any) => {
+  queryParams.topCategoryId = '';
+  queryParams.mediumCategoryId = '';
+  queryParams.bottomCategoryId = '';
+  if (data.parentId == 0) {
+    queryParams.topCategoryId = data.id;
+  } else if (data.children) {
+    queryParams.mediumCategoryId = data.id;
+  } else {
+    queryParams.bottomCategoryId = data.id;
+  }
+  handleQuery();
+};
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.goodsIds, tableData.value, multipleSelection.value);
+  diyStore.editComponent.goodsIds = newIds;
+  showDialog.value = false;
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+
+const onAdd = () => {
+  diyStore.editComponent.brandList.push({
+    imageUrl: '',
+    url: '',
+    imgType: 1,
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.brandList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+  }
+
+  .edit-attr-box {
+    padding: 18px 10px 0 10px;
+    border: 1px solid #e5e6eb;
+    border-radius: 4px;
+    position: relative;
+    margin-top: 18px;
+
+    .images-bos {
+      flex: 1;
+      height: 98px;
+      padding: 5px 0;
+    }
+
+    .images-box {
+      font-size: 13px;
+      color: #666;
+    }
+
+    .circleClose {
+      position: absolute;
+      top: -9px;
+      right: -9px;
+      cursor: pointer;
+    }
+
+    .annotation3 {
+      font-size: 12px;
+      color: #666;
+      line-height: 14px;
+    }
+  }
+
+  .btnStyle {
+    min-width: 60px;
+    padding: 5px;
+    cursor: pointer;
+    border-radius: 4px;
+    margin-right: 4px;
+
+    &.btnStyle1 {
+      border: 1px solid var(--el-color-primary);
+    }
+
+    .btn1 {
+      background-color: var(--el-color-primary);
+      padding: 5px 15px;
+      border-radius: 15px;
+      font-size: 12px;
+      color: #ffffff;
+      line-height: 1;
+    }
+
+    .btn2 {
+      color: var(--el-color-primary);
+      border: 1px solid var(--el-color-primary);
+      height: 26px;
+      width: 26px;
+      border-radius: 50%;
+    }
+  }
+
+  .data-num {
+    width: 100%;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    color: var(--el-color-primary);
+    cursor: pointer;
+  }
+
+  .data-bos {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0 12px;
+    .data-list {
+      background-color: #f9fafb;
+      border: 1px solid #e5e7eb;
+      &.border-primary {
+        border-color: var(--el-color-primary);
+      }
+      img {
+        width: 100%;
+      }
+    }
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 536 - 0
src/views/diy/pcEdit/goods-edit.vue

@@ -0,0 +1,536 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">样式设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="数据来源">
+            <el-radio-group v-model="diyStore.editComponent.styleType">
+              <el-radio :value="1">固定显示</el-radio>
+              <el-radio :value="2">单行滑动</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">购买按钮</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="是否显示">
+            <el-switch v-model="diyStore.editComponent.btnShow" />
+          </el-form-item>
+          <el-form-item label="样式">
+            <div class="flex-row-start">
+              <div
+                @click="diyStore.editComponent.btnStyle = 1"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 1 }"
+              >
+                <div class="btn1">购买</div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 2"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 2 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <el-icon size="14"><Plus /></el-icon>
+                </div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 3"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 3 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <icon name="iconfont icongouwuche" size="14px" />
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item label="按钮文字">
+            <el-input v-model="diyStore.editComponent.btnText" placeholder="请输入按钮文字" :maxlength="4" show-word-limit />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品数据</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="显示内容">
+            <el-checkbox-group v-model="diyStore.editComponent.goodsShow">
+              <el-checkbox label="商品名称" :value="1" />
+              <el-checkbox label="销售价格" :value="2" />
+              <el-checkbox label="划线价格" :value="3" />
+            </el-checkbox-group>
+          </el-form-item>
+          <el-form-item label="选择方式">
+            <el-radio-group v-model="diyStore.editComponent.goodsType">
+              <el-radio :value="1">指定商品</el-radio>
+              <el-radio :value="2">商品分类</el-radio>
+              <el-radio :value="3">商品品牌</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="指定商品" v-if="diyStore.editComponent.goodsType == 1">
+            <div class="data-num" @click="openDialog">
+              <span v-if="diyStore.editComponent.goodsIds.length == 0">请选择</span>
+              <span v-else>已选择{{ diyStore.editComponent.goodsIds.length }}个</span>
+              <el-icon><ArrowRight /></el-icon>
+            </div>
+          </el-form-item>
+          <el-form-item label="商品分类" v-if="diyStore.editComponent.goodsType == 2">
+            <el-tree-select
+              v-model="diyStore.editComponent.goodsClassify"
+              :data="categoryOptions1"
+              :props="treeProps"
+              value-key="id"
+              placeholder="请选择商品分类"
+              clearable
+              check-strictly
+              @change="goodsClassifyChange"
+            />
+          </el-form-item>
+          <el-form-item label="品牌" v-if="diyStore.editComponent.goodsType == 3">
+            <el-select
+              v-model="diyStore.editComponent.goodsBrand"
+              filterable
+              remote
+              reserve-keyword
+              placeholder="请输入品牌名称"
+              :remote-method="remoteMethod"
+              :loading="Brandloading"
+              style="width: 240px"
+            >
+              <el-option v-for="(item, index) in BrandList" :key="index" :label="item.brandName" :value="item.id" />
+              <template #loading>
+                <svg class="circular" viewBox="0 0 50 50">
+                  <circle class="path" cx="25" cy="25" r="20" fill="none" />
+                </svg>
+              </template>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="商品数量" v-if="diyStore.editComponent.goodsType != 1">
+            <div>
+              <el-input-number v-model="diyStore.editComponent.goodsNumber" :min="0" />
+              <div class="annotation3 mt-[5px]">(0代表不限)</div>
+            </div>
+          </el-form-item>
+          <el-form-item label="商品排序" v-if="diyStore.editComponent.goodsType != 1">
+            <el-radio-group v-model="diyStore.editComponent.goodsSort" fill="#409eff">
+              <el-radio-button label="综合" :value="1" />
+              <el-radio-button label="销量" :value="2" />
+              <el-radio-button label="价格" :value="3" />
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="商品背景">
+            <span class="mr-[10px]">{{ diyStore.editComponent.goodsbackgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsbackgroundColor" />
+            <el-button @click="diyStore.editComponent.goodsbackgroundColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="商品名称">
+            <el-radio-group v-model="diyStore.editComponent.goodsTitleType">
+              <el-radio :value="1">加粗</el-radio>
+              <el-radio :value="2">单行</el-radio>
+              <el-radio :value="3">多行</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="名称颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.goodsTitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsTitleColor" />
+            <el-button @click="diyStore.editComponent.goodsTitleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="销售价">
+            <span class="mr-[10px]">{{ diyStore.editComponent.priceColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.priceColor" />
+            <el-button @click="diyStore.editComponent.priceColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="上圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.goodstopRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="下圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.goodsbottomRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="购买按钮">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnColor" />
+            <el-button @click="diyStore.editComponent.btnColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="背景颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnbackgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnbackgroundColor" />
+            <el-button @click="diyStore.editComponent.btnbackgroundColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择商品" width="1400">
+      <div class="dialog-bos">
+        <el-input v-model="queryParams.itemName" placeholder="请输入商品名称" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button :icon="Search" @click="handleQuery" />
+          </template>
+        </el-input>
+        <div class="flex">
+          <div class="tree-bos">
+            <el-tree :data="categoryOptions" :props="defaultProps" @node-click="handleNodeClick" :highlight-current="true" />
+          </div>
+          <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" />
+            <el-table-column label="商品图片" align="center" prop="productImage" width="100">
+              <template #default="scope">
+                <image-preview :src="scope.row.productImage" :width="60" :height="60" />
+              </template>
+            </el-table-column>
+            <el-table-column label="商品信息" align="center" minWidth="250" show-overflow-tooltip>
+              <template #default="scope">
+                <div class="text-left">
+                  <div>{{ scope.row.itemName }}</div>
+                  <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="SKU价格" align="center" width="180">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">市场价:</span>
+                    <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">会员价:</span>
+                    <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">最低价:</span>
+                    <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="成本情况" align="center" width="150">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">采购价:</span>
+                    <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">暂估毛利率:</span>
+                    <span>{{ scope.row.tempGrossMargin || '0.0000' }}%</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { categoryTree, listBase } from '@/api/pmsProduct/base';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { Search } from '@element-plus/icons-vue';
+import type { TableInstance } from 'element-plus';
+import { listBrand } from '@/api/product/brand';
+const diyStore = usePcdiyStore();
+
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const Brandloading = ref(false);
+const BrandList = ref<any>([]);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  itemName: '',
+  topCategoryId: '',
+  mediumCategoryId: '',
+  bottomCategoryId: ''
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+const categoryOptions = ref<any>([]);
+const categoryOptions1 = ref<any>([]);
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+};
+const treeProps = {
+  value: 'id',
+  label: 'label',
+  children: 'children'
+};
+
+onMounted(() => {
+  getCategoryTree();
+});
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listBase(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.goodsIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 查询分类树 */
+const getCategoryTree = async () => {
+  categoryOptions.value = [];
+  categoryOptions1.value = [];
+  const res = await categoryTree();
+  const list = res.data || [];
+  categoryOptions.value = [...list];
+  categoryOptions1.value = [...list];
+  categoryOptions.value.unshift({
+    id: '',
+    label: '全部'
+  });
+};
+
+//打开弹窗
+const openDialog = () => {
+  showDialog.value = true;
+  getList();
+};
+
+const handleNodeClick = (data: any) => {
+  queryParams.topCategoryId = '';
+  queryParams.mediumCategoryId = '';
+  queryParams.bottomCategoryId = '';
+  if (data.parentId == 0) {
+    queryParams.topCategoryId = data.id;
+  } else if (data.children) {
+    queryParams.mediumCategoryId = data.id;
+  } else {
+    queryParams.bottomCategoryId = data.id;
+  }
+  handleQuery();
+};
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.goodsIds, tableData.value, multipleSelection.value);
+  diyStore.editComponent.goodsIds = newIds;
+  showDialog.value = false;
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+
+//选择商品分类
+const goodsClassifyChange = (res: any) => {
+  const foundNode = findNodeByKey(categoryOptions1.value, res);
+  diyStore.editComponent.goodsClassify = res;
+  diyStore.editComponent.topCategoryId = '';
+  diyStore.editComponent.mediumCategoryId = '';
+  diyStore.editComponent.bottomCategoryId = '';
+  if (foundNode.parentId == 0) {
+    diyStore.editComponent.topCategoryId = foundNode.id;
+  } else if (foundNode.children) {
+    diyStore.editComponent.mediumCategoryId = foundNode.id;
+  } else {
+    diyStore.editComponent.bottomCategoryId = foundNode.id;
+  }
+};
+
+// 递归查找节点的辅助函数
+const findNodeByKey = (nodes: any, key: any) => {
+  for (const node of nodes) {
+    if (node.id === key) {
+      return node;
+    }
+    if (node.children) {
+      const found = findNodeByKey(node.children, key);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+//品牌输入
+const remoteMethod = (query: string) => {
+  if (query) {
+    Brandloading.value = true;
+    listBrand({ pageNum: 1, pageSize: 100, brandName: query }).then((res) => {
+      Brandloading.value = false;
+      if (res.code == 200) {
+        BrandList.value = res.rows || [];
+      }
+    });
+    console.log(query);
+    // setTimeout(() => {
+    //   Brandloading.value = false
+    //   options.value = list.value.filter((item) => {
+    //     return item.label.toLowerCase().includes(query.toLowerCase())
+    //   })
+    // }, 3000)
+  } else {
+    BrandList.value = [];
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .btnStyle {
+      min-width: 60px;
+      padding: 5px;
+      cursor: pointer;
+      border-radius: 4px;
+      margin-right: 4px;
+
+      &.btnStyle1 {
+        border: 1px solid var(--el-color-primary);
+      }
+
+      .btn1 {
+        background-color: var(--el-color-primary);
+        padding: 5px 15px;
+        border-radius: 15px;
+        font-size: 12px;
+        color: #ffffff;
+        line-height: 1;
+      }
+
+      .btn2 {
+        color: var(--el-color-primary);
+        border: 1px solid var(--el-color-primary);
+        height: 26px;
+        width: 26px;
+        border-radius: 50%;
+      }
+    }
+
+    .data-num {
+      width: 100%;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--el-color-primary);
+      cursor: pointer;
+    }
+  }
+
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  .dialog-bos {
+    .tree-bos {
+      max-height: 900px;
+      overflow: auto;
+      width: 240px;
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 583 - 0
src/views/diy/pcEdit/goodsList-edit.vue

@@ -0,0 +1,583 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">购买按钮</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="是否显示">
+            <el-switch v-model="diyStore.editComponent.btnShow" />
+          </el-form-item>
+          <el-form-item label="样式">
+            <div class="flex-row-start">
+              <div
+                @click="diyStore.editComponent.btnStyle = 1"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 1 }"
+              >
+                <div class="btn1">购买</div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 2"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 2 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <el-icon size="14"><Plus /></el-icon>
+                </div>
+              </div>
+              <div
+                @click="diyStore.editComponent.btnStyle = 3"
+                class="btnStyle flex-row-center"
+                :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 3 }"
+              >
+                <div class="btn2 flex-row-center">
+                  <icon name="iconfont icongouwuche" size="14px" />
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item label="按钮文字">
+            <el-input v-model="diyStore.editComponent.btnText" placeholder="请输入按钮文字" :maxlength="4" show-word-limit />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品数据</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="显示内容">
+            <el-checkbox-group v-model="diyStore.editComponent.goodsShow">
+              <el-checkbox label="商品名称" :value="1" />
+              <el-checkbox label="销售价格" :value="2" />
+              <el-checkbox label="划线价格" :value="3" />
+            </el-checkbox-group>
+          </el-form-item>
+          <el-form-item label="商品数量">
+            <div>
+              <el-input-number v-model="diyStore.editComponent.goodsNumber" :min="0" />
+              <div class="annotation3 mt-[5px]">(0代表不限)</div>
+            </div>
+          </el-form-item>
+          <el-form-item label="商品排序">
+            <el-radio-group v-model="diyStore.editComponent.goodsSort" fill="#409eff">
+              <el-radio-button label="综合" :value="1" />
+              <el-radio-button label="销量" :value="2" />
+              <el-radio-button label="价格" :value="3" />
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>选项卡</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <el-form label-width="80px" class="px-[10px]">
+          <draggable v-model="diyStore.editComponent.tabList" item-key="id">
+            <template #item="{ element, index }">
+              <div class="edit-attr-box" @click="diyStore.editComponent.tabIndex = index">
+                <el-icon @click.stop="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                  <CircleCloseFilled />
+                </el-icon>
+                <el-form-item label="名称">
+                  <el-input v-model="element.title" placeholder="请输入选项卡名称" :maxlength="10" show-word-limit />
+                </el-form-item>
+                <el-form-item label="选择方式">
+                  <el-radio-group v-model="element.goodsType">
+                    <el-radio :value="1">指定商品</el-radio>
+                    <el-radio :value="2">商品分类</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+                <el-form-item label="指定商品" v-if="element.goodsType == 1">
+                  <div class="data-num">
+                    <span @click="openDialog(index)" v-if="element.goodsIds.length == 0">请选择</span>
+                    <span @click="openDialog(index)" v-else>已选择{{ element.goodsIds.length }}个</span>
+                    <el-icon><ArrowRight /></el-icon>
+                  </div>
+                </el-form-item>
+                <el-form-item label="商品分类" v-if="element.goodsType == 2">
+                  <el-tree-select
+                    v-model="element.goodsClassify"
+                    :data="categoryOptions1"
+                    :props="treeProps"
+                    value-key="id"
+                    placeholder="请选择商品分类"
+                    clearable
+                    check-strictly
+                    @change="(res: any) => goodsClassifyChange(res, element)"
+                  />
+                </el-form-item>
+              </div>
+            </template>
+          </draggable>
+          <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增选项卡</el-button>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">选项卡样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="背景">
+            <span class="mr-[10px]">{{ diyStore.editComponent.tabbackgroundColor1 }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabbackgroundColor1" />
+            <el-button @click="diyStore.editComponent.tabbackgroundColor1 = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="选中背景">
+            <span class="mr-[10px]">{{ diyStore.editComponent.tabbackgroundColor2 }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabbackgroundColor2" />
+            <el-button @click="diyStore.editComponent.tabbackgroundColor2 = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.tabColor1 }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabColor1" />
+            <el-button @click="diyStore.editComponent.tabColor1 = '#333333'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="选中文字">
+            <span class="mr-[10px]">{{ diyStore.editComponent.tabColor2 }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabColor2" />
+            <el-button @click="diyStore.editComponent.tabColor2 = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.tabRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="商品背景">
+            <span class="mr-[10px]">{{ diyStore.editComponent.goodsbackgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsbackgroundColor" />
+            <el-button @click="diyStore.editComponent.goodsbackgroundColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="商品名称">
+            <el-radio-group v-model="diyStore.editComponent.goodsTitleType">
+              <el-radio :value="1">加粗</el-radio>
+              <el-radio :value="2">单行</el-radio>
+              <el-radio :value="3">多行</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="名称颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.goodsTitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsTitleColor" />
+            <el-button @click="diyStore.editComponent.goodsTitleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="销售价">
+            <span class="mr-[10px]">{{ diyStore.editComponent.priceColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.priceColor" />
+            <el-button @click="diyStore.editComponent.priceColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="上圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.goodstopRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="下圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.goodsbottomRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">商品样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="购买按钮">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnColor" />
+            <el-button @click="diyStore.editComponent.btnColor = '#ffffff'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="背景颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.btnbackgroundColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnbackgroundColor" />
+            <el-button @click="diyStore.editComponent.btnbackgroundColor = '#E7000B'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择商品" width="1400">
+      <div class="dialog-bos">
+        <el-input v-model="queryParams.itemName" placeholder="请输入商品名称" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button :icon="Search" @click="handleQuery" />
+          </template>
+        </el-input>
+        <div class="flex">
+          <div class="tree-bos">
+            <el-tree :data="categoryOptions" :props="defaultProps" @node-click="handleNodeClick" :highlight-current="true" />
+          </div>
+          <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" />
+            <el-table-column label="商品图片" align="center" prop="productImage" width="100">
+              <template #default="scope">
+                <image-preview :src="scope.row.productImage" :width="60" :height="60" />
+              </template>
+            </el-table-column>
+            <el-table-column label="商品信息" align="center" minWidth="250" show-overflow-tooltip>
+              <template #default="scope">
+                <div class="text-left">
+                  <div>{{ scope.row.itemName }}</div>
+                  <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="SKU价格" align="center" width="180">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">市场价:</span>
+                    <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">会员价:</span>
+                    <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">最低价:</span>
+                    <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="成本情况" align="center" width="150">
+              <template #default="scope">
+                <div class="text-left" style="font-size: 12px">
+                  <div>
+                    <span class="text-gray-500">采购价:</span>
+                    <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
+                  </div>
+                  <div>
+                    <span class="text-gray-500">暂估毛利率:</span>
+                    <span>{{ scope.row.tempGrossMargin || '0.0000' }}%</span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import { categoryTree, listBase } from '@/api/pmsProduct/base';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { Search } from '@element-plus/icons-vue';
+import type { TableInstance } from 'element-plus';
+const diyStore = usePcdiyStore();
+
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  itemName: '',
+  topCategoryId: '',
+  mediumCategoryId: '',
+  bottomCategoryId: ''
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+const categoryOptions = ref<any>([]);
+const categoryOptions1 = ref<any>([]);
+const navIndex = ref<any>(0);
+const defaultProps = {
+  children: 'children',
+  label: 'label'
+};
+const treeProps = {
+  value: 'id',
+  label: 'label',
+  children: 'children'
+};
+
+onMounted(() => {
+  getCategoryTree();
+});
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listBase(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.tabList[navIndex.value].goodsIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 查询分类树 */
+const getCategoryTree = async () => {
+  categoryOptions.value = [];
+  categoryOptions1.value = [];
+  const res = await categoryTree();
+  const list = res.data || [];
+  categoryOptions.value = [...list];
+  categoryOptions1.value = [...list];
+  categoryOptions.value.unshift({
+    id: '',
+    label: '全部'
+  });
+};
+
+//打开弹窗
+const openDialog = (res: any) => {
+  navIndex.value = res;
+  showDialog.value = true;
+  getList();
+};
+
+const handleNodeClick = (data: any) => {
+  queryParams.topCategoryId = '';
+  queryParams.mediumCategoryId = '';
+  queryParams.bottomCategoryId = '';
+  if (data.parentId == 0) {
+    queryParams.topCategoryId = data.id;
+  } else if (data.children) {
+    queryParams.mediumCategoryId = data.id;
+  } else {
+    queryParams.bottomCategoryId = data.id;
+  }
+  handleQuery();
+};
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.tabList[navIndex.value].goodsIds, tableData.value, multipleSelection.value);
+  diyStore.editComponent.tabList[navIndex.value].goodsIds = newIds;
+  showDialog.value = false;
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+
+//选择商品分类
+const goodsClassifyChange = (res: any, element: any) => {
+  const foundNode = findNodeByKey(categoryOptions1.value, res);
+  element.goodsClassify = res;
+  element.topCategoryId = '';
+  element.mediumCategoryId = '';
+  element.bottomCategoryId = '';
+  if (foundNode.parentId == 0) {
+    element.topCategoryId = foundNode.id;
+  } else if (foundNode.children) {
+    element.mediumCategoryId = foundNode.id;
+  } else {
+    element.bottomCategoryId = foundNode.id;
+  }
+};
+
+// 递归查找节点的辅助函数
+const findNodeByKey = (nodes: any, key: any) => {
+  for (const node of nodes) {
+    if (node.id === key) {
+      return node;
+    }
+    if (node.children) {
+      const found = findNodeByKey(node.children, key);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+const onAdd = () => {
+  diyStore.editComponent.tabList.push({
+    title: '',
+    goodsType: 1,
+    goodsIds: [],
+    goodsClassify: '',
+    topCategoryId: '',
+    mediumCategoryId: '',
+    bottomCategoryId: '',
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.tabList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+
+      .annotation3 {
+        font-size: 12px;
+        color: #666;
+        line-height: 14px;
+      }
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .btnStyle {
+      min-width: 60px;
+      padding: 5px;
+      cursor: pointer;
+      border-radius: 4px;
+      margin-right: 4px;
+
+      &.btnStyle1 {
+        border: 1px solid var(--el-color-primary);
+      }
+
+      .btn1 {
+        background-color: var(--el-color-primary);
+        padding: 5px 15px;
+        border-radius: 15px;
+        font-size: 12px;
+        color: #ffffff;
+        line-height: 1;
+      }
+
+      .btn2 {
+        color: var(--el-color-primary);
+        border: 1px solid var(--el-color-primary);
+        height: 26px;
+        width: 26px;
+        border-radius: 50%;
+      }
+    }
+
+    .data-num {
+      width: 100%;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--el-color-primary);
+      cursor: pointer;
+    }
+  }
+
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  .dialog-bos {
+    .tree-bos {
+      max-height: 900px;
+      overflow: auto;
+      width: 240px;
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 640 - 0
src/views/diy/pcEdit/head-edit.vue

@@ -0,0 +1,640 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">头部组件</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="设置">
+            <el-radio-group v-model="diyStore.editComponent.settings" fill="#409eff">
+              <el-radio-button label="顶部" :value="1" />
+              <el-radio-button label="左侧" :value="2" />
+              <el-radio-button label="中间" :value="3" />
+              <el-radio-button label="右侧" :value="4" />
+            </el-radio-group>
+          </el-form-item>
+          <!-- <el-form-item label="轮播区域">
+            <el-radio-group v-model="diyStore.editComponent.carousel">
+              <el-radio :value="1">仅轮播图</el-radio>
+              <el-radio :value="2">内嵌广告</el-radio>
+            </el-radio-group>
+          </el-form-item> -->
+        </el-form>
+      </div>
+      <template v-if="diyStore.editComponent.settings == 1">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">顶部设置</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <el-form-item label="样式选择">
+              <el-radio-group v-model="diyStore.editComponent.topStyle">
+                <el-radio :value="1">样式1</el-radio>
+                <el-radio :value="2">样式2</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="logo">
+              <div>
+                <upload-image v-model="diyStore.editComponent.logo" :limit="1" />
+                <div class="annotation">建议尺寸:宽度{{ diyStore.editComponent.topStyle == 1 ? '185' : '390' }}px*90px</div>
+              </div>
+            </el-form-item>
+            <el-form-item label="二维码">
+              <div>
+                <upload-image v-model="diyStore.editComponent.code" :limit="1" />
+                <div class="annotation">建议尺寸:宽度100px*100px</div>
+              </div>
+            </el-form-item>
+            <el-form-item label="全部商品分类" label-width="120">
+              <el-checkbox v-model="diyStore.editComponent.classifyShow" label="显示" />
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="edit-attr-item-wrap">
+          <div class="edit-attr-title flex-row-between">
+            <div>
+              <span>导航栏</span>
+              <span class="title2">鼠标拖拽可以改变顺序</span>
+            </div>
+
+            <el-radio-group v-model="diyStore.editComponent.topType" fill="#409eff">
+              <el-radio-button label="顶部导航" :value="1" />
+              <el-radio-button label="标签" :value="2" />
+            </el-radio-group>
+          </div>
+          <el-form label-width="80px" class="px-[10px]">
+            <template v-if="diyStore.editComponent.topType == 1">
+              <draggable v-model="diyStore.editComponent.topNav" item-key="id">
+                <template #item="{ element, index }">
+                  <div class="edit-attr-box">
+                    <el-icon @click="onDel(1, index)" color="#F56C6C" size="18px" class="circleClose">
+                      <CircleCloseFilled />
+                    </el-icon>
+                    <el-form-item label="导航名称">
+                      <el-input v-model="element.title" placeholder="请输入导航名称" />
+                    </el-form-item>
+                    <el-form-item label="链接地址">
+                      <el-input v-model="element.url" placeholder="请输入链接地址" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </draggable>
+              <el-button @click="onAdd(1)" style="width: 100%; margin-top: 10px">新增顶部导航</el-button>
+            </template>
+            <template v-if="diyStore.editComponent.topType == 2">
+              <draggable v-model="diyStore.editComponent.toplabel" item-key="id">
+                <template #item="{ element, index }">
+                  <div class="edit-attr-box">
+                    <el-icon @click="onDel(2, index)" color="#F56C6C" size="18px" class="circleClose">
+                      <CircleCloseFilled />
+                    </el-icon>
+                    <el-form-item label="标签名称">
+                      <el-input v-model="element.title" placeholder="请输入标签名称" />
+                    </el-form-item>
+                    <el-form-item label="链接地址">
+                      <el-input v-model="element.url" placeholder="请输入链接地址" />
+                    </el-form-item>
+                  </div>
+                </template>
+              </draggable>
+              <el-button @click="onAdd(2)" style="width: 100%; margin-top: 10px">新增标签</el-button>
+            </template>
+          </el-form>
+        </div>
+      </template>
+      <template v-if="diyStore.editComponent.settings == 2">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">左侧导航设置</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <el-form-item label="样式选择">
+              <el-radio-group v-model="diyStore.editComponent.leftStyle">
+                <el-radio :value="1">样式1</el-radio>
+                <el-radio :value="2">样式2</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+      <template v-if="diyStore.editComponent.settings == 3">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">中间轮播图</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <el-form-item label="背景全屏">
+              <el-checkbox v-model="diyStore.editComponent.carouselStyle" label="开启" />
+              <span class="annotation2">(开启后轮播图背景随轮播图变化)</span>
+            </el-form-item>
+            <el-form-item label="轮播左右键" label-width="120">
+              <el-radio-group v-model="diyStore.editComponent.carouselType">
+                <el-radio :value="1">圆形</el-radio>
+                <el-radio :value="2">方形</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="edit-attr-item-wrap">
+          <div class="edit-attr-title flex-row-between">
+            <div>
+              <span>广告图片</span>
+              <span class="title2">鼠标拖拽可以改变顺序</span>
+            </div>
+            <el-radio-group v-model="diyStore.editComponent.centreType" fill="#409eff">
+              <el-radio-button label="轮播图" :value="1" />
+              <el-radio-button label="内嵌广告" :value="2" />
+            </el-radio-group>
+          </div>
+          <template v-if="diyStore.editComponent.centreType == 1">
+            <draggable v-model="diyStore.editComponent.carouselList" item-key="id">
+              <template #item="{ element, index }">
+                <div class="edit-attr-box">
+                  <el-icon @click="onDel(3, index)" color="#F56C6C" size="18px" class="circleClose">
+                    <CircleCloseFilled />
+                  </el-icon>
+                  <el-form-item label="图片上传">
+                    <div class="flex-row-start">
+                      <upload-image v-model="element.imageUrl" :limit="1" />
+                      <div class="flex-column-between images-bos">
+                        <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸756*{{ diyStore.editComponent.advertNum == 0 ? '540' : '400' }})</div>
+                        <div class="flex-row-between images-box">
+                          <div>缩放模式</div>
+                          <div class="flex-row-start" @click="openImageType(element, 1)">
+                            <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                              element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                            }}</span>
+                            <el-icon class="cursor-pointer">
+                              <ArrowRight />
+                            </el-icon>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </el-form-item>
+                  <el-form-item label="链接地址">
+                    <el-input v-model="element.url" placeholder="请输入链接地址" />
+                  </el-form-item>
+                  <el-form-item label="是否显示">
+                    <el-switch v-model="element.show" />
+                  </el-form-item>
+                </div>
+              </template>
+            </draggable>
+            <el-button @click="onAdd(3)" style="width: 100%; margin-top: 10px">新增轮播图</el-button>
+          </template>
+          <template v-if="diyStore.editComponent.centreType == 2">
+            <el-form label-width="80px" class="px-[10px]">
+              <el-form-item label="广告数量" style="margin-top: 10px">
+                <el-radio-group @change="onAdvertNum" v-model="diyStore.editComponent.advertNum">
+                  <el-radio :value="0">0张</el-radio>
+                  <el-radio :value="1">1张</el-radio>
+                  <el-radio :value="2">2张</el-radio>
+                  <el-radio :value="3">3张</el-radio>
+                  <el-radio :value="4">4张</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-form>
+            <draggable v-model="diyStore.editComponent.advertList" item-key="id">
+              <template #item="{ element }">
+                <div class="edit-attr-box" v-show="element.show">
+                  <el-form-item label="图片上传">
+                    <div class="flex-row-start">
+                      <upload-image v-model="element.imageUrl" :limit="1" />
+                      <div class="flex-column-between images-bos">
+                        <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸{{ Math.floor(756 / diyStore.editComponent.advertNum) }}*130)</div>
+                        <div class="flex-row-between images-box">
+                          <div>缩放模式</div>
+                          <div class="flex-row-start" @click="openImageType(element, 2)">
+                            <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                              element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                            }}</span>
+                            <el-icon class="cursor-pointer">
+                              <ArrowRight />
+                            </el-icon>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </el-form-item>
+                  <el-form-item label="链接地址">
+                    <el-input v-model="element.url" placeholder="请输入链接地址" />
+                  </el-form-item>
+                </div>
+              </template>
+            </draggable>
+          </template>
+        </div>
+      </template>
+      <template v-if="diyStore.editComponent.settings == 4">
+        <div class="edit-attr-item-wrap">
+          <h3 class="mb-[10px]">右侧设置</h3>
+          <el-form label-width="80px" class="px-[10px]">
+            <el-form-item label="资讯位置">
+              <el-radio-group v-model="diyStore.editComponent.realType">
+                <el-radio :value="1">在上</el-radio>
+                <el-radio :value="2">在下</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="数据来源">
+              <el-radio-group v-model="diyStore.editComponent.realDataType">
+                <el-radio :value="1">默认</el-radio>
+                <el-radio :value="2">手动</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="文章数量" v-if="diyStore.editComponent.realDataType == 1">
+              <el-slider v-model="diyStore.editComponent.realNumber" show-input :min="1" :max="10" />
+            </el-form-item>
+            <el-form-item label="手动选择" v-else>
+              <div class="data-num" @click="openDialog">
+                <span v-if="diyStore.editComponent.realIds.length == 0">请选择</span>
+                <span v-else>已选择{{ diyStore.editComponent.realIds.length }}个</span>
+                <el-icon><ArrowRight /></el-icon>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="edit-attr-item-wrap">
+          <div class="edit-attr-title flex-row-between">
+            <div>
+              <span>导航设置</span>
+              <span class="title2">鼠标拖拽可以改变顺序</span>
+            </div>
+          </div>
+          <el-form label-width="80px" class="px-[10px]">
+            <draggable v-model="diyStore.editComponent.navlList" item-key="id">
+              <template #item="{ element, index }">
+                <div class="edit-attr-box">
+                  <el-icon @click="onDel(4, index)" color="#F56C6C" size="18px" class="circleClose">
+                    <CircleCloseFilled />
+                  </el-icon>
+                  <el-form-item label="图片上传">
+                    <div class="flex-row-start">
+                      <upload-image v-model="element.imageUrl" :limit="1" />
+                      <div class="flex-column-between images-bos">
+                        <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                        <div class="flex-row-between images-box">
+                          <div>缩放模式</div>
+                          <div class="flex-row-start" @click="openImageType(element, 3)">
+                            <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                              element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                            }}</span>
+                            <el-icon class="cursor-pointer">
+                              <ArrowRight />
+                            </el-icon>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </el-form-item>
+                  <el-form-item label="标签名称">
+                    <el-input v-model="element.title" placeholder="请输入标签名称" :maxlength="4" show-word-limit />
+                  </el-form-item>
+                  <el-form-item label="链接地址">
+                    <el-input v-model="element.url" placeholder="请输入链接地址" />
+                  </el-form-item>
+                </div>
+              </template>
+            </draggable>
+            <el-button @click="onAdd(4)" style="width: 100%; margin-top: 10px">新增导航</el-button>
+          </el-form>
+        </div>
+      </template>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">轮播区域设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="切换间隔ms" label-width="100px">
+            <el-slider v-model="diyStore.editComponent.carouselInterval" show-input :min="1000" :max="6000" />
+          </el-form-item>
+          <el-form-item label="图片圆角">
+            <el-slider v-model="diyStore.editComponent.carouselRadius" show-input :min="0" :max="100" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">左侧区域设置</h3>
+        <el-form class="px-[10px]">
+          <el-row>
+            <el-col :span="12">
+              <el-form-item label="主文字颜色">
+                <el-color-picker v-model="diyStore.editComponent.leftColor1" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="背景颜色">
+                <el-color-picker v-model="diyStore.editComponent.leftBackground" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="副文字颜色">
+                <el-color-picker v-model="diyStore.editComponent.leftColor2" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">右侧区域导航</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider v-model="diyStore.editComponent.rightRadius" show-input :min="0" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+
+    <!-- 手动选择 -->
+    <el-dialog v-model="showDialog" title="选择资讯">
+      <div class="data-bos">
+        <el-input v-model="queryParams.announcementTitle" placeholder="请输入资讯标题" clearable style="width: 300px; margin-bottom: 10px">
+          <template #append>
+            <el-button @click="handleQuery" :icon="Search" />
+          </template>
+        </el-input>
+        <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" />
+          <el-table-column label="标题" prop="announcementTitle" align="center" min-width="200" show-overflow-tooltip />
+          <el-table-column label="发布时间" align="center" prop="createTime" width="180" />
+        </el-table>
+        <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
+          <template #slotDiv>
+            <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
+          </template>
+        </pagination>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="onConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+import { listAnnouncement } from '@/api/system/announcement';
+import type { TableInstance } from 'element-plus';
+import { Search } from '@element-plus/icons-vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+const multipleTableRef = ref<TableInstance>();
+const showDialog = ref(false);
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const multipleSelection: any = ref([]); // 选中数据
+const total = ref(0);
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  announcementTitle: ''
+});
+const resultList = ref<any>([]); //单页之前被选中的数据
+
+const onAdd = (res: any) => {
+  if (res == 1) {
+    //顶部导航
+    diyStore.editComponent.topNav.push({
+      title: '',
+      url: ''
+    });
+  } else if (res == 2) {
+    //标签
+    diyStore.editComponent.toplabel.push({
+      title: '',
+      url: ''
+    });
+  } else if (res == 3) {
+    //轮播图
+    diyStore.editComponent.carouselList.push({
+      imageUrl: '',
+      url: '',
+      imgType: 1,
+      show: true,
+      id: Date.now()
+    });
+  } else if (res == 4) {
+    //导航
+    diyStore.editComponent.navlList.push({
+      imageUrl: '',
+      title: '',
+      url: '',
+      imgType: 1,
+      id: Date.now()
+    });
+  }
+};
+
+const onDel = (res: any, index: any) => {
+  if (res == 1) {
+    //顶部导航
+    diyStore.editComponent.topNav.splice(index, 1);
+  } else if (res == 2) {
+    //标签
+    diyStore.editComponent.toplabel.splice(index, 1);
+  } else if (res == 3) {
+    //轮播图
+    diyStore.editComponent.carouselList.splice(index, 1);
+  } else if (res == 4) {
+    //导航
+    diyStore.editComponent.navlList.splice(index, 1);
+  }
+};
+
+const onAdvertNum = (res: any) => {
+  diyStore.editComponent.advertList.forEach((item: any, index: any) => {
+    item.show = false;
+    if (index < res) {
+      item.show = true;
+    }
+  });
+};
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+
+// 图片类型返回
+const confirmCallBack = (res: any, type: any) => {
+  let list = [];
+  if (type == 1) list = diyStore.editComponent.carouselList;
+  if (type == 2) list = diyStore.editComponent.advertList;
+  if (type == 3) list = diyStore.editComponent.navlList;
+  list.forEach((item: any) => {
+    if (item.id == res.id) {
+      item.imgType = res.imgType;
+    }
+  });
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/** 获取列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listAnnouncement(queryParams);
+    tableData.value = res.rows || [];
+    const result = tableData.value.filter((item: any) => diyStore.editComponent.realIds.includes(item.id));
+    resultList.value = result;
+    nextTick(() => {
+      result.forEach((item: any) => {
+        multipleTableRef.value?.toggleRowSelection(item, true);
+      });
+    });
+
+    console.log('result', result);
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+//打开弹窗
+const openDialog = () => {
+  showDialog.value = true;
+  getList();
+};
+
+// 监听表格单行选中
+const handleSelectionChange = (val: []) => {
+  multipleSelection.value = val;
+};
+//确定
+const onConfirm = () => {
+  const newIds = calculateNewIds(diyStore.editComponent.realIds, tableData.value, multipleSelection.value);
+  diyStore.editComponent.realIds = newIds;
+  showDialog.value = false;
+};
+const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
+  // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
+  const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
+  // 2. 获取最终选中项的 ID 集合
+  const selectedIdSet = new Set(selectedItems.map((item) => item.id));
+  // 3. 过滤旧的缓存 IDs
+  const retainedOldIds = cacheIds.filter((id) => {
+    // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
+    if (!currentPageIdSet.has(id)) {
+      return true;
+    }
+    // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
+    if (selectedIdSet.has(id)) {
+      return true;
+    }
+    // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
+    // 返回 false,将其剔除
+    return false;
+  });
+  // 4. 合并:保留的旧数据 + 当前页新选中的数据
+  // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
+  const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
+  // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
+  // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
+  return Array.from(newIdsSet).sort((a, b) => a - b);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .data-num {
+      width: 100%;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--el-color-primary);
+      cursor: pointer;
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .annotation {
+    // position: absolute;
+    // bottom: 0;
+    // left: 0;
+    font-size: 12px;
+    color: #666;
+  }
+
+  .annotation2 {
+    font-size: 12px;
+    color: #666;
+    margin-left: 10px;
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 168 - 0
src/views/diy/pcEdit/imageCube-edit.vue

@@ -0,0 +1,168 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">魔方设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="一行数量">
+            <el-radio-group v-model="diyStore.editComponent.number">
+              <el-radio :value="1">1张</el-radio>
+              <el-radio :value="2">2张</el-radio>
+              <el-radio :value="3">3张</el-radio>
+              <el-radio :value="4">4张</el-radio>
+              <el-radio :value="5">5张</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="图片高度">
+            <el-input-number v-model="diyStore.editComponent.imageHeight" :min="1" :max="300">
+              <template #suffix>
+                <span>px</span>
+              </template>
+            </el-input-number>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>魔方布局</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <draggable v-model="diyStore.editComponent.imagelList" item-key="id">
+          <template #item="{ element, index }">
+            <div class="edit-attr-box" v-show="index < diyStore.editComponent.number">
+              <el-form-item label="图片上传">
+                <div class="flex-row-start">
+                  <upload-image v-model="element.imageUrl" :limit="1" />
+                  <div class="flex-column-between images-bos">
+                    <div class="annotation3">
+                      (建议上传尺寸相同图片,推荐尺寸{{ Math.floor(1200 / diyStore.editComponent.number) }}*{{ diyStore.editComponent.imageHeight }})
+                    </div>
+                    <div class="flex-row-between images-box">
+                      <div>缩放模式</div>
+                      <div class="flex-row-start" @click="openImageType(element, index)">
+                        <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                          element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                        }}</span>
+                        <el-icon class="cursor-pointer">
+                          <ArrowRight />
+                        </el-icon>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+              <el-form-item label="链接地址">
+                <el-input v-model="element.url" placeholder="请输入链接地址" />
+              </el-form-item>
+            </div>
+          </template>
+        </draggable>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片间隙</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片间隙">
+            <el-slider size="small" v-model="diyStore.editComponent.gap" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="上圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageTopRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="下圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageBottomRoundedRounded" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+//图片类型返回
+const confirmCallBack = (res: any, index: any) => {
+  diyStore.editComponent.imagelList[index].imgType = res.imgType;
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+
+      .annotation3 {
+        font-size: 12px;
+        color: #666;
+        line-height: 14px;
+      }
+    }
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+  :deep(.file-selector) {
+    display: none;
+  }
+}
+</style>

+ 226 - 0
src/views/diy/pcEdit/navigation-edit.vue

@@ -0,0 +1,226 @@
+<template>
+  <div class="pc-edit">
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">导航模式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="展示风格">
+            <el-radio-group v-model="diyStore.editComponent.styleType">
+              <el-radio :value="1">固定显示</el-radio>
+              <el-radio :value="2">单行滑动</el-radio>
+              <el-radio :value="3">分页滑动</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="每行数量">
+            <el-radio-group v-model="diyStore.editComponent.number">
+              <el-radio :value="3">3个</el-radio>
+              <el-radio :value="4">4个</el-radio>
+              <el-radio :value="5">5个</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="每页行数" v-if="diyStore.editComponent.styleType == 3">
+            <el-radio-group v-model="diyStore.editComponent.count">
+              <el-radio :value="1">1个</el-radio>
+              <el-radio :value="2">2个</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <div class="edit-attr-title flex-row-between">
+          <div>
+            <span>导航设置</span>
+            <span class="title2">鼠标拖拽可以改变顺序</span>
+          </div>
+        </div>
+        <el-form label-width="86px" class="px-[10px]">
+          <draggable v-model="diyStore.editComponent.navlList" item-key="id">
+            <template #item="{ element, index }">
+              <div class="edit-attr-box">
+                <el-icon @click="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
+                  <CircleCloseFilled />
+                </el-icon>
+                <el-form-item label="图片上传">
+                  <div class="flex-row-start">
+                    <upload-image v-model="element.imageUrl" :limit="1" />
+                    <div class="flex-column-between images-bos">
+                      <div class="annotation3">(建议上传尺寸相同图片,推荐尺寸150*150)</div>
+                      <div class="flex-row-between images-box">
+                        <div>缩放模式</div>
+                        <div class="flex-row-start" @click="openImageType(element, index)">
+                          <span style="margin-top: 2px" class="text-primary flex-1 cursor-pointer">{{
+                            element.imgType == 1 ? '拉伸' : element.imgType == 2 ? '缩放' : '填充'
+                          }}</span>
+                          <el-icon class="cursor-pointer">
+                            <ArrowRight />
+                          </el-icon>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </el-form-item>
+                <el-form-item label="标题名称">
+                  <el-input v-model="element.title" placeholder="请输入标签名称" :maxlength="10" show-word-limit />
+                </el-form-item>
+                <el-form-item label="副标题名称">
+                  <el-input v-model="element.subtitle" placeholder="请输入标签名称" />
+                </el-form-item>
+                <el-form-item label="链接地址">
+                  <el-input v-model="element.url" placeholder="请输入链接地址" />
+                </el-form-item>
+              </div>
+            </template>
+          </draggable>
+          <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增导航</el-button>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">样式设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.titleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字加粗">
+            <el-radio-group size="small" v-model="diyStore.editComponent.titleWeight" fill="#409eff">
+              <el-radio-button label="加粗" :value="'bold'" />
+              <el-radio-button label="不加粗" :value="'normal'" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.titleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.titleColor" />
+            <el-button @click="diyStore.editComponent.titleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">副标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.subtitleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.subtitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.subtitleColor" />
+            <el-button @click="diyStore.editComponent.subtitleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">图片设置</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="图片圆角">
+            <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <ImagesForm ref="ImagesFormRef" @confirmCallBack="confirmCallBack"></ImagesForm>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import uploadImage from '@/components/upload-image/index.vue';
+import ImagesForm from '@/components/ImagesForm/index.vue';
+const diyStore = usePcdiyStore();
+const ImagesFormRef = ref();
+
+// 打开图片类型
+const openImageType = (element: any, type: any) => {
+  const datas = JSON.parse(JSON.stringify(element));
+  ImagesFormRef.value.onOpen(datas, type);
+};
+//图片类型返回
+const confirmCallBack = (res: any, index: any) => {
+  diyStore.editComponent.navlList[index].imgType = res.imgType;
+};
+const onAdd = () => {
+  diyStore.editComponent.navlList.push({
+    imageUrl: '',
+    title: '标题名称',
+    subtitle: '副标题名称',
+    url: '',
+    imgType: 1,
+    id: Date.now()
+  });
+};
+
+const onDel = (index: any) => {
+  diyStore.editComponent.navlList.splice(index, 1);
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+
+    .edit-attr-title {
+      display: flex;
+
+      .title2 {
+        font-size: 12px;
+        color: #666;
+        margin-left: 6px;
+      }
+    }
+
+    .edit-attr-box {
+      padding: 18px 10px 0 10px;
+      border: 1px solid #e5e6eb;
+      border-radius: 4px;
+      position: relative;
+      margin-top: 18px;
+
+      .images-bos {
+        flex: 1;
+        height: 98px;
+        padding: 5px 0;
+      }
+
+      .images-box {
+        font-size: 13px;
+        color: #666;
+      }
+
+      .circleClose {
+        position: absolute;
+        top: -9px;
+        right: -9px;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .annotation3 {
+    font-size: 12px;
+    color: #666;
+    line-height: 14px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+
+  :deep(.file-selector) {
+    display: none;
+  }
+
+  // :deep(.el-radio){
+  //   margin-right: 10px;
+  // }
+}
+</style>

+ 281 - 0
src/views/diy/pcEdit/textTitle-edit.vue

@@ -0,0 +1,281 @@
+<template>
+  <div class="pc-edit">
+    <!-- 内容 -->
+    <div class="content-wrap" v-show="diyStore.editTab == 'content'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">风格设置</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="风格选择" class="flex">
+            <span class="text-primary flex-1 cursor-pointer" @click="showStyle">风格{{ diyStore.editComponent.styleType }}</span>
+            <el-icon @click="showStyle" class="cursor-pointer">
+              <ArrowRight />
+            </el-icon>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">标题内容</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="标题名称" class="flex">
+            <el-input v-model="diyStore.editComponent.title" placeholder="请输入标题内容" />
+          </el-form-item>
+          <el-form-item label="链接地址">
+            <WebLinkInput v-model="diyStore.editComponent.titleUrl" placeholder="请输入或选择链接" />
+            <!-- <el-input v-model="diyStore.editComponent.titleUrl" placeholder="请输入链接地址" /> -->
+          </el-form-item>
+          <el-form-item label="对齐方式" class="flex" v-if="diyStore.editComponent.styleType == 1">
+            <el-radio-group v-model="diyStore.editComponent.titleAlign">
+              <el-radio :value="'left'">居左</el-radio>
+              <el-radio :value="'center'">居中</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div
+        class="edit-attr-item-wrap"
+        v-if="
+          diyStore.editComponent.styleType == 10 ||
+          diyStore.editComponent.styleType == 11 ||
+          diyStore.editComponent.styleType == 12 ||
+          diyStore.editComponent.styleType == 14
+        "
+      >
+        <h3 class="mb-[10px]">副标题内容</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="副标题名称" class="flex">
+            <el-input v-model="diyStore.editComponent.subtitle" placeholder="请输入副标题内容" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.styleType == 12 || diyStore.editComponent.styleType == 14">
+        <h3 class="mb-[10px]">"更多"按钮</h3>
+        <el-form label-width="90px" class="px-[10px]">
+          <el-form-item label="按钮文字" class="flex">
+            <el-input v-model="diyStore.editComponent.more" placeholder="请输入按钮文字" />
+          </el-form-item>
+          <el-form-item label="链接地址" class="flex">
+            <el-input v-model="diyStore.editComponent.moreUrl" placeholder="请输入链接地址" />
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+    <!-- 样式 -->
+    <div class="style-wrap" v-show="diyStore.editTab == 'style'">
+      <div class="edit-attr-item-wrap">
+        <h3 class="mb-[10px]">标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.titleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字加粗">
+            <el-radio-group size="small" v-model="diyStore.editComponent.titleWeight" fill="#409eff">
+              <el-radio-button label="加粗" :value="'bold'" />
+              <el-radio-button label="不加粗" :value="'normal'" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.titleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.titleColor" />
+            <el-button @click="diyStore.editComponent.titleColor = '#101828'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div
+        class="edit-attr-item-wrap"
+        v-if="
+          diyStore.editComponent.styleType == 10 ||
+          diyStore.editComponent.styleType == 11 ||
+          diyStore.editComponent.styleType == 12 ||
+          diyStore.editComponent.styleType == 14
+        "
+      >
+        <h3 class="mb-[10px]">副标题样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.subtitleSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.subtitleColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.subtitleColor" />
+            <el-button @click="diyStore.editComponent.subtitleColor = '#b7bcd2'" size="small">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div class="edit-attr-item-wrap" v-if="diyStore.editComponent.styleType == 12 || diyStore.editComponent.styleType == 14">
+        <h3 class="mb-[10px]">"更多"按钮样式</h3>
+        <el-form label-width="80px" class="px-[10px]">
+          <el-form-item label="文字大小">
+            <el-slider size="small" v-model="diyStore.editComponent.moreSize" show-input :min="1" :max="50" />
+          </el-form-item>
+          <el-form-item label="文字颜色">
+            <span class="mr-[10px]">{{ diyStore.editComponent.moreColor }}</span>
+            <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.moreColor" />
+            <el-button @click="diyStore.editComponent.moreColor = '#b7bcd2'" size="small">重置</el-button>
+          </el-form-item>
+          <el-form-item label="是否显示">
+            <el-switch v-model="diyStore.editComponent.moreShow" />
+          </el-form-item>
+        </el-form>
+      </div>
+      <!-- 组件样式 -->
+      <slot name="style"></slot>
+    </div>
+    <!-- 风格弹窗 -->
+    <el-dialog v-model="showDialog" title="风格选择">
+      <div class="data-bos">
+        <template v-for="(item, index) in styleList" :key="index">
+          <div
+            :class="{ 'border-primary': styleId == item.id }"
+            @click="changeTitleStyle(item)"
+            class="data-list flex items-center justify-center overflow-hidden w-[200px] h-[100px] mr-[12px] mb-[12px] cursor-pointer border bg-[#eee]"
+          >
+            <img :src="item.img" />
+          </div>
+        </template>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="showDialog = false">取消</el-button>
+          <el-button type="primary" @click="confirmTitleStyle">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import usePcdiyStore from '@/store/modules/pcdiy';
+import titlle1 from '@/assets/images/pcdiy/titlle1.png';
+import titlle2 from '@/assets/images/pcdiy/titlle2.png';
+import titlle3 from '@/assets/images/pcdiy/titlle3.png';
+import titlle4 from '@/assets/images/pcdiy/titlle4.png';
+import titlle5 from '@/assets/images/pcdiy/titlle5.png';
+import titlle6 from '@/assets/images/pcdiy/titlle6.png';
+import titlle7 from '@/assets/images/pcdiy/titlle7.png';
+import titlle8 from '@/assets/images/pcdiy/titlle8.png';
+import titlle9 from '@/assets/images/pcdiy/titlle9.png';
+import titlle10 from '@/assets/images/pcdiy/titlle10.png';
+import titlle11 from '@/assets/images/pcdiy/titlle11.png';
+import titlle12 from '@/assets/images/pcdiy/titlle12.png';
+import titlle13 from '@/assets/images/pcdiy/titlle13.png';
+import titlle14 from '@/assets/images/pcdiy/titlle14.png';
+import titlle15 from '@/assets/images/pcdiy/titlle15.png';
+
+const diyStore = usePcdiyStore();
+const styleId = ref<any>(1);
+const showDialog = ref(false);
+
+const styleList = [
+  {
+    img: titlle1,
+    id: 1
+  },
+  {
+    img: titlle2,
+    id: 2
+  },
+  {
+    img: titlle3,
+    id: 3
+  },
+  {
+    img: titlle4,
+    id: 4
+  },
+  {
+    img: titlle5,
+    id: 5
+  },
+  {
+    img: titlle6,
+    id: 6
+  },
+  {
+    img: titlle7,
+    id: 7
+  },
+  {
+    img: titlle8,
+    id: 8
+  },
+  {
+    img: titlle9,
+    id: 9
+  },
+  {
+    img: titlle10,
+    id: 10
+  },
+  {
+    img: titlle11,
+    id: 11
+  },
+  {
+    img: titlle12,
+    id: 12
+  },
+  {
+    img: titlle13,
+    id: 13
+  },
+  {
+    img: titlle14,
+    id: 14
+  },
+  {
+    img: titlle15,
+    id: 15
+  }
+];
+
+//打开弹窗
+const showStyle = () => {
+  showDialog.value = true;
+  styleId.value = diyStore.editComponent.styleType;
+};
+//选择弹窗
+const changeTitleStyle = (item: any) => {
+  styleId.value = item.id;
+};
+
+//确定弹窗
+const confirmTitleStyle = () => {
+  diyStore.editComponent.styleType = styleId.value;
+  showDialog.value = false;
+};
+</script>
+
+<style lang="scss" scoped>
+.pc-edit {
+  .edit-attr-item-wrap {
+    border-top: 2px solid var(--el-color-info-light-8);
+    padding-top: 20px;
+
+    &:first-of-type {
+      border-top: none;
+      padding-top: 0;
+    }
+  }
+
+  .data-bos {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0 12px;
+    .data-list {
+      background-color: #f9fafb;
+      border: 1px solid #e5e7eb;
+      &.border-primary {
+        border-color: var(--el-color-primary);
+      }
+      img {
+        width: 100%;
+      }
+    }
+  }
+
+  :deep(.el-form-item__label) {
+    font-weight: 400;
+  }
+}
+</style>

+ 209 - 0
src/views/diy/pcList.vue

@@ -0,0 +1,209 @@
+<template>
+  <div class="p-2">
+    <div class="head-card">
+      <el-card shadow="hover">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+          <el-form-item label="页面名称">
+            <el-input v-model="queryParams.name" placeholder="请输入页面名称" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="所属应用">
+            <el-select v-model="queryParams.type" placeholder="请选择所属应用" clearable>
+              <el-option label="平台商城" :value="1" />
+              <el-option label="工业品商城" :value="2" />
+              <el-option label="福利商城" :value="3" />
+              <el-option label="客户站点商城" :value="4" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </div>
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleAdd">添加页面</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
+              删除
+            </el-button>
+          </el-col>
+        </el-row>
+      </template>
+      <el-table v-loading="loading" border :data="dataList">
+        <el-table-column label="页面名称" align="center" prop="name" :show-overflow-tooltip="true" />
+        <el-table-column label="页面类型" align="center" :show-overflow-tooltip="true">
+          <template #default="scope">
+            {{
+              scope.row.type == 1
+                ? '平台商城'
+                : scope.row.type == 2
+                  ? '工业品商城'
+                  : scope.row.type == 3
+                    ? '福利商城'
+                    : scope.row.type == 4
+                      ? '客户站点商城'
+                      : ''
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="150">
+          <template #default="scope">
+            {{ scope.row.isHome == 1 ? '开启' : '关闭' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="更新时间" align="center" :show-overflow-tooltip="true" width="200">
+          <template #default="scope">
+            {{ scope.row.updateTime }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </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="dialogVisible" title="创建新页面" width="400px">
+      <el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules">
+        <el-form-item label="页面名称" prop="title">
+          <el-input v-model.trim="formData.title" placeholder="请输入标题" clearable maxlength="12" show-word-limit class="w-full" />
+        </el-form-item>
+        <el-form-item label="页面类型" prop="type">
+          <el-select v-model="formData.type" placeholder="请选择页面类型" class="!w-full">
+            <el-option label="平台商城" :value="1" />
+            <el-option label="工业品商城" :value="2" />
+            <el-option label="福利商城" :value="3" />
+            <el-option label="客户站点商城" :value="4" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="addEvent(formRef)">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { pcDiyList, template, pcDelDiy } from '@/api/diy/index';
+import { FormInstance } from 'element-plus';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dataList = ref<any[]>([]);
+const loading = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const pageType: any = reactive({}); // 页面类型
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+// 添加自定义页面
+const formData = reactive({
+  title: '',
+  type: ''
+});
+// 表单验证规则
+const formRules = computed(() => {
+  return {
+    title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+    type: [{ required: true, message: '请选择页面类型', trigger: 'blur' }]
+  };
+});
+
+const queryParams = ref<any>({
+  pageNum: 1,
+  pageSize: 10,
+  title: '',
+  addon_name: '',
+  type: ''
+});
+
+/** 查询字典类型列表 */
+const getList = () => {
+  loading.value = true;
+  pcDiyList(queryParams.value).then((res) => {
+    if (res.code == 200) {
+      dataList.value = res.rows;
+      total.value = res.total;
+      loading.value = false;
+    }
+  });
+};
+
+/** 表单重置 */
+const reset = () => {};
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+/** 重置按钮操作 */
+const resetQuery = () => {
+  handleQuery();
+};
+/** 新增按钮操作 */
+const router = useRouter();
+const handleAdd = () => {
+  dialogVisible.value = true;
+  // const url = router.resolve({
+  //   path: '/diy/edit',
+  //   query: {}
+  // });
+  // window.open(url.href);
+};
+/** 修改按钮操作 */
+const handleUpdate = async (row?: any) => {};
+/** 删除按钮操作 */
+const handleDelete = async (row?: any) => {
+  await proxy?.$modal.confirm('是否确认删除"' + row.name + '"的diy数据项?').finally(() => (loading.value = false));
+  await pcDelDiy(row.id);
+  proxy?.$modal.msgSuccess('删除成功');
+  getList();
+};
+
+const addEvent = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+
+  await formEl.validate(async (valid) => {
+    if (valid) {
+      const query = { type: formData.type, title: formData.title };
+      const url = router.resolve({
+        path: '/diy/pcedit',
+        query
+      });
+      window.open(url.href);
+      dialogVisible.value = false;
+      formData.title = '';
+      formData.type = '';
+    }
+  });
+};
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style lang="scss" scoped>
+.head-card {
+  margin-bottom: 20px;
+  :deep(.el-card__body) {
+    padding-bottom: 0px !important;
+  }
+}
+</style>

+ 173 - 0
src/views/diy/pcPages/advert.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="pcPages">
+    <div class="carousel-bos" :style="warpCss" v-if="componentData.styleType == 3">
+      <el-carousel :height="270 * componentData.count + (componentData.count == 2 ? 10 : 0) + 'px'" :autoplay="false" arrow="always">
+        <el-carousel-item v-for="(item1, index1) in dataList" :key="index1" class="w100% h100%">
+          <div class="carousel-list">
+            <div v-for="(item, index) in item1" :key="index" class="data-list flex-column-between" :style="boxCss">
+              <el-image
+                class="img"
+                :src="item.imageUrl ? item.imageUrl : figure"
+                :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+                :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+              />
+              <div :style="titleCss" class="title">{{ item.title || '' }}</div>
+              <div :style="subtitleCss" class="mt-[2px] mb-[12px] subtitle ellipsis">{{ item.subtitle || '' }}</div>
+            </div>
+          </div>
+        </el-carousel-item>
+      </el-carousel>
+    </div>
+    <div v-else :style="warpCss" class="data-bos">
+      <div v-for="(item, index) in componentData.navlList" :key="index" class="data-list flex-column-between" :style="boxCss">
+        <el-image
+          class="img"
+          :src="item.imageUrl ? item.imageUrl : figure"
+          :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+          :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+        />
+        <div :style="titleCss" class="title">{{ item.title || '' }}</div>
+        <div :style="subtitleCss" class="mt-[2px] mb-[12px] subtitle ellipsis">{{ item.subtitle || '' }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import usePcdiyStore from '@/store/modules/pcdiy';
+import figure from '@/assets/images/figure.png';
+const diyStore = usePcdiyStore();
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+
+const dataList = computed(() => {
+  const chunkSize = componentData.number * componentData.count;
+  const result = [];
+  for (let i = 0; i < componentData.navlList.length; i += chunkSize) {
+    const chunk = componentData.navlList.slice(i, i + chunkSize);
+    result.push(chunk);
+  }
+  return result;
+});
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+    if (componentData.styleType == 1) style += 'flex-wrap:wrap' + ';';
+  }
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  if (componentData.number) style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * 10}px) / ${componentData.number})` + ';';
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+  return style;
+});
+
+// 标题样式
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.titleColor) style += 'color:' + componentData.titleColor + ';';
+  if (componentData.titleSize) style += 'font-size:' + componentData.titleSize + 'px;';
+  if (componentData.titleWeight) style += 'font-weight:' + componentData.titleWeight + ';';
+  return style;
+});
+
+// 副标题样式
+const subtitleCss = computed(() => {
+  let style = '';
+  if (componentData.subtitleColor) style += 'color:' + componentData.subtitleColor + ';';
+  if (componentData.subtitleSize) style += 'font-size:' + componentData.subtitleSize + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .data-bos {
+    display: flex;
+    gap: 10px;
+    width: 100%;
+    overflow-x: auto;
+    .data-list {
+      min-height: 270px;
+      width: 0;
+
+      .title {
+        text-align: center;
+        padding: 0 15px;
+      }
+
+      .subtitle {
+        text-align: center;
+        padding: 0 15px;
+      }
+
+      .img {
+        height: 200px;
+        width: 100%;
+      }
+    }
+  }
+
+  .carousel-bos {
+    .carousel-list {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      gap: 10px;
+      flex-wrap: wrap;
+      .data-list {
+        height: 270px;
+        width: 0;
+
+        .title {
+          text-align: center;
+          padding: 0 15px;
+        }
+
+        .subtitle {
+          text-align: center;
+          padding: 0 15px;
+        }
+
+        .img {
+          height: 200px;
+          width: 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 183 - 0
src/views/diy/pcPages/article.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="article-bos" :style="boxCss">
+      <template v-for="(item, index) in dataList" :key="index">
+        <div class="article-list" :style="dataCss" v-if="componentData.dataType == 2 ? true : index < componentData.dataNumber">
+          <img :src="item.caseImage ? item.caseImage : figure" alt="" />
+          <div class="caseTitle">{{ item.caseTitle }}</div>
+          <div class="projectBrief">{{ item.projectBrief }}</div>
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { listServiceCase } from '@/api/product/serviceCase';
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+const dataList = ref<any>([]);
+
+onMounted(() => {
+  getDataList();
+});
+const getDataList = () => {
+  dataList.value = [];
+  // 默认
+  if (componentData.dataType == 1) {
+    listServiceCase({ pageSize: 16 }).then((res) => {
+      if (res.code == 200) {
+        dataList.value = res.rows;
+      }
+    });
+  } else {
+    //手动选择
+    listServiceCase({ pageSize: 16 }).then((res) => {
+      if (res.code == 200) {
+        const result = res.rows.filter((item: any) => componentData.dataIds.includes(item.id));
+        dataList.value = result;
+        console.log('result', result);
+      }
+    });
+  }
+};
+
+// 监听 componentData 变化,重新请求数据
+watch(
+  () => componentData.dataType,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.dataIds,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  return style;
+});
+
+//样式
+const dataCss = computed(() => {
+  let style = '';
+  //背景颜色
+  if (componentData.backgroundColor) style += 'background-color:' + componentData.backgroundColor + ';';
+  //圆角
+  if (componentData.boxTopRounded) style += 'border-top-left-radius:' + componentData.boxTopRounded + 'px;';
+  if (componentData.boxTopRounded) style += 'border-top-right-radius:' + componentData.boxTopRounded + 'px;';
+  if (componentData.boxBottomRounded) style += 'border-bottom-left-radius:' + componentData.boxBottomRounded + 'px;';
+  if (componentData.boxBottomRounded) style += 'border-bottom-right-radius:' + componentData.boxBottomRounded + 'px;';
+  //投影
+  if (componentData.border == 2 && componentData.borderColor) style += 'box-shadow:' + componentData.borderColor + ' 0px 0px 5px';
+  //描边
+  if (componentData.border == 3 && componentData.borderColor) style += 'border:' + componentData.borderColor + ' 1px solid;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+
+  .article-bos {
+    width: 100%;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 15px;
+
+    .article-list {
+      flex: 0 0 calc((100% - 45px) / 4);
+      width: 0;
+      overflow: hidden;
+
+      .caseTitle {
+        font-size: 14px;
+        color: #333333;
+        height: 20px;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        width: 100%;
+        margin: 5px 0;
+        padding: 0 15px;
+      }
+
+      .projectBrief {
+        font-size: 12px;
+        color: #666666;
+        height: 50px;
+        display: -webkit-box;
+        -webkit-line-clamp: 3;
+        line-clamp: 3;
+        /* 添加标准属性 */
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 100%;
+        padding: 0 15px;
+        margin-bottom: 15px;
+      }
+
+      img {
+        width: 100%;
+        height: 165px;
+      }
+
+      // flex: 0 0 calc((100% - ${(componentData.number - 1) * 10}px);
+    }
+  }
+}
+</style>

+ 168 - 0
src/views/diy/pcPages/brand.vue

@@ -0,0 +1,168 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="big-brand" :style="boxCss">
+      <el-image
+        class="bigBrand-one"
+        :src="componentData.imageUrl ? componentData.imageUrl : figure"
+        :fit="componentData.imgType == 1 ? 'fill' : componentData.imgType == 2 ? 'contain' : 'cover'"
+        :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}"
+      />
+      <div class="bigBrand-bos">
+        <template v-for="(item, index) in componentData.brandList" :key="index">
+          <div
+            class="bigBrand-list"
+            v-if="Number(index) < 10"
+            :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}"
+          >
+            <el-image
+              class="img"
+              :src="item.imageUrl ? item.imageUrl : figure"
+              :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+              :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+            />
+            <div :style="titleCss" class="bigBrand1">{{ item.title || '' }}</div>
+            <div :style="subtitleCss" class="bigBrand2">
+              {{ item.subtitle || '' }}
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  if (componentData.number) style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * 10}px) / ${componentData.number})` + ';';
+  return style;
+});
+
+// 标题样式
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.titleColor) style += 'color:' + componentData.titleColor + ';';
+  if (componentData.titleSize) style += 'font-size:' + componentData.titleSize + 'px;';
+  if (componentData.titleWeight) style += 'font-weight:' + componentData.titleWeight + ';';
+  return style;
+});
+
+// 副标题样式
+const subtitleCss = computed(() => {
+  let style = '';
+  if (componentData.subtitleColor) style += 'color:' + componentData.subtitleColor + ';';
+  if (componentData.subtitleSize) style += 'font-size:' + componentData.subtitleSize + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  // 大牌推荐
+  .big-brand {
+    height: 334px;
+    display: flex;
+    gap: 10px;
+    width: 100%;
+
+    .bigBrand-one {
+      width: 230px;
+      height: 340px;
+    }
+
+    .bigBrand-bos {
+      flex: 1;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      overflow: hidden;
+
+      .bigBrand-list {
+        flex: 0 0 calc((100% - 40px) / 5);
+        height: 162px;
+        background: #ffffff;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        padding: 32px 20px 0 20px;
+        width: 0;
+
+        .img {
+          width: 150px;
+          height: 30px;
+        }
+
+        .bigBrand1 {
+          font-weight: 600;
+          font-size: 14px;
+          color: #101828;
+          margin: 10px 0 4px 0;
+        }
+
+        .bigBrand2 {
+          font-weight: 400;
+          font-size: 12px;
+          color: #364153;
+          display: -webkit-box;
+          -webkit-line-clamp: 2;
+          line-clamp: 2;
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+}
+</style>

+ 102 - 0
src/views/diy/pcPages/carousel.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="pcPages" :style="warpCss" :class="['position' + componentData.position, 'styleType' + componentData.styleType]">
+    <el-carousel :interval="componentData.interval" :height="componentData.imageHeight + 'px'" class="carousel-bos" :style="boxCss">
+      <el-carousel-item v-for="(item, index) in componentData.imagelList" :key="index" class="carousel-item">
+        <el-image
+          class="img"
+          :src="item.imageUrl ? item.imageUrl : figure"
+          :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+        />
+      </el-carousel-item>
+    </el-carousel>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import usePcdiyStore from '@/store/modules/pcdiy';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+import figure from '@/assets/images/figure.png';
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  //圆角
+  if (componentData.imageRadius) style += 'border-radius:' + componentData.imageRadius + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+
+  .carousel-bos {
+    width: 100%;
+    overflow: hidden;
+
+    .carousel-item {
+      width: 100%;
+      height: 100%;
+
+      .img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+
+  &.position1 {
+    :deep(.el-carousel__indicators--horizontal) {
+      left: 5%;
+    }
+  }
+  &.position2 {
+    :deep(.el-carousel__indicators--horizontal) {
+      left: 50%;
+    }
+  }
+  &.position3 {
+    :deep(.el-carousel__indicators--horizontal) {
+      left: 95%;
+    }
+  }
+  &.styleType2 {
+    :deep(.el-carousel__button) {
+      height: 10px;
+      width: 10px;
+      border-radius: 10px;
+    }
+  }
+}
+</style>

+ 449 - 0
src/views/diy/pcPages/discover.vue

@@ -0,0 +1,449 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="discover-bos" :style="boxCss">
+      <!-- 头部 -->
+      <div class="home-title flex-row-between" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+        <div>
+          <span :style="titleCss" class="title1 mr-[10px]">{{ componentData.title }}</span>
+          <span :style="subtitleCss">{{ componentData.subtitle }}</span>
+        </div>
+        <div class="title-more flex-row-start">
+          <div class="ml-[10px]" v-for="(item, index) in componentData.labelList" :key="index">{{ item.title }}</div>
+        </div>
+      </div>
+      <!-- 中间区域 -->
+      <div class="discover-box">
+        <el-image
+          class="discover-image"
+          :src="componentData.imageUrl ? componentData.imageUrl : figure"
+          :fit="componentData.imgType == 1 ? 'fill' : componentData.imgType == 2 ? 'contain' : 'cover'"
+          :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}"
+        />
+        <div class="plan-bos" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+          <div class="plan-head">方案推荐</div>
+          <div v-for="(item, index) in componentData.planList" :key="index" class="plan-list">
+            <el-image
+              class="plan-image"
+              :src="item.imageUrl ? item.imageUrl : figure"
+              :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+              :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+            />
+            <div class="plan-box flex-column-between">
+              <div class="plan-title ellipsis">{{ item.title }}</div>
+              <div class="plan-subtitle">{{ item.subtitle }}</div>
+            </div>
+          </div>
+        </div>
+        <div class="detect-bos" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+          <div class="detect-head">发现</div>
+          <div class="detect-box">
+            <div class="detect-two">
+              <div class="detect-list" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+                <div class="detect-item">
+                  <div class="detect-title ellipsis">{{ componentData.detectList[0].title }}</div>
+                  <div class="detect-subtitle mt-[6px] h-[32px]">{{ componentData.detectList[0].subtitle }}</div>
+                  <div class="detect-btn" :style="{ backgroundColor: componentData.boxColor }">立即进入</div>
+                </div>
+                <el-image
+                  class="detect-image"
+                  :src="componentData.detectList[0].imageUrl ? componentData.detectList[0].imageUrl : figure"
+                  :fit="componentData.detectList[0].imgType == 1 ? 'fill' : componentData.detectList[0].imgType == 2 ? 'contain' : 'cover'"
+                  :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+                />
+              </div>
+              <div class="detect-list" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+                <div class="detect-item">
+                  <div class="detect-title ellipsis">{{ componentData.detectList[1].title }}</div>
+                  <div class="detect-subtitle mt-[6px] h-[32px]">{{ componentData.detectList[1].subtitle }}</div>
+                  <div class="detect-btn" :style="{ backgroundColor: componentData.boxColor }">立即进入</div>
+                </div>
+                <el-image
+                  class="detect-image"
+                  :src="componentData.detectList[1].imageUrl ? componentData.detectList[1].imageUrl : figure"
+                  :fit="componentData.detectList[1].imgType == 1 ? 'fill' : componentData.detectList[1].imgType == 2 ? 'contain' : 'cover'"
+                  :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+                />
+              </div>
+            </div>
+            <div class="detect-one flex-column-between" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+              <div>
+                <div class="detect-title ellipsis">{{ componentData.detectList[2].title }}</div>
+                <div class="detect-subtitle mt-[6px]">{{ componentData.detectList[2].subtitle }}</div>
+              </div>
+              <el-image
+                class="detect-img"
+                :src="componentData.detectList[2].imageUrl ? componentData.detectList[2].imageUrl : figure"
+                :fit="componentData.detectList[2].imgType == 1 ? 'fill' : componentData.detectList[2].imgType == 2 ? 'contain' : 'cover'"
+                :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- 底部 -->
+      <div class="discover-foot">
+        <div class="discover-tab" :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}">
+          <div class="tab-head" :style="{ color: componentData.boxColor }">采购导航</div>
+          <div class="tab-bos">
+            <div v-for="(item, index) in componentData.tabList" :key="index" class="tab-list flex-row-center">
+              {{ item.title }}
+            </div>
+          </div>
+        </div>
+        <div
+          class="goods-bos flex-column-between"
+          v-for="(item, index) in dataList"
+          :key="index"
+          :style="componentData.boxRadius ? { borderRadius: componentData.boxRadius + 'px' } : {}"
+        >
+          <img
+            class="goods-img"
+            :src="item.productImage ? item.productImage : figure"
+            alt=""
+            :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+          />
+          <div>
+            <div class="goods-name">{{ item.itemName || '' }}</div>
+            <div class="goods-price" :style="{ color: componentData.boxColor }">¥{{ item.memberPrice }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { listBase } from '@/api/pmsProduct/base';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+const dataList = ref<any>([{}, {}, {}, {}]);
+
+onMounted(() => {
+  getDataList();
+});
+
+const getDataList = () => {
+  dataList.value = [{}, {}, {}, {}];
+  if (componentData.goodsIds.length > 0) {
+    listBase({ pageNum: 1, pageSize: 10, ids: componentData.goodsIds.join(',') }).then((res) => {
+      if (res.code == 200) {
+        dataList.value = res.rows;
+      }
+    });
+  }
+};
+
+watch(
+  () => componentData.goodsIds,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  if (componentData.number) style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * 10}px) / ${componentData.number})` + ';';
+  return style;
+});
+
+// 标题样式
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.titleColor) style += 'color:' + componentData.titleColor + ';';
+  if (componentData.titleSize) style += 'font-size:' + componentData.titleSize + 'px;';
+  if (componentData.titleWeight) style += 'font-weight:' + componentData.titleWeight + ';';
+  return style;
+});
+
+// 副标题样式
+const subtitleCss = computed(() => {
+  let style = '';
+  if (componentData.subtitleColor) style += 'color:' + componentData.subtitleColor + ';';
+  if (componentData.subtitleSize) style += 'font-size:' + componentData.subtitleSize + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .discover-bos {
+    width: 100%;
+    .home-title {
+      width: 100%;
+      background-color: #ffffff;
+      padding: 15px 20px;
+      .title-more {
+        font-size: 14px;
+        color: #333333;
+      }
+    }
+  }
+  //中间区域
+  .discover-box {
+    height: 340px;
+    width: 100%;
+    margin-top: 15px;
+    display: flex;
+    gap: 10px;
+
+    .discover-image {
+      width: 230px;
+      height: 340px;
+    }
+
+    // 方案
+    .plan-bos {
+      flex: 1;
+      height: 340px;
+      background: #ffffff;
+      padding: 0px 15px;
+      display: flex;
+      flex-direction: column;
+      min-width: 0;
+
+      .plan-head {
+        font-weight: 600;
+        font-size: 16px;
+        color: #101828;
+        height: 50px;
+        line-height: 50px;
+      }
+
+      .plan-list {
+        flex: 1;
+        display: flex;
+        border-bottom: 1px solid #e5e7eb;
+        cursor: pointer;
+        width: 100%;
+        margin-bottom: 14px;
+
+        &:last-child {
+          border-bottom: none;
+          margin-bottom: 0;
+        }
+
+        .plan-image {
+          width: 72px;
+          height: 72px;
+          margin-right: 10px;
+        }
+
+        .plan-box {
+          height: 72px;
+          flex: 1;
+          padding: 8px 0px 8px 5px;
+          width: 0;
+          .plan-title {
+            font-weight: 600;
+            font-size: 14px;
+            color: #101828;
+          }
+          .plan-subtitle {
+            height: 34px;
+            font-weight: 400;
+            font-size: 12px;
+            color: #364153;
+            display: -webkit-box;
+            -webkit-line-clamp: 2;
+            line-clamp: 2;
+            /* 添加标准属性 */
+            -webkit-box-orient: vertical;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+        }
+      }
+    }
+
+    .detect-bos {
+      width: 470px;
+      height: 340px;
+      background: #ffffff;
+      padding: 0px 15px 15px 15px;
+      .detect-head {
+        font-weight: 600;
+        font-size: 16px;
+        color: #101828;
+        height: 50px;
+        line-height: 50px;
+      }
+
+      .detect-box {
+        display: flex;
+        gap: 10px;
+        .detect-title {
+          font-weight: 600;
+          font-size: 14px;
+          color: #101828;
+        }
+        .detect-subtitle {
+          font-weight: 400;
+          font-size: 12px;
+          color: #364153;
+          display: -webkit-box;
+          -webkit-line-clamp: 2;
+          line-clamp: 2;
+          /* 添加标准属性 */
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+        .detect-two {
+          flex: 1;
+          width: 0;
+          .detect-list {
+            width: 100%;
+            height: 132.5px;
+            border: 1px solid #e5e7eb;
+            margin-top: 10px;
+            padding: 25px 10px;
+            display: flex;
+            gap: 10px;
+            &:first-child {
+              margin-top: 0;
+            }
+            .detect-item {
+              flex: 1;
+              width: 0;
+              .detect-btn {
+                width: 68px;
+                height: 24px;
+                text-align: center;
+                line-height: 24px;
+                font-size: 12px;
+                color: #ffffff;
+                margin-top: 12px;
+              }
+            }
+            .detect-image {
+              height: 72px;
+              width: 72px;
+            }
+          }
+        }
+        .detect-one {
+          width: 180px;
+          height: 275px;
+          border: 1px solid #e5e7eb;
+          background-color: #ffffff;
+          padding: 25px 10px;
+          .detect-img {
+            height: 124px;
+            width: 124px;
+            margin: 0 auto;
+          }
+        }
+      }
+    }
+  }
+  //底部
+  .discover-foot {
+    display: flex;
+    gap: 10px;
+    margin-top: 15px;
+    .discover-tab {
+      width: 230px;
+      height: 310px;
+      background: #ffffff;
+      padding: 0 15px 15px 15px;
+      .tab-head {
+        font-weight: 600;
+        font-size: 16px;
+        height: 50px;
+        line-height: 50px;
+      }
+      .tab-bos {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        gap: 10px 0;
+        .tab-list {
+          width: 94px;
+          height: 32px;
+          background: #f4f4f4;
+          font-size: 14px;
+          color: #101828;
+          border-radius: 4px 4px 4px 4px;
+        }
+      }
+    }
+    .goods-bos {
+      flex: 0 0 calc((100% - 270px) / 4);
+      width: 0;
+      background: #ffffff;
+      height: 310px;
+      padding: 15px 20px;
+      .goods-img {
+        width: 100%;
+        height: 190px;
+      }
+      .goods-name {
+        width: 100%;
+        display: -webkit-box;
+        -webkit-line-clamp: 3;
+        line-clamp: 3;
+        /* 添加标准属性 */
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-size: 14px;
+        color: #101828;
+        height: 57px;
+      }
+      .goods-price {
+        font-size: 16px;
+        margin-top: 5px;
+      }
+    }
+  }
+}
+</style>

+ 252 - 0
src/views/diy/pcPages/floor.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="floor-bos" :style="boxCss">
+      <el-image
+        class="floor-one"
+        :src="componentData.imageUrl ? componentData.imageUrl : figure"
+        :fit="componentData.imgType == 1 ? 'fill' : componentData.imgType == 2 ? 'contain' : 'cover'"
+        :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+      />
+      <div class="floor-box">
+        <div v-for="(item, index) in dataList" :key="index" class="goods-list flex-column-between">
+          <img
+            class="goods-img"
+            :src="item.productImage ? item.productImage : figure"
+            alt=""
+            :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+          />
+          <div v-if="componentData.goodsShow.includes(1)" class="itemName">{{ item.itemName || '' }}</div>
+          <div class="flex-row-between">
+            <div>
+              <span v-if="componentData.goodsShow.includes(2)" class="memberPrice" :style="{ color: componentData.btnbackgroundColor }"
+                >¥{{ item.memberPrice }}</span
+              >
+              <span v-if="componentData.goodsShow.includes(3)" class="marketPrice">¥{{ item.marketPrice }}</span>
+            </div>
+            <template v-if="componentData.btnShow">
+              <div v-if="componentData.btnStyle == 1" :style="btnCss1" class="btn1 ellipsis">{{ componentData.btnText }}</div>
+              <div v-if="componentData.btnStyle == 2" :style="btnCss2" class="btn2 flex-row-center">
+                <el-icon size="14"><Plus /></el-icon>
+              </div>
+              <div v-if="componentData.btnStyle == 3" :style="btnCss2" class="btn2 flex-row-center">
+                <icon name="iconfont icongouwuche" size="14px" />
+              </div>
+            </template>
+          </div>
+        </div>
+      </div>
+      <div class="goods-brand flex-column-between" v-if="componentData.styleType == 2">
+        <div class="brand-bos">
+          <template v-for="(item, index) in componentData.brandList" :key="index">
+            <el-image
+              v-if="Number(index) < 6"
+              class="brand-img"
+              :src="item.imageUrl ? item.imageUrl : figure"
+              :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+            />
+          </template>
+        </div>
+        <div class="brand-more flex-row-center" v-if="componentData.moreShow" :style="{ color: componentData.moreColor }">
+          <div>{{ componentData.moreTitle }}</div>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { listBase } from '@/api/pmsProduct/base';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+const dataList = ref<any>([{}, {}, {}, {}, {}, {}, {}, {}]);
+
+onMounted(() => {
+  getDataList();
+});
+
+watch(
+  () => componentData.goodsIds,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+const getDataList = () => {
+  dataList.value = [{}, {}, {}, {}, {}, {}, {}, {}];
+  //手动选择
+  if (componentData.goodsIds.length > 0) {
+    listBase({ pageNum: 1, pageSize: 20, ids: componentData.goodsIds.join(',') }).then((res) => {
+      if (res.code == 200) {
+        dataList.value = res.rows;
+      }
+    });
+  }
+};
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  if (componentData.number) style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * 10}px) / ${componentData.number})` + ';';
+  return style;
+});
+
+const btnCss1 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'background-color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnColor) style += 'color:' + componentData.btnColor + ';';
+  return style;
+});
+
+const btnCss2 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnbackgroundColor) style += 'border-color:' + componentData.btnbackgroundColor + ';';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .floor-bos {
+    height: 560px;
+    display: flex;
+    gap: 10px;
+    width: 100%;
+    .floor-one {
+      width: 230px;
+      height: 560px;
+    }
+    .floor-box {
+      flex: 1;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      overflow: hidden;
+      .goods-list {
+        padding: 15px 10px;
+        height: 275px;
+        width: 0;
+        flex: 0 0 calc((100% - 30px) / 4);
+        background-color: #ffffff;
+        overflow: hidden;
+        border-radius: 10px;
+        .goods-img {
+          width: 100%;
+          height: 140px;
+        }
+        .itemName {
+          width: 100%;
+          font-size: 14px;
+          color: #101828;
+          display: -webkit-box;
+          -webkit-line-clamp: 2;
+          line-clamp: 2;
+          /* 添加标准属性 */
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+        .memberPrice {
+          font-size: 16px;
+          font-weight: bold;
+        }
+        .marketPrice {
+          font-size: 12px;
+          color: #99a1af;
+          line-height: 20px;
+          text-decoration-line: line-through;
+          text-transform: none;
+          margin-left: 6px;
+          color: #99a1af;
+        }
+
+        .btn1 {
+          padding: 5px 15px;
+          font-size: 12px;
+          border-radius: 15px;
+        }
+        .btn2 {
+          color: var(--el-color-primary);
+          border: 1px solid var(--el-color-primary);
+          height: 26px;
+          width: 26px;
+          border-radius: 50%;
+        }
+      }
+    }
+    .goods-brand {
+      width: 140px;
+      height: 560px;
+      border-radius: 10px;
+      background: #ffffff;
+      padding: 15px 10px;
+      .brand-bos {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        gap: 10px 0;
+        .brand-img {
+          width: 100%;
+          height: 74px;
+          border-radius: 4px;
+          border: 1px solid #e5e7eb;
+          cursor: pointer;
+        }
+      }
+      .brand-more {
+        color: var(--el-color-primary);
+        font-size: 14px;
+        cursor: pointer;
+      }
+    }
+  }
+}
+</style>

+ 268 - 0
src/views/diy/pcPages/goods.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="goods-bos" :style="boxCss">
+      <div v-for="(item, index) in dataList" :key="index" class="goods-list flex-column-between" :style="goodsCss">
+        <img
+          class="goods-img"
+          :src="item.productImage ? item.productImage : figure"
+          alt=""
+          :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+        />
+        <div v-if="componentData.goodsShow.includes(1)" :style="titleCss" class="itemName">{{ item.itemName || '' }}</div>
+        <div class="flex-row-between">
+          <div>
+            <span v-if="componentData.goodsShow.includes(2)" class="memberPrice" :style="{ color: componentData.priceColor }"
+              >¥{{ item.memberPrice }}</span
+            >
+            <span v-if="componentData.goodsShow.includes(3)" class="marketPrice">¥{{ item.marketPrice }}</span>
+          </div>
+          <template v-if="componentData.btnShow">
+            <div v-if="componentData.btnStyle == 1" :style="btnCss1" class="btn1">{{ componentData.btnText }}</div>
+            <div v-if="componentData.btnStyle == 2" :style="btnCss2" class="btn2 flex-row-center">
+              <el-icon size="14"><Plus /></el-icon>
+            </div>
+            <div v-if="componentData.btnStyle == 3" :style="btnCss2" class="btn2 flex-row-center">
+              <icon name="iconfont icongouwuche" size="14px" />
+            </div>
+          </template>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { listBase } from '@/api/pmsProduct/base';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+const dataList = ref<any>([{}, {}, {}, {}, {}]);
+
+onMounted(() => {
+  getDataList();
+});
+
+const getDataList = () => {
+  dataList.value = [{}, {}, {}, {}, {}];
+  //手动选择
+  if (componentData.goodsType == 1) {
+    if (componentData.goodsIds.length > 0) {
+      listBase({ pageNum: 1, pageSize: 20, ids: componentData.goodsIds.join(',') }).then((res) => {
+        if (res.code == 200) {
+          dataList.value = res.rows;
+        }
+      });
+    }
+  } else if (componentData.goodsType == 2) {
+    //分类查询
+    if (componentData.goodsClassify) {
+      listBase({
+        pageNum: 1,
+        pageSize: componentData.goodsNumber == 0 ? 50 : componentData.goodsNumber,
+        topCategoryId: componentData.topCategoryId,
+        mediumCategoryId: componentData.mediumCategoryId,
+        bottomCategoryId: componentData.bottomCategoryId
+      }).then((res) => {
+        if (res.code == 200) {
+          dataList.value = res.rows;
+        }
+      });
+    }
+  } else if (componentData.goodsType == 3) {
+    //品牌查询
+    if (componentData.goodsBrand) {
+      listBase({
+        pageNum: 1,
+        pageSize: componentData.goodsNumber == 0 ? 50 : componentData.goodsNumber,
+        brandId: componentData.goodsBrand
+      }).then((res) => {
+        if (res.code == 200) {
+          dataList.value = res.rows;
+        }
+      });
+    }
+  }
+};
+
+watch(
+  () => componentData.goodsType,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.goodsClassify,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.goodsNumber,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.goodsBrand,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.goodsIds,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+
+  if (componentData.styleType == 1) style += 'flex-wrap:wrap' + ';';
+  return style;
+});
+
+const goodsCss = computed(() => {
+  let style = '';
+  if (componentData.goodsbackgroundColor) style += 'background-color:' + componentData.goodsbackgroundColor + ';';
+  //圆角
+  if (componentData.goodstopRounded) style += 'border-top-left-radius:' + componentData.goodstopRounded + 'px;';
+  if (componentData.goodstopRounded) style += 'border-top-right-radius:' + componentData.goodstopRounded + 'px;';
+  if (componentData.goodsbottomRounded) style += 'border-bottom-left-radius:' + componentData.goodsbottomRounded + 'px;';
+  if (componentData.goodsbottomRounded) style += 'border-bottom-right-radius:' + componentData.goodsbottomRounded + 'px;';
+  return style;
+});
+
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.goodsTitleColor) style += 'color:' + componentData.goodsTitleColor + ';';
+  if (componentData.goodsTitleType == 1) style += 'font-weight:bold' + ';';
+  if (componentData.goodsTitleType == 3) {
+    style += 'display: -webkit-box' + ';';
+    style += '-webkit-line-clamp: 2' + ';';
+    style += 'line-clamp: 2' + ';';
+    style += '-webkit-box-orient: vertical' + ';';
+  } else {
+    style += 'white-space:nowrap' + ';';
+  }
+  return style;
+});
+
+const btnCss1 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'background-color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnColor) style += 'color:' + componentData.btnColor + ';';
+  return style;
+});
+
+const btnCss2 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnbackgroundColor) style += 'border-color:' + componentData.btnbackgroundColor + ';';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .goods-bos {
+    display: flex;
+    gap: 10px;
+    overflow: auto;
+    .goods-list {
+      padding: 20px 15px;
+      height: 300px;
+      width: 0;
+      flex: 0 0 calc((100% - 40px) / 5);
+      overflow: hidden;
+      .goods-img {
+        width: 100%;
+        height: 180px;
+      }
+      .itemName {
+        font-size: 14px;
+        height: 40px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      .memberPrice {
+        font-size: 16px;
+        font-weight: bold;
+      }
+      .marketPrice {
+        font-size: 12px;
+        color: #99a1af;
+        line-height: 20px;
+        text-decoration-line: line-through;
+        text-transform: none;
+        margin-left: 6px;
+      }
+
+      .btn1 {
+        padding: 5px 15px;
+        font-size: 12px;
+        border-radius: 15px;
+      }
+      .btn2 {
+        color: var(--el-color-primary);
+        border: 1px solid var(--el-color-primary);
+        height: 26px;
+        width: 26px;
+        border-radius: 50%;
+      }
+    }
+  }
+}
+</style>

+ 284 - 0
src/views/diy/pcPages/goodsList.vue

@@ -0,0 +1,284 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div :style="boxCss">
+      <div class="tab-bos flex-row-start" :style="tabCss">
+        <div v-for="(item, index) in componentData.tabList" :key="index" class="tab-list flex-row-start">
+          <div class="tab-border" v-if="index != 0"></div>
+          <div class="tab-title" :style="tabHigCss" v-if="index == componentData.tabIndex">{{ item.title || '选项卡名称' }}</div>
+          <div class="tab-title" :style="{ color: componentData.tabColor1 }" v-else>{{ item.title || '选项卡名称' }}</div>
+        </div>
+      </div>
+      <div class="goods-bos">
+        <div v-for="(item, index) in dataList" :key="index" class="goods-list flex-column-between" :style="goodsCss">
+          <img
+            class="goods-img"
+            :src="item.productImage ? item.productImage : figure"
+            alt=""
+            :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+          />
+          <div v-if="componentData.goodsShow.includes(1)" :style="titleCss" class="itemName">{{ item.itemName || '' }}</div>
+          <div class="flex-row-between">
+            <div>
+              <span v-if="componentData.goodsShow.includes(2)" class="memberPrice" :style="{ color: componentData.priceColor }"
+                >¥{{ item.memberPrice }}</span
+              >
+              <span v-if="componentData.goodsShow.includes(3)" class="marketPrice">¥{{ item.marketPrice }}</span>
+            </div>
+            <template v-if="componentData.btnShow">
+              <div v-if="componentData.btnStyle == 1" :style="btnCss1" class="btn1">{{ componentData.btnText }}</div>
+              <div v-if="componentData.btnStyle == 2" :style="btnCss2" class="btn2 flex-row-center">
+                <el-icon size="14"><Plus /></el-icon>
+              </div>
+              <div v-if="componentData.btnStyle == 3" :style="btnCss2" class="btn2 flex-row-center">
+                <icon name="iconfont icongouwuche" size="14px" />
+              </div>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { listBase } from '@/api/pmsProduct/base';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+const dataList = ref<any>([{}, {}, {}, {}, {}]);
+
+onMounted(() => {
+  getDataList();
+});
+
+const getDataList = () => {
+  dataList.value = [{}, {}, {}, {}, {}];
+  if (componentData.tabList && componentData.tabList.length > 0) {
+    const datas = componentData.tabList[componentData.tabIndex];
+    //手动选择
+    if (datas.goodsType == 1) {
+      if (datas.goodsIds.length > 0) {
+        listBase({ pageNum: 1, pageSize: 20, ids: datas.goodsIds.join(',') }).then((res) => {
+          if (res.code == 200) {
+            dataList.value = res.rows;
+          }
+        });
+      }
+    } else if (datas.goodsType == 2) {
+      //分类查询
+      if (datas.goodsClassify) {
+        listBase({
+          pageNum: 1,
+          pageSize: datas.goodsNumber == 0 ? 50 : componentData.goodsNumber,
+          topCategoryId: datas.topCategoryId,
+          mediumCategoryId: datas.mediumCategoryId,
+          bottomCategoryId: datas.bottomCategoryId
+        }).then((res) => {
+          if (res.code == 200) {
+            dataList.value = res.rows;
+          }
+        });
+      }
+    }
+  }
+};
+
+watch(
+  () => componentData.tabIndex,
+  () => {
+    console.log('tabIndex', componentData.tabIndex);
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.tabList,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+
+  if (componentData.styleType == 1) style += 'flex-wrap:wrap' + ';';
+  return style;
+});
+
+const goodsCss = computed(() => {
+  let style = '';
+  if (componentData.goodsbackgroundColor) style += 'background-color:' + componentData.goodsbackgroundColor + ';';
+  //圆角
+  if (componentData.goodstopRounded) style += 'border-top-left-radius:' + componentData.goodstopRounded + 'px;';
+  if (componentData.goodstopRounded) style += 'border-top-right-radius:' + componentData.goodstopRounded + 'px;';
+  if (componentData.goodsbottomRounded) style += 'border-bottom-left-radius:' + componentData.goodsbottomRounded + 'px;';
+  if (componentData.goodsbottomRounded) style += 'border-bottom-right-radius:' + componentData.goodsbottomRounded + 'px;';
+  return style;
+});
+
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.goodsTitleColor) style += 'color:' + componentData.goodsTitleColor + ';';
+  if (componentData.goodsTitleType == 1) style += 'font-weight:bold' + ';';
+  if (componentData.goodsTitleType == 3) {
+    style += 'display: -webkit-box' + ';';
+    style += '-webkit-line-clamp: 2' + ';';
+    style += 'line-clamp: 2' + ';';
+    style += '-webkit-box-orient: vertical' + ';';
+  } else {
+    style += 'white-space:nowrap' + ';';
+  }
+  return style;
+});
+
+const btnCss1 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'background-color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnColor) style += 'color:' + componentData.btnColor + ';';
+  return style;
+});
+
+const btnCss2 = computed(() => {
+  let style = '';
+  if (componentData.btnbackgroundColor) style += 'color:' + componentData.btnbackgroundColor + ';';
+  if (componentData.btnbackgroundColor) style += 'border-color:' + componentData.btnbackgroundColor + ';';
+  return style;
+});
+
+const tabCss = computed(() => {
+  let style = '';
+  if (componentData.tabbackgroundColor1) style += 'background-color:' + componentData.tabbackgroundColor1 + ';';
+  if (componentData.tabRadius) style += 'border-radius:' + componentData.tabRadius + 'px' + ';';
+  return style;
+});
+
+const tabHigCss = computed(() => {
+  let style = '';
+  if (componentData.tabbackgroundColor2) style += 'background-color:' + componentData.tabbackgroundColor2 + ';';
+  if (componentData.tabColor2) style += 'color:' + componentData.tabColor2 + ';';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .tab-bos {
+    width: 100%;
+    margin-bottom: 10px;
+    padding: 15px;
+    overflow: auto;
+    .tab-list {
+      flex-shrink: 0;
+      .tab-border {
+        width: 1px;
+        height: 15px;
+        background-color: #e5e7eb;
+        margin-left: 30px;
+      }
+      .tab-title {
+        font-size: 14px;
+        margin-left: 30px;
+        border-radius: 24px;
+        height: 24px;
+        line-height: 24px;
+        padding: 0 10px;
+      }
+    }
+  }
+  .goods-bos {
+    display: flex;
+    gap: 10px;
+    overflow: auto;
+    .goods-list {
+      padding: 20px 15px;
+      height: 300px;
+      width: 0;
+      flex: 0 0 calc((100% - 40px) / 5);
+      overflow: hidden;
+      .goods-img {
+        width: 100%;
+        height: 180px;
+      }
+      .itemName {
+        font-size: 14px;
+        height: 40px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      .memberPrice {
+        font-size: 16px;
+        font-weight: bold;
+      }
+      .marketPrice {
+        font-size: 12px;
+        color: #99a1af;
+        line-height: 20px;
+        text-decoration-line: line-through;
+        text-transform: none;
+        margin-left: 6px;
+      }
+
+      .btn1 {
+        padding: 5px 15px;
+        font-size: 12px;
+        border-radius: 15px;
+      }
+      .btn2 {
+        color: var(--el-color-primary);
+        border: 1px solid var(--el-color-primary);
+        height: 26px;
+        width: 26px;
+        border-radius: 50%;
+      }
+    }
+  }
+}
+</style>

+ 919 - 0
src/views/diy/pcPages/head.vue

@@ -0,0 +1,919 @@
+<template>
+  <div class="head-bos" :style="warpCss">
+    <!-- 搜索组件 -->
+    <div class="search-bos">
+      <img
+        class="logo"
+        :src="componentData.logo ? componentData.logo : figure"
+        alt=""
+        :style="{ 'width': componentData.topStyle == 1 ? '185px' : '390px' }"
+      />
+      <div class="search-box">
+        <div class="search-div flex-row-start">
+          <div class="search-input flex-row-center">
+            <el-input class="el-input" readonly placeholder="搜索商品、品牌、分类..." />
+            <div class="bnt flex-row-center">
+              <el-icon color="#ffffff" size="20">
+                <Search />
+              </el-icon>
+            </div>
+          </div>
+          <el-badge :value="5">
+            <div class="cat-bos flex-row-center">
+              <img src="@/assets/images/pcdiy/layout4.png" alt="" />
+              <span>我的购物车</span>
+            </div>
+          </el-badge>
+        </div>
+        <div class="search-text">
+          <div v-for="(item, index) in componentData.toplabel" :key="index">{{ item.title }}</div>
+        </div>
+      </div>
+      <img class="code" :src="componentData.code ? componentData.code : figure" alt="" />
+    </div>
+    <!-- 分类 -->
+    <div class="nav-pages">
+      <div class="nav-bos">
+        <div class="nav-all flex-row-center" v-if="componentData.classifyShow">
+          <img src="@/assets/images/pcdiy/layout2.png" alt="" />
+          <div>全部商品分类</div>
+        </div>
+        <div v-for="(item, index) in componentData.topNav" :key="index" class="nav-list" :class="index == 0 ? 'hig' : ''">
+          {{ item.title }}
+        </div>
+      </div>
+    </div>
+
+    <!--  头部 -->
+    <div class="head-pages">
+      <div class="bg-img" v-if="componentData.carouselStyle">
+        <img :src="componentData.carouselList && componentData.carouselList[0].imageUrl" alt="" />
+      </div>
+      <div class="home-head">
+        <div
+          class="classify"
+          v-if="componentData.leftStyle == 1"
+          :style="{
+            backgroundColor: componentData.leftBackground
+          }"
+        >
+          <div class="classify-list" v-for="(item, index) in classifyList" :key="index">
+            <div :style="{ 'color': componentData.leftColor1 }" class="label ellipsis">{{ item.label }}</div>
+            <div :style="{ 'color': componentData.leftColor2 }" class="info info1 ellipsis">{{ item.oneLable1 }}</div>
+            <div :style="{ 'color': componentData.leftColor2 }" class="info ellipsis">{{ item.oneLable2 }}</div>
+          </div>
+        </div>
+        <div class="classify2" v-else>
+          <div class="classify-list" v-for="(item, index) in classifyList2" :key="index">
+            <div class="label ellipsis">{{ item.label }}</div>
+            <div class="two-level">
+              <template v-for="(item1, index1) in item.children" :key="index1">
+                <div class="two-hig" v-if="Number(index1) < 2">{{ item1.label }}</div>
+                <div style="margin: 0 4px" v-if="index1 == 0 && item.children.length > 1">/</div>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="head-bos">
+          <div
+            class="carousel"
+            :class="{ 'carousel-type': componentData.carouselType == 2 }"
+            :style="{
+              height: componentData.advertNum == 0 ? '540px' : '400px',
+              borderRadius: componentData.carouselRadius + 'px'
+            }"
+          >
+            <el-carousel :autoplay="false" trigger="click" :height="componentData.advertNum == 0 ? '540px' : '400px'" arrow="always">
+              <template v-if="componentData.carouselList && componentData.carouselList.length > 0">
+                <el-carousel-item v-for="item in componentData.carouselList" :key="item">
+                  <el-image
+                    style="width: 100%; height: 100%"
+                    :src="item.imageUrl ? item.imageUrl : figure"
+                    :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+                  />
+                </el-carousel-item>
+              </template>
+              <template v-else>
+                <el-carousel-item>
+                  <img :src="figure" alt="" />
+                </el-carousel-item>
+              </template>
+            </el-carousel>
+          </div>
+          <div class="head-box" v-if="componentData.advertNum != 0">
+            <template v-for="(item, index) in diyStore.editComponent.advertList" :key="index">
+              <div class="head-item" v-if="item.show">
+                <el-image
+                  style="width: 100%; height: 100%"
+                  :src="item.imageUrl ? item.imageUrl : figure"
+                  :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+                />
+              </div>
+            </template>
+          </div>
+        </div>
+        <!-- 右边 -->
+        <div
+          class="head-right"
+          :style="{
+            borderRadius: componentData.rightRadius + 'px'
+          }"
+        >
+          <div class="login-bos">
+            <div class="login-box">
+              <img :src="figure" alt="" />
+              <div>
+                <div class="login1">您好,欢迎来到优易达</div>
+                <div class="login2">请先登录</div>
+              </div>
+            </div>
+            <div class="loginBtn-bos flex-row-center">
+              <div class="login-bnt1 flex-row-center">登录</div>
+              <div class="login-bnt2 flex-row-center">注册</div>
+            </div>
+          </div>
+          <div class="real-time">
+            <div class="real-title flex-row-between">
+              <div class="real1">优易资讯</div>
+              <div class="real2 flex-row-start">
+                <div>更多</div>
+                <el-icon :size="13" color="#83899F">
+                  <ArrowRight />
+                </el-icon>
+              </div>
+            </div>
+            <template v-for="(item, index) in realList" :key="index">
+              <div class="real-list ellipsis" v-if="componentData.realDataType == 2 ? true : Number(index) < componentData.realNumber">
+                {{ item.announcementTitle }}
+              </div>
+            </template>
+          </div>
+          <div class="interests">
+            <div class="interests-title">企业会员权益</div>
+            <div class="interests-bos">
+              <div v-for="(item, index) in componentData.navlList" :key="index" class="interests-item flex-column-center">
+                <el-image
+                  class="img"
+                  :src="item.imageUrl ? item.imageUrl : figure"
+                  :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+                />
+                <div style="height: 18px">{{ item.title }}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import figure from '@/assets/images/figure.png';
+import usePcdiyStore from '@/store/modules/pcdiy';
+import { listAnnouncement } from '@/api/system/announcement';
+const diyStore = usePcdiyStore();
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+onMounted(() => {
+  getDataList();
+});
+
+const getDataList = () => {
+  realList.value = [];
+  // 默认
+  if (componentData.realDataType == 1) {
+    listAnnouncement({ pageSize: 20 }).then((res) => {
+      if (res.code == 200) {
+        realList.value = res.rows;
+      }
+    });
+  } else {
+    //手动选择
+    listAnnouncement({ pageSize: 20, ids: componentData.realIds.join(',') }).then((res) => {
+      if (res.code == 200) {
+        const result = res.rows.filter((item: any) => componentData.realIds.includes(item.id));
+        realList.value = result;
+        console.log('result', result);
+      }
+    });
+  }
+};
+
+// 监听 componentData 变化,重新请求数据
+watch(
+  () => componentData.realDataType,
+  () => {
+    getDataList();
+  }
+);
+
+watch(
+  () => componentData.realIds,
+  () => {
+    getDataList();
+  },
+  { deep: true } // 5. 数组变化需要 deep 监听
+);
+
+//左侧分类
+const classifyList = ref<any>([
+  {
+    id: 12792,
+    label: '电脑/外设产品',
+    oneLable1: '联想',
+    oneLable2: '华硕'
+  },
+  {
+    id: 12792,
+    label: '通讯/数码智能',
+    oneLable1: '华为',
+    oneLable2: '罗技'
+  },
+  {
+    id: 12854,
+    label: '办公设备',
+    oneLable1: '惠普',
+    oneLable2: 'MAXHUB'
+  },
+  {
+    id: 12962,
+    label: '办公耗品',
+    oneLable1: '奔图',
+    oneLable2: '莱盛'
+  },
+  {
+    id: 14184,
+    label: '文具用品',
+    oneLable1: '齐心',
+    oneLable2: '得力'
+  },
+  {
+    id: 13162,
+    label: '个护清洁',
+    oneLable1: '洁柔',
+    oneLable2: '宝洁'
+  },
+  {
+    id: 13164,
+    label: '清洁纸品',
+    oneLable1: '洁柔',
+    oneLable2: '宝洁'
+  },
+  {
+    id: 12716,
+    label: '食品饮料',
+    oneLable1: '金龙鱼',
+    oneLable2: '贝蒂斯'
+  },
+  {
+    id: 13320,
+    label: '家居日用',
+    oneLable1: '膳魔师',
+    oneLable2: '博洋'
+  },
+  {
+    id: 13420,
+    label: '运动户外',
+    oneLable1: '红双喜',
+    oneLable2: '捷瑞特'
+  },
+  {
+    id: 13054,
+    label: '家用电器',
+    oneLable1: '摩飞',
+    oneLable2: '小熊'
+  },
+  {
+    id: 13478,
+    label: '家具用品',
+    oneLable1: '科飞亚',
+    oneLable2: '全能'
+  },
+  {
+    id: 13530,
+    label: '工业用品',
+    oneLable1: '代尔塔',
+    oneLable2: '宝工'
+  }
+]);
+//左侧分类
+const classifyList2 = ref<any>([
+  {
+    id: 12792,
+    label: '电脑/外设产品',
+    children: [
+      {
+        label: '电脑整机'
+      },
+      {
+        label: '电脑外设'
+      }
+    ]
+  },
+  {
+    id: 12792,
+    label: '电气控制及工业自动化',
+    children: [
+      {
+        label: '配电及控制电器'
+      },
+      {
+        label: '电气辅材'
+      }
+    ]
+  },
+  {
+    id: 12854,
+    label: '手工具、动力工具及耗材',
+    children: [
+      {
+        label: '防爆工具'
+      },
+      {
+        label: '钳工工具'
+      }
+    ]
+  },
+  {
+    id: 12962,
+    label: '气动、液压、泵、管阀',
+    children: [
+      {
+        label: '气源处理元件'
+      },
+      {
+        label: '气动控制元件'
+      }
+    ]
+  },
+  {
+    id: 14184,
+    label: '轴承、传动、通用设备',
+    children: [
+      {
+        label: '轴承及其工具'
+      },
+      {
+        label: '皮带'
+      }
+    ]
+  },
+  {
+    id: 13162,
+    label: '刀具、量具、磨具、仪表',
+    children: [
+      {
+        label: '刃具'
+      },
+      {
+        label: '刀具智能系统'
+      }
+    ]
+  },
+  {
+    id: 13164,
+    label: '紧固、密封、建工材料',
+    children: [
+      {
+        label: '螺栓'
+      },
+      {
+        label: '组合件'
+      }
+    ]
+  },
+  {
+    id: 12716,
+    label: '搬运、存储、包材',
+    children: [
+      {
+        label: '智能仓储设备'
+      },
+      {
+        label: '搬运堆高设备'
+      }
+    ]
+  }
+]);
+const realList = ref<any>([]);
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //边距
+  if (diyStore.componentList[props.index].padding) {
+    if (diyStore.componentList[props.index].padding.top > 0) {
+      style += 'padding-top:' + diyStore.componentList[props.index].padding.top + 'px' + ';';
+    }
+    if (diyStore.componentList[props.index].padding.bottom > 0) {
+      style += 'padding-bottom:' + diyStore.componentList[props.index].padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + diyStore.componentList[props.index].padding.both + 'px' + ';';
+    style += 'padding-left:' + diyStore.componentList[props.index].padding.both + 'px' + ';';
+  }
+
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.head-bos {
+  width: 1300px;
+
+  // 搜索栏
+  .search-bos {
+    margin: 0 auto;
+    width: 1300px;
+    display: flex;
+    background-color: #ffffff;
+    padding: 0 50px;
+
+    .logo {
+      width: 185px;
+      height: 90px;
+      border-radius: 4px;
+      margin-top: 15px;
+      margin-right: 30px;
+    }
+
+    .search-box {
+      flex: 1;
+      padding-top: 57px;
+
+      .search-div {
+        .search-input {
+          flex: 1;
+          height: 48px;
+          border-radius: 10px;
+          border: 2px solid #fb2c36;
+          padding-right: 4px;
+
+          .el-input {
+            height: 40px;
+            width: 100%;
+            font-size: 16px;
+
+            :deep(.el-input__wrapper) {
+              border: none;
+              /* 可选:去除聚焦时的高亮 */
+              box-shadow: none;
+              outline: none;
+            }
+          }
+
+          .bnt {
+            width: 68px;
+            height: 40px;
+            background: #e7000b;
+            border-radius: 8px;
+            font-weight: bold;
+            cursor: pointer;
+          }
+        }
+
+        .cat-bos {
+          width: 143px;
+          height: 48px;
+          background: #ffffff;
+          border-radius: 10px;
+          border: 1px solid #e5e7eb;
+          margin-left: 24px;
+          font-size: 16px;
+          color: #e7000b;
+          cursor: pointer;
+
+          img {
+            width: 16px;
+            height: 16px;
+            margin-right: 8px;
+            margin-top: 2px;
+          }
+        }
+      }
+
+      .search-text {
+        font-size: 14px;
+        color: #e7000b;
+        display: flex;
+        margin-top: 6px;
+
+        div {
+          margin-left: 10px;
+        }
+      }
+    }
+
+    .code {
+      height: 90px;
+      width: 90px;
+      margin-top: 30px;
+      margin-left: 70px;
+      border-radius: 4px;
+    }
+  }
+
+  //分类
+  .nav-pages {
+    width: 1300px;
+    background-color: #ffffff;
+    .nav-bos {
+      margin: 0 auto;
+      width: 1200px;
+      display: flex;
+      position: relative;
+      padding-top: 15px;
+
+      .nav-all {
+        width: 234px;
+        height: 48px;
+        background: #e7000b;
+        padding: 0 10px;
+        font-size: 16px;
+        color: #ffffff;
+        cursor: pointer;
+
+        img {
+          height: 16px;
+          width: 16px;
+          margin-right: 6px;
+        }
+      }
+
+      .nav-list {
+        line-height: 48px;
+        font-size: 16px;
+        color: #364153;
+        margin: 0 16px;
+        text-align: center;
+        cursor: pointer;
+
+        &.hig {
+          color: #e7000b;
+          position: relative;
+          &::before {
+            content: '';
+            position: absolute;
+            bottom: 0;
+            left: 0px;
+            display: inline-block;
+            width: 100%;
+            height: 3px;
+            background: #e7000b;
+            margin-right: 8px;
+          }
+        }
+
+        &:hover {
+          color: #e7000b;
+        }
+      }
+    }
+  }
+
+  // 头部
+  .head-pages {
+    width: 1300px;
+
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    position: relative;
+    backdrop-filter: blur(95px);
+    filter: blur(30rpx);
+
+    .bg-img {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      z-index: -1;
+      filter: blur(30rpx);
+      overflow: hidden;
+
+      img {
+        width: 100%;
+        height: 100%;
+        transform: scale(1.2);
+        filter: blur(30px);
+      }
+    }
+
+    .home-head {
+      width: 1200px;
+      position: relative;
+      display: flex;
+      gap: 0px 10px;
+      margin: 10px auto 0 auto;
+
+      .classify {
+        width: 234px;
+        height: 540px;
+        background: #ffffff;
+        border-radius: 5px;
+
+        .classify-list {
+          width: 100%;
+          height: 40px;
+          cursor: pointer;
+          display: flex;
+          align-items: center;
+          padding-left: 15px;
+          position: relative;
+
+          .label {
+            max-width: 100px;
+            font-weight: 600;
+            font-size: 14px;
+            color: #101828;
+            white-space: nowrap;
+            margin-right: 10px;
+
+            &:hover {
+              color: #e7000b;
+            }
+          }
+
+          .info {
+            max-width: 60px;
+            font-size: 12px;
+            color: #364153;
+            white-space: nowrap;
+
+            &.info1 {
+              margin-right: 6px;
+            }
+
+            &:hover {
+              color: #e7000b;
+            }
+          }
+
+          .classify-border {
+            position: absolute;
+            right: -1px;
+            top: 0px;
+            width: 1px;
+            height: 38px;
+            background-color: #ffffff;
+            z-index: 2;
+          }
+        }
+      }
+
+      .classify2 {
+        width: 234px;
+        height: 540px;
+        background: #ffffff;
+        padding: 10px 0px;
+
+        .classify-list {
+          width: 100%;
+          height: 60px;
+          cursor: pointer;
+          position: relative;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          padding-left: 25px;
+
+          &.classify-hig {
+            border: 1px solid var(--el-color-primary);
+            border-right: 0px solid var(--el-color-primary);
+          }
+
+          .label {
+            width: 100%;
+            font-size: 14px;
+            color: #101828;
+            white-space: nowrap;
+            margin-right: 10px;
+            font-weight: 600;
+
+            &:hover {
+              color: var(--el-color-primary);
+            }
+          }
+
+          .two-level {
+            display: flex;
+            align-items: center;
+            font-size: 12px;
+            color: #364153;
+            margin-top: 6px;
+
+            .two-hig {
+              &:hover {
+                color: var(--el-color-primary);
+              }
+            }
+          }
+        }
+      }
+
+      // 头部中间
+      .head-bos {
+        width: 756px;
+
+        .carousel {
+          overflow: hidden;
+          width: 756px;
+          background: #ffffff;
+
+          &.carousel-type {
+            :deep(.el-carousel__arrow) {
+              border-radius: 0;
+            }
+          }
+        }
+
+        .head-box {
+          width: 756px;
+          height: 130px;
+          margin-top: 10px;
+          display: flex;
+          gap: 0 10px;
+
+          .head-item {
+            flex: 1;
+            background-color: #ffffff;
+            height: 130px;
+            display: flex;
+            align-items: center;
+            cursor: pointer;
+            border-radius: 5px;
+            overflow: hidden;
+
+            img {
+              width: 100%;
+              height: 100%;
+            }
+          }
+        }
+      }
+
+      //右边
+      .head-right {
+        flex: 1;
+        width: 0;
+        height: 540px;
+        background: #ffffff;
+        display: flex;
+        flex-direction: column;
+
+        .login-bos {
+          width: calc(100% - 20px);
+          height: 110px;
+          border-bottom: 1px solid #e5e7eb;
+          margin: 0 10px;
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          padding: 16px 0;
+
+          .login-box {
+            display: flex;
+            align-items: center;
+
+            img {
+              width: 40px;
+              height: 40px;
+              margin-right: 8px;
+              border-radius: 40px;
+            }
+
+            .login1 {
+              font-size: 13px;
+              color: #444444;
+            }
+
+            .login2 {
+              margin-top: 2px;
+              font-size: 12px;
+              color: #6a7282;
+            }
+          }
+
+          .loginBtn-bos {
+            width: 100%;
+            .login-bnt1 {
+              width: 64px;
+              height: 24px;
+              background-color: #e7000b;
+              color: #ffffff;
+              border-radius: 24px;
+              font-size: 12px;
+            }
+            .login-bnt2 {
+              width: 64px;
+              height: 24px;
+              border: 1px solid #e7000b;
+              color: #e7000b;
+              border-radius: 24px;
+              font-size: 12px;
+              margin-left: 10px;
+            }
+          }
+        }
+
+        .real-time {
+          width: calc(100% - 20px);
+          // height: 230px;
+          border-bottom: 1px solid #e5e7eb;
+          margin: 0 10px;
+          padding-top: 15px;
+
+          .real-title {
+            position: relative;
+            margin-bottom: 12px;
+
+            &::after {
+              content: '';
+              top: 3px;
+              left: 0;
+              position: absolute;
+              width: 4px;
+              height: 14px;
+              background: #e7000b;
+            }
+
+            .real1 {
+              font-weight: 600;
+              font-size: 14px;
+              color: #1d2129;
+              padding-left: 15px;
+            }
+
+            .real2 {
+              font-size: 13px;
+              color: #83899f;
+              cursor: pointer;
+            }
+          }
+
+          .real-list {
+            width: 100%;
+            font-size: 14px;
+            color: #1d2129;
+            margin-bottom: 12px;
+            cursor: pointer;
+
+            &:hover {
+              color: #e7000b;
+            }
+          }
+        }
+
+        .interests {
+          flex: 1;
+          width: calc(100% - 20px);
+          margin: 0 10px;
+          padding-top: 15px;
+
+          .interests-title {
+            position: relative;
+            font-weight: 600;
+            font-size: 14px;
+            color: #1d2129;
+            padding-left: 15px;
+
+            &::after {
+              content: '';
+              top: 3px;
+              left: 0;
+              position: absolute;
+              width: 4px;
+              height: 14px;
+              background: #e7000b;
+            }
+          }
+
+          .interests-bos {
+            display: flex;
+            flex-wrap: wrap;
+
+            .interests-item {
+              width: 33.333%;
+              font-size: 12px;
+              color: #101828;
+              margin-top: 15px;
+              cursor: pointer;
+
+              &:hover {
+                color: #e7000b;
+              }
+
+              .img {
+                width: 34px;
+                height: 34px;
+                border-radius: 6px;
+                margin-bottom: 7px;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 93 - 0
src/views/diy/pcPages/imageCube.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="pcPages" :style="warpCss">
+    <div class="imageCube-bos" :style="{ gap: componentData.gap + 'px' }">
+      <template v-for="(item, index) in componentData.imagelList" :key="index">
+        <div v-if="index < componentData.number" class="imageCube-list" :style="boxCss">
+          <el-image
+            class="img"
+            :src="item.imageUrl ? item.imageUrl : figure"
+            :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+            :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+          />
+        </div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import usePcdiyStore from '@/store/modules/pcdiy';
+import figure from '@/assets/images/figure.png';
+const diyStore = usePcdiyStore();
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  // 宽度
+  if (componentData.number)
+    style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * componentData.gap}px) / ${componentData.number})` + ';';
+  //圆角
+  if (componentData.imageTopRounded) style += 'border-top-left-radius:' + componentData.imageTopRounded + 'px;';
+  if (componentData.imageTopRounded) style += 'border-top-right-radius:' + componentData.imageTopRounded + 'px;';
+  if (componentData.imageBottomRoundedRounded) style += 'border-bottom-left-radius:' + componentData.imageBottomRoundedRounded + 'px;';
+  if (componentData.imageBottomRoundedRounded) style += 'border-bottom-right-radius:' + componentData.imageBottomRoundedRounded + 'px;';
+  //高度
+  if (componentData.imageHeight) style += 'height:' + componentData.imageHeight + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .imageCube-bos {
+    display: flex;
+    .imageCube-list {
+      overflow: hidden;
+      .img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+}
+</style>

+ 151 - 0
src/views/diy/pcPages/navigation.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="pcPages">
+    <div class="carousel-bos" :style="warpCss" v-if="componentData.styleType == 3">
+      <el-carousel :height="170 * componentData.count + (componentData.count == 2 ? 10 : 0) + 'px'" :autoplay="false" arrow="always">
+        <el-carousel-item v-for="(item1, index1) in dataList" :key="index1" class="w100% h100%">
+          <div class="carousel-list">
+            <div v-for="(item, index) in item1" :key="index" class="data-list flex-column-center" :style="boxCss">
+              <div :style="titleCss">{{ item.title || '' }}</div>
+              <div :style="subtitleCss" class="mt-[2px] mb-[12px]">{{ item.subtitle || '' }}</div>
+              <el-image
+                class="img"
+                :src="item.imageUrl ? item.imageUrl : figure"
+                :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+                :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+              />
+            </div>
+          </div>
+        </el-carousel-item>
+      </el-carousel>
+    </div>
+    <div v-else :style="warpCss" class="data-bos">
+      <div v-for="(item, index) in componentData.navlList" :key="index" class="data-list flex-column-center" :style="boxCss">
+        <div :style="titleCss">{{ item.title || '' }}</div>
+        <div :style="subtitleCss" class="mt-[2px] mb-[12px]">{{ item.subtitle || '' }}</div>
+        <el-image
+          class="img"
+          :src="item.imageUrl ? item.imageUrl : figure"
+          :fit="item.imgType == 1 ? 'fill' : item.imgType == 2 ? 'contain' : 'cover'"
+          :style="componentData.imageRadius ? { borderRadius: componentData.imageRadius + 'px' } : {}"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import usePcdiyStore from '@/store/modules/pcdiy';
+import figure from '@/assets/images/figure.png';
+const diyStore = usePcdiyStore();
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+
+const dataList = computed(() => {
+  const chunkSize = componentData.number * componentData.count;
+  const result = [];
+  for (let i = 0; i < componentData.navlList.length; i += chunkSize) {
+    const chunk = componentData.navlList.slice(i, i + chunkSize);
+    result.push(chunk);
+  }
+  return result;
+});
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+    if (componentData.styleType == 1) style += 'flex-wrap:wrap' + ';';
+  }
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  if (componentData.number) style += 'flex:' + `0 0 calc((100% - ${(componentData.number - 1) * 10}px) / ${componentData.number})` + ';';
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+  return style;
+});
+
+// 标题样式
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.titleColor) style += 'color:' + componentData.titleColor + ';';
+  if (componentData.titleSize) style += 'font-size:' + componentData.titleSize + 'px;';
+  if (componentData.titleWeight) style += 'font-weight:' + componentData.titleWeight + ';';
+  return style;
+});
+
+// 副标题样式
+const subtitleCss = computed(() => {
+  let style = '';
+  if (componentData.subtitleColor) style += 'color:' + componentData.subtitleColor + ';';
+  if (componentData.subtitleSize) style += 'font-size:' + componentData.subtitleSize + 'px;';
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+  .data-bos {
+    display: flex;
+    gap: 10px;
+    width: 100%;
+    overflow-x: auto;
+    .data-list {
+      min-height: 170px;
+
+      .img {
+        height: 80px;
+        width: 80px;
+      }
+    }
+  }
+
+  .carousel-bos {
+    .carousel-list {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      gap: 10px;
+      flex-wrap: wrap;
+      .data-list {
+        height: 170px;
+
+        .img {
+          height: 80px;
+          width: 80px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 457 - 0
src/views/diy/pcPages/textTitle.vue

@@ -0,0 +1,457 @@
+<template>
+  <div class="pcPages">
+    <div :style="warpCss">
+      <!-- 风格1 -->
+      <div :style="boxCss" v-if="componentData.styleType == 1">
+        <div :style="titleCss">{{ componentData.title }}{{ componentData.titleUrl }}</div>
+      </div>
+      <!-- 风格2 -->
+      <div :style="boxCss" class="style2 flex-row-center" v-else-if="componentData.styleType == 2">
+        <div class="style2-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+        <div class="style2-text" :style="titleCss">{{ componentData.title }}</div>
+        <div class="style2-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+      </div>
+      <!-- 风格3 -->
+      <div :style="boxCss" class="style3 flex-column-center" v-else-if="componentData.styleType == 3">
+        <div :style="titleCss" class="mb-[4px]">{{ componentData.title }}</div>
+        <div class="style3-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+        <div class="style3-jiao" :style="{ borderColor: componentData.titleColor }"></div>
+      </div>
+      <!-- 风格4 -->
+      <div :style="boxCss" class="style4 flex-column-center" v-else-if="componentData.styleType == 4">
+        <div :style="titleCss" class="mb-[4px]">{{ componentData.title }}</div>
+        <div class="style4-border1" :style="{ backgroundColor: componentData.titleColor }"></div>
+        <div class="style4-border2" :style="{ backgroundColor: componentData.titleColor }"></div>
+      </div>
+      <!-- 风格5 -->
+      <div :style="boxCss" class="style5 flex-column-center" v-else-if="componentData.styleType == 5">
+        <div :style="titleCss" class="mb-[4px]">{{ componentData.title }}</div>
+        <div class="flex-row-center">
+          <div class="style5-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+          <div class="style5-box" :style="{ borderColor: componentData.titleColor }"></div>
+          <div class="style5-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+        </div>
+      </div>
+      <!-- 风格6 -->
+      <div :style="boxCss" class="style6 flex-column-center" v-else-if="componentData.styleType == 6">
+        <div class="style6-bos flex-row-center" :style="{ borderColor: componentData.titleColor }">
+          <div class="style6-border style6-border1" :style="{ backgroundColor: componentData.titleColor }"></div>
+          <div :style="titleCss" class="style6-box">{{ componentData.title }}</div>
+          <div class="style6-border style6-border2" :style="{ backgroundColor: componentData.titleColor }"></div>
+        </div>
+      </div>
+      <!-- 风格7 -->
+      <div :style="boxCss" class="style7 flex-column-center" v-else-if="componentData.styleType == 7">
+        <div class="style7-bos">
+          <div class="style7-text" :style="titleCss">{{ componentData.title }}</div>
+          <div class="style7-line" :style="{ borderColor: componentData.titleColor }"></div>
+        </div>
+      </div>
+      <!-- 风格8 -->
+      <div :style="boxCss" class="style8 flex-column-center" v-else-if="componentData.styleType == 8">
+        <div class="style8-bos">
+          <div class="style8-text" :style="titleCss">{{ componentData.title }}</div>
+          <div class="style8-line" :style="{ borderColor: componentData.titleColor }"></div>
+        </div>
+      </div>
+      <!-- 风格9 -->
+      <div :style="boxCss" class="style9 flex-row-start" v-else-if="componentData.styleType == 9">
+        <div class="style9-border" :style="{ backgroundColor: componentData.titleColor }"></div>
+        <div class="pl-[10px]" :style="titleCss">{{ componentData.title }}</div>
+      </div>
+      <!-- 风格10 -->
+      <div :style="boxCss" class="style10 flex-column-center" v-else-if="componentData.styleType == 10">
+        <div class="style10-bos flex-row-start">
+          <img class="style10-img1" src="@/assets/images/pcdiy/style10-1.png" alt="" />
+          <div :style="titleCss">{{ componentData.title }}</div>
+          <img class="style10-img2" src="@/assets/images/pcdiy/style10-2.png" alt="" />
+        </div>
+        <div class="mt-[2px]" :style="subtitleCss">
+          {{ componentData.subtitle }}
+        </div>
+      </div>
+      <!-- 风格11 -->
+      <div :style="boxCss" class="style11 flex-column-center" v-else-if="componentData.styleType == 11">
+        <div class="style11-bos flex-row-start">
+          <img class="style11-img style11-img1" src="@/assets/images/pcdiy/style11-1.png" alt="" />
+          <div :style="titleCss" class="style11-text">{{ componentData.title }}</div>
+          <img class="style11-img style11-img2" src="@/assets/images/pcdiy/style11-2.png" alt="" />
+          <img class="style11-img3" src="@/assets/images/pcdiy/style11-3.png" alt="" />
+        </div>
+        <div class="mt-[2px]" :style="subtitleCss">
+          {{ componentData.subtitle }}
+        </div>
+      </div>
+      <!-- 风格12 -->
+      <div :style="boxCss" class="style12 flex-row-between" v-else-if="componentData.styleType == 12">
+        <div class="flex-row-start">
+          <div :style="titleCss">{{ componentData.title }}</div>
+          <div :style="subtitleCss" class="ml-[5px]">{{ componentData.subtitle }}</div>
+        </div>
+        <div v-if="componentData.moreShow" class="flex-row-start" :style="moreCss">
+          <div>{{ componentData.more }}</div>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+      </div>
+      <!-- 风格13 -->
+      <div :style="boxCss" class="style13 flex-column-center" v-else-if="componentData.styleType == 13">
+        <div class="style13-bos flex-row-start">
+          <img class="style13-img1" src="@/assets/images/pcdiy/style13-1.png" alt="" />
+          <div :style="titleCss">{{ componentData.title }}</div>
+          <img class="style13-img2" src="@/assets/images/pcdiy/style13-2.png" alt="" />
+        </div>
+      </div>
+      <!-- 风格14 -->
+      <div :style="boxCss" class="style14 flex-row-between" v-else-if="componentData.styleType == 14">
+        <div>
+          <div :style="titleCss">{{ componentData.title }}</div>
+          <div :style="subtitleCss">{{ componentData.subtitle }}</div>
+        </div>
+        <div v-if="componentData.moreShow" class="flex-row-start" :style="moreCss">
+          <div>{{ componentData.more }}</div>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="Index" lang="ts">
+import usePcdiyStore from '@/store/modules/pcdiy';
+const diyStore = usePcdiyStore();
+
+const props = defineProps<{
+  index: number; // 确保声明 index 为可选属性
+}>();
+const componentData = diyStore.componentList[props.index];
+
+const warpCss = computed(() => {
+  let style = '';
+  style += 'position:relative;';
+  //背景颜色
+  if (componentData.pageStartBgColor) {
+    if (componentData.pageStartBgColor && componentData.pageEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.pageStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.pageStartBgColor) style += `background: ${componentData.pageStartBgColor};`;
+    else if (componentData.pageEndBgColor) style += `background: ${componentData.pageEndBgColor};`;
+  }
+  //背景图片
+  if (componentData.componentBgUrl) {
+    style += `background-image:url('${componentData.componentBgUrl}');`;
+    style += 'background-size: cover;background-repeat: no-repeat;';
+  }
+  //边距
+  if (componentData.padding) {
+    if (componentData.padding.top > 0) {
+      style += 'padding-top:' + componentData.padding.top + 'px' + ';';
+    }
+    if (componentData.padding.bottom > 0) {
+      style += 'padding-bottom:' + componentData.padding.bottom + 'px' + ';';
+    }
+    style += 'padding-right:' + componentData.padding.both + 'px' + ';';
+    style += 'padding-left:' + componentData.padding.both + 'px' + ';';
+  }
+  //圆角
+  if (componentData.topRounded) style += 'border-top-left-radius:' + componentData.topRounded + 'px;';
+  if (componentData.topRounded) style += 'border-top-right-radius:' + componentData.topRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-left-radius:' + componentData.bottomRounded + 'px;';
+  if (componentData.bottomRounded) style += 'border-bottom-right-radius:' + componentData.bottomRounded + 'px;';
+
+  return style;
+});
+
+//组件样式
+const boxCss = computed(() => {
+  let style = '';
+  if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+    style += `background:linear-gradient(${componentData.componentGradientAngle},${componentData.componentStartBgColor},${componentData.componentEndBgColor});`;
+  else if (componentData.componentStartBgColor) style += 'background-color:' + componentData.componentStartBgColor + ';';
+  else if (componentData.componentEndBgColor) style += 'background-color:' + componentData.componentEndBgColor + ';';
+  return style;
+});
+
+// 标题样式
+const titleCss = computed(() => {
+  let style = '';
+  if (componentData.titleColor) style += 'color:' + componentData.titleColor + ';';
+  if (componentData.titleSize) style += 'font-size:' + componentData.titleSize + 'px;';
+  if (componentData.titleWeight) style += 'font-weight:' + componentData.titleWeight + ';';
+  if (componentData.titleAlign && componentData.styleType == 1) style += 'text-align:' + componentData.titleAlign;
+  if (componentData.titleColor && (componentData.styleType == 6 || componentData.styleType == 7)) {
+    style += 'border-color:' + componentData.titleColor;
+  }
+  if (componentData.titleColor && componentData.styleType == 8) {
+    style += 'background-color:' + componentData.titleColor;
+  }
+  return style;
+});
+
+// 副标题样式
+const subtitleCss = computed(() => {
+  let style = '';
+  if (componentData.subtitleColor) style += 'color:' + componentData.subtitleColor + ';';
+  if (componentData.subtitleSize) style += 'font-size:' + componentData.subtitleSize + 'px;';
+  return style;
+});
+
+// 更多样式
+const moreCss = computed(() => {
+  let style = '';
+  if (componentData.moreColor) style += 'color:' + componentData.moreColor + ';';
+  if (componentData.moreSize) style += 'font-size:' + componentData.moreSize + 'px;';
+  return style;
+});
+
+// 背景图加遮罩层
+const maskLayer = computed(() => {
+  let style = '';
+  //背景颜色
+  if (componentData.componentStartBgColor) {
+    if (componentData.componentStartBgColor && componentData.componentEndBgColor)
+      style += `background:linear-gradient(${componentData.pageGradientAngle},${componentData.componentStartBgColor},${componentData.pageEndBgColor});`;
+    else if (componentData.componentStartBgColor) style += `background: ${componentData.componentStartBgColor};`;
+    else if (componentData.componentEndBgColor) style += `background: ${componentData.componentEndBgColor};`;
+  }
+
+  return style;
+});
+</script>
+
+<style lang="scss" scoped>
+.pcPages {
+  width: 1200px;
+  margin: 0 auto;
+
+  .style2 {
+    .style2-text {
+      margin: 0 30px;
+    }
+
+    .style2-border {
+      width: 160px;
+      height: 1px;
+    }
+  }
+
+  .style3 {
+    position: relative;
+
+    .style3-border {
+      width: 260px;
+      height: 1px;
+    }
+
+    .style3-jiao {
+      background: transparent !important;
+      display: inline-block;
+      border: 6px solid #cccccc;
+      border-top-color: transparent !important;
+      border-left-color: transparent !important;
+      -webkit-transform: rotate(45deg);
+      transform: rotate(45deg);
+      margin-top: -6px;
+    }
+  }
+
+  .style4 {
+    .style4-border1 {
+      width: 160px;
+      height: 2px;
+    }
+
+    .style4-border2 {
+      width: 260px;
+      height: 1px;
+    }
+  }
+
+  .style5 {
+    .style5-box {
+      width: 10px;
+      height: 10px;
+      border: 1px solid #000;
+      display: inline-block;
+      -webkit-transform: rotate(45deg);
+      transform: rotate(45deg);
+      margin: 0 10px;
+    }
+
+    .style5-border {
+      width: 160px;
+      height: 1px;
+    }
+  }
+
+  .style6 {
+    .style6-bos {
+      border: 1px solid #ccc;
+      padding: 4px 4px;
+      position: relative;
+
+      .style6-box {
+        border: 1px solid #ccc;
+        padding: 2px 14px;
+      }
+
+      .style6-border {
+        width: 50px;
+        height: 4px;
+        position: absolute;
+        top: 50%;
+
+        &.style6-border1 {
+          left: -40px;
+        }
+
+        &.style6-border2 {
+          right: -40px;
+        }
+      }
+    }
+  }
+
+  .style7 {
+    .style7-bos {
+      position: relative;
+      margin-bottom: 6px;
+
+      .style7-text {
+        padding: 2px 14px;
+        position: relative;
+        z-index: 2;
+        background-color: #ffffff;
+        border: 1px solid #ccc;
+      }
+
+      .style7-line {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        border: 1px solid #ccc;
+        top: 6px;
+        left: 6px;
+        z-index: 0;
+      }
+    }
+  }
+
+  .style8 {
+    .style8-bos {
+      position: relative;
+      margin-bottom: 6px;
+
+      .style8-text {
+        padding: 2px 14px;
+        position: relative;
+        z-index: 2;
+        color: #ffffff !important;
+      }
+
+      .style8-line {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        border: 1px solid #ccc;
+        top: 6px;
+        left: 6px;
+        z-index: 0;
+      }
+    }
+  }
+
+  .style9 {
+    position: relative;
+
+    .style9-border {
+      height: 80%;
+      width: 3px;
+      position: absolute;
+    }
+  }
+
+  .style10 {
+    .style10-bos {
+      height: 100%;
+      position: relative;
+
+      img {
+        height: 100%;
+        position: absolute;
+
+        &.style10-img1 {
+          left: -70%;
+        }
+
+        &.style10-img2 {
+          right: -70%;
+        }
+      }
+    }
+  }
+
+  .style11 {
+    .style11-bos {
+      height: 100%;
+      position: relative;
+
+      .style11-img {
+        height: 100%;
+        position: absolute;
+
+        &.style11-img1 {
+          left: -60%;
+        }
+
+        &.style11-img2 {
+          right: -60%;
+        }
+      }
+
+      .style11-text {
+        position: relative;
+        z-index: 2;
+      }
+
+      .style11-img3 {
+        position: absolute;
+        width: 120%;
+        bottom: 0px;
+        left: -10%;
+      }
+    }
+  }
+
+  .style13 {
+    .style13-bos {
+      height: 100%;
+      position: relative;
+
+      img {
+        height: 100%;
+        position: absolute;
+
+        &.style13-img1 {
+          left: -80%;
+        }
+
+        &.style13-img2 {
+          right: -80%;
+        }
+      }
+    }
+  }
+}
+
+.textTitle-pages {
+  width: 100%;
+
+  .textTitle-bos {
+    background-color: #ffffff;
+    line-height: 45px;
+    padding-left: 20px;
+    font-size: 18px;
+    color: #333333;
+    font-weight: bold;
+  }
+}
+</style>

+ 7 - 4
src/views/platform/customerOperation/vipSite/index.vue

@@ -35,7 +35,7 @@
                 </el-form-item>
               </el-col>
               <el-col :span="6">
-                <el-form-item label="平台状态" prop="status">
+                <el-form-item label="状态" prop="status">
                   <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 100%">
                     <el-option label="启用" value="0" />
                     <el-option label="禁用" value="1" />
@@ -92,7 +92,7 @@
 
       <el-table v-loading="loading" border :data="siteList">
         <el-table-column label="客户编号" align="center" prop="clientNo" width="100" />
-        <el-table-column label="客户名称" align="center" prop="clientName" min-width="150" />
+        <el-table-column label="客户名称" align="center" prop="clientName" min-width="100" />
         <el-table-column label="站点名称" align="center" prop="siteName" min-width="180" />
         <el-table-column label="站点域名" align="center" prop="siteDomain" min-width="220">
           <template #default="scope">
@@ -116,7 +116,7 @@
             <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="操作" align="center" width="120" fixed="right">
+        <el-table-column label="操作" align="center" width="200" fixed="right">
           <template #default="scope">
             <div class="flex flex-col items-center gap-1">
               <el-link type="primary" :underline="false" @click="handleSiteConfig(scope.row)">站点配置</el-link>
@@ -296,7 +296,10 @@ const handleProductConfig = (row: SiteVO) => {
 
 /** 样式设计 */
 const handleStyleDesign = (row: SiteVO) => {
-  proxy?.$modal.msgWarning('样式设计功能开发中...');
+  router.push({
+    path: '/customerOperation/vipSite/styleDesign',
+    query: { siteId: row.id, siteName: row.siteName }
+  });
 };
 
 /** 状态切换 */

+ 127 - 19
src/views/platform/customerOperation/vipSite/productConfig.vue

@@ -65,7 +65,7 @@
         <el-table-column label="商品编号" align="center" prop="productNo" width="100" />
         <el-table-column label="商品图片" align="center" width="100">
           <template #default="scope">
-            <image-preview :src="scope.row.productImageUrl" :width="60" :height="60"/>
+            <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
           </template>
         </el-table-column>
         <el-table-column label="商品信息" align="center" min-width="400">
@@ -96,9 +96,9 @@
         </el-table-column>
         <el-table-column label="采购价" align="center" prop="purchasePrice" width="100" />
         <el-table-column label="协议价" align="center" prop="agreementPrice" width="100" />
-        <el-table-column label="平台售价毛利率" align="center" width="130">
+        <el-table-column label="毛利率" align="center" width="130">
           <template #default="scope">
-            {{ scope.row.grossMargin ? `${scope.row.grossMargin}%` : '-' }}
+            {{ calcGrossMargin(scope.row) }}
           </template>
         </el-table-column>
         <el-table-column label="商品状态" align="center" width="100">
@@ -136,6 +136,35 @@
       </template>
     </el-dialog>
 
+    <!-- 导入商品弹窗 -->
+    <el-dialog title="导入商品" v-model="importDialog.visible" width="500px" append-to-body>
+      <el-form label-width="100px">
+        <el-form-item label="导入模板">
+          <el-button type="primary" link icon="Download" @click="handleDownloadTemplate">下载导入模板</el-button>
+        </el-form-item>
+        <el-form-item label="选择文件">
+          <el-upload
+            ref="importUploadRef"
+            action="#"
+            :auto-upload="false"
+            :limit="1"
+            accept=".xlsx,.xls"
+            :on-change="handleImportFileChange"
+            :on-remove="() => (importDialog.file = null)"
+          >
+            <el-button type="primary" icon="Upload">选择文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">只支持 .xlsx / .xls 格式文件</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="importDialog.visible = false">取 消</el-button>
+        <el-button type="primary" :loading="importDialog.loading" @click="handleConfirmImport">确 定</el-button>
+      </template>
+    </el-dialog>
+
     <!-- 添加商品弹窗 -->
     <el-dialog title="添加商品" v-model="addProductDialog.visible" width="1500px" append-to-body top="5vh">
       <div class="add-product-dialog">
@@ -166,7 +195,7 @@
           <el-table-column label="商品编号" align="center" prop="productNo" width="100" />
           <el-table-column label="商品图片" align="center" width="100">
             <template #default="scope">
-              <image-preview :src="scope.row.productImageUrl" :width="60" :height="60"/>
+              <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
             </template>
           </el-table-column>
           <el-table-column label="商品信息" align="center" min-width="400">
@@ -205,7 +234,12 @@
             </template>
           </el-table-column>
           <el-table-column label="采购价" align="center" prop="purchasingPrice" width="80" />
-          <el-table-column label="商品状态" align="center" prop="productStatus" width="80" />
+          <el-table-column label="商品状态" align="center" prop="productStatus" width="80" >
+            <template #default="scope">
+              <el-tag v-if="scope.row.productStatus === '1'" type="success" size="small">上架</el-tag>
+              <el-tag v-else type="info" size="small">下架</el-tag>
+            </template>
+          </el-table-column>
           <el-table-column label="协议价" align="center" width="150">
             <template #default="scope">
               <el-input-number
@@ -244,10 +278,18 @@ import type { FormInstance } from 'element-plus';
 import { useRoute, useRouter } from 'vue-router';
 import { ArrowLeft } from '@element-plus/icons-vue';
 import { listBase } from '@/api/pmsProduct/base';
-import { BaseVO, BaseQuery } from '@/api/pmsProduct/base/types';
 import { listProductCategory } from '@/api/customerOperation/customerBlacklist';
 import { listBrand } from '@/api/product/brand';
-import { addSiteProduct } from '@/api/product/siteProduct';
+import {
+  addSiteProduct,
+  getSiteProductPage,
+  exportSiteProductData,
+  importSiteProductData,
+  getSiteProductImportTemplate,
+  updateSiteProduct,
+  delSiteProduct
+} from '@/api/product/siteProduct/index';
+import FileSaver from 'file-saver';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const route = useRoute();
@@ -307,15 +349,24 @@ const addProductDialog = reactive({
   productList: [] as any[],
   total: 0
 });
-const addProductQuery = ref<BaseQuery>({
+const addProductQuery = ref({
   pageNum: 1,
   pageSize: 10,
-  productNo: undefined,
-  itemName: undefined
+  isSelf: 1,
+  productNo: undefined as string | undefined,
+  itemName: undefined as string | undefined
 });
-const selectedProducts = ref<BaseVO[]>([]);
+const selectedProducts = ref<any[]>([]);
 const addProductTableRef = ref<any>();
 
+// 导入商品弹窗
+const importDialog = reactive({
+  visible: false,
+  loading: false,
+  file: null as File | null
+});
+const importUploadRef = ref<any>();
+
 /** 初始化 */
 const initData = () => {
   siteId.value = route.query.siteId as string;
@@ -328,8 +379,7 @@ const initData = () => {
 const getList = async () => {
   loading.value = true;
   try {
-    // TODO: 调用站点商品列表接口
-    const res = await listBase({
+    const res = await getSiteProductPage({
       ...queryParams.value,
       bottomCategoryId: queryParams.value.bottomCategoryId || queryParams.value.mediumCategoryId || queryParams.value.topCategoryId
     });
@@ -385,6 +435,15 @@ const handleMediumCategoryChange = (val: number | undefined) => {
   }
 };
 
+/** 计算毛利率:(协议价 - 采购价) ÷ 采购价 × 100% */
+const calcGrossMargin = (row: any): string => {
+  const agreement = parseFloat(row.agreementPrice);
+  const purchase = parseFloat(row.purchasePrice || row.purchasingPrice);
+  if (!purchase || purchase === 0 || isNaN(agreement) || isNaN(purchase)) return '-';
+  const margin = ((agreement - purchase) / purchase) * 100;
+  return `${margin.toFixed(2)}%`;
+};
+
 /** 搜索 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
@@ -428,7 +487,10 @@ const handlePriceEdit = (row: any) => {
 /** 提交价格修改 */
 const submitPriceForm = async () => {
   await priceFormRef.value?.validate();
-  // TODO: 调用修改协议价接口
+  await updateSiteProduct({
+    id: priceDialog.form.id,
+    agreementPrice: String(priceDialog.form.agreementPrice)
+  });
   proxy?.$modal.msgSuccess('价格修改成功');
   priceDialog.visible = false;
   await getList();
@@ -437,7 +499,7 @@ const submitPriceForm = async () => {
 /** 删除商品 */
 const handleDelete = async (row: any) => {
   await proxy?.$modal.confirm(`确认要删除商品"${row.itemName}"吗?`);
-  // TODO: 调用删除接口
+  await delSiteProduct(row.id);
   proxy?.$modal.msgSuccess('删除成功');
   await getList();
 };
@@ -448,6 +510,7 @@ const handleAddProduct = () => {
   addProductQuery.value = {
     pageNum: 1,
     pageSize: 10,
+    isSelf: 1,
     productNo: undefined,
     itemName: undefined
   };
@@ -478,7 +541,7 @@ const handleSearchProducts = () => {
 };
 
 /** 选择变化 */
-const handleSelectionChange = (selection: BaseVO[]) => {
+const handleSelectionChange = (selection: any[]) => {
   selectedProducts.value = selection;
 };
 
@@ -537,14 +600,59 @@ const handleAddSingleProduct = async (row: any) => {
   }
 };
 
+/** 下载导入模板 */
+const handleDownloadTemplate = async () => {
+  try {
+    const res = await getSiteProductImportTemplate();
+    const blob = new Blob([res as any], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+    FileSaver.saveAs(blob, '站点商品导入模板.xlsx');
+  } catch (error) {
+    console.error('下载模板失败:', error);
+  }
+};
+
+/** 导入文件选择 */
+const handleImportFileChange = (file: any) => {
+  importDialog.file = file.raw;
+};
+
+/** 确认导入 */
+const handleConfirmImport = async () => {
+  if (!importDialog.file) {
+    proxy?.$modal.msgWarning('请先选择要导入的文件');
+    return;
+  }
+  importDialog.loading = true;
+  try {
+    await importSiteProductData(importDialog.file);
+    proxy?.$modal.msgSuccess('导入成功');
+    importDialog.visible = false;
+    importDialog.file = null;
+    if (importUploadRef.value) importUploadRef.value.clearFiles();
+    await getList();
+  } catch (error) {
+    console.error('导入失败:', error);
+  } finally {
+    importDialog.loading = false;
+  }
+};
+
 /** 导入商品 */
 const handleImportProduct = () => {
-  proxy?.$modal.msgWarning('导入商品功能开发中...');
+  importDialog.visible = true;
+  importDialog.file = null;
+  if (importUploadRef.value) importUploadRef.value.clearFiles();
 };
 
 /** 导出商品 */
-const handleExportProduct = () => {
-  proxy?.$modal.msgWarning('导出商品功能开发中...');
+const handleExportProduct = async () => {
+  try {
+    const res = await exportSiteProductData({ siteId: siteId.value });
+    const blob = new Blob([res as any], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+    FileSaver.saveAs(blob, `站点商品数据_${new Date().getTime()}.xlsx`);
+  } catch (error) {
+    console.error('导出失败:', error);
+  }
 };
 
 onMounted(() => {

+ 10 - 1
src/views/platform/customerOperation/vipSite/siteConfig.vue

@@ -87,6 +87,14 @@
           />
         </el-form-item>
 
+        <el-form-item label="是否自定义" prop="isDiy">
+          <el-switch
+            v-model="form.isDiy"
+            :active-value="1"
+            :inactive-value="0"
+          />
+        </el-form-item>
+
         <el-form-item label="站点logo" prop="siteLogo">
           <div class="flex items-center gap-2">
             <el-image
@@ -197,7 +205,8 @@ const initForm: SiteForm = {
   startTime: '',
   endTime: '',
   status: '1',
-  isShow: 1
+  isShow: 1,
+  isDiy: 0
 };
 const form = ref<SiteForm>({ ...initForm });
 

+ 693 - 0
src/views/platform/customerOperation/vipSite/styleDesign.vue

@@ -0,0 +1,693 @@
+<template>
+  <div class="p-2">
+    <!-- 返回按钮 + 标题 -->
+    <el-card shadow="never" class="mb-2">
+      <div class="flex items-center">
+        <el-button link @click="handleBack">
+          <el-icon><ArrowLeft /></el-icon>
+          返回
+        </el-button>
+        <el-divider direction="vertical" />
+        <span class="text-base font-medium">样式设置</span>
+        <span v-if="siteName" class="text-gray-500 ml-2 text-sm">- {{ siteName }}</span>
+      </div>
+    </el-card>
+
+    <!-- 搜索 + 操作区域 -->
+    <el-card shadow="never" class="mb-2">
+      <div class="flex items-center gap-3">
+        <span class="text-sm whitespace-nowrap">楼层名称</span>
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入楼层名称"
+          clearable
+          style="width: 220px"
+          @keyup.enter="handleQuery"
+        />
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+        <el-button type="primary" icon="Plus" @click="handleAdd">添加楼层</el-button>
+      </div>
+    </el-card>
+
+    <!-- 表格区域 -->
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex justify-between items-center">
+          <span class="font-bold text-[#409EFF]">楼层信息列表</span>
+          <el-tooltip content="刷新">
+            <el-button circle icon="Refresh" size="small" @click="getList" />
+          </el-tooltip>
+        </div>
+      </template>
+
+      <el-table v-loading="loading" border :data="floorList">
+        <el-table-column label="楼层名称" align="center" prop="name" min-width="150" />
+        <el-table-column label="是否显示" align="center" width="100">
+          <template #default="scope">
+            <el-link
+              :type="scope.row.isShow === '1' ? 'primary' : 'info'"
+              :underline="false"
+              @click="handleToggleShow(scope.row)"
+            >
+              {{ scope.row.isShow === '1' ? '显示' : '隐藏' }}
+            </el-link>
+          </template>
+        </el-table-column>
+        <el-table-column label="排序" align="center" prop="sort" width="80" />
+        <el-table-column label="更新时间" align="center" prop="updateTime" width="170" />
+        <el-table-column label="操作" align="center" width="300" fixed="right">
+          <template #default="scope">
+            <el-link type="danger" :underline="false" class="mr-2" @click="handleDelete(scope.row)">删除</el-link>
+            <el-link type="primary" :underline="false" class="mr-2" @click="handleEdit(scope.row)">编辑</el-link>
+            <el-link type="primary" :underline="false" class="mr-2" @click="handleConfigProduct(scope.row)">配置商品</el-link>
+            <el-link type="warning" :underline="false" @click="handleConfigBrand(scope.row)">配置品牌</el-link>
+          </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 v-model="dialogVisible" :title="dialogTitle" width="620px" :close-on-click-modal="false">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="楼层名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入楼层名称" maxlength="50" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="链接" prop="link">
+              <el-input v-model="form.link" placeholder="请输入链接" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="排序" prop="sort">
+              <el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 150px" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <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-form-item label="图片" prop="imageUrl">
+          <image-upload v-model="form.imageUrl" :limit="1" :file-size="5" :file-type="['png', 'jpg', 'jpeg']" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确 认</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 配置商品对话框 -->
+    <el-dialog v-model="productDialog.visible" title="推荐商品" width="900px" append-to-body>
+      <div class="flex items-center gap-3 mb-4">
+        <el-button @click="handleAddProduct">新增商品</el-button>
+        <el-button @click="handleImportProduct">导入商品</el-button>
+      </div>
+      <el-table v-loading="productDialog.loading" :data="linkedProducts" border>
+        <el-table-column label="商品编号" align="center" prop="productNo" width="140" />
+        <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" prop="productName" min-width="200" show-overflow-tooltip />
+        <el-table-column label="价格" align="center" prop="price" width="120" />
+        <el-table-column label="操作" align="center" width="80">
+          <template #default="scope">
+            <el-link type="danger" :underline="false" @click="handleRemoveLinkedProduct(scope.row)">删除</el-link>
+          </template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button @click="productDialog.visible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 配置品牌对话框 -->
+    <el-dialog v-model="brandDialog.visible" :title="`配置品牌 - ${brandDialog.floorName}`" width="900px" append-to-body>
+      <div class="flex items-center gap-3 mb-4">
+        <el-button type="primary" @click="handleAddBrand">新增品牌</el-button>
+      </div>
+      <el-table v-loading="brandDialog.loading" :data="linkedBrands" border>
+        <el-table-column label="品牌编号" align="center" prop="brandNo" width="120" />
+        <el-table-column label="品牌名称" align="center" prop="brandName" min-width="200" show-overflow-tooltip />
+        <el-table-column label="品牌图片" align="center" width="100">
+          <template #default="scope">
+            <el-image v-if="scope.row.brandLogo" :src="scope.row.brandLogo" fit="contain" style="width: 60px; height: 40px" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="80">
+          <template #default="scope">
+            <el-link type="danger" :underline="false" @click="handleRemoveLinkedBrand(scope.row)">删除</el-link>
+          </template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button type="primary" @click="brandDialog.visible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 选择商品对话框 -->
+    <el-dialog v-model="selectProductDialog.visible" title="选择商品" width="900px" append-to-body>
+      <div class="flex items-center gap-3 mb-4">
+        <el-input v-model="selectProductDialog.productNo" placeholder="请输入商品编号" style="width: 200px" clearable />
+        <el-input v-model="selectProductDialog.productName" placeholder="请输入商品名称" style="width: 200px" clearable />
+        <el-button type="primary" @click="searchProducts">搜 索</el-button>
+      </div>
+      <el-table v-loading="selectProductDialog.loading" :data="productList" border @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-column label="价格" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.minSellingPrice || scope.row.marketPrice || '-' }}
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination v-show="selectProductDialog.total > 0" v-model:page="selectProductDialog.pageNum" v-model:limit="selectProductDialog.pageSize" :total="selectProductDialog.total" @pagination="getProductList" />
+      <template #footer>
+        <el-button type="primary" @click="confirmSelectProducts">确 定</el-button>
+        <el-button @click="selectProductDialog.visible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 选择品牌对话框 -->
+    <el-dialog v-model="selectBrandDialog.visible" title="选择品牌" width="900px" append-to-body>
+      <div class="flex items-center gap-3 mb-4">
+        <el-input v-model="selectBrandDialog.keyword" placeholder="请输入品牌编号或名称搜索" style="width: 300px" clearable />
+        <el-button type="primary" @click="searchBrands">搜 索</el-button>
+      </div>
+      <el-table v-loading="selectBrandDialog.loading" :data="brandList" border @selection-change="handleBrandSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="品牌编号" align="center" prop="brandNo" width="120" />
+        <el-table-column label="品牌名称" align="center" prop="brandName" min-width="180" show-overflow-tooltip />
+        <el-table-column label="品牌图片" align="center" width="100">
+          <template #default="scope">
+            <el-image v-if="scope.row.brandLogo" :src="scope.row.brandLogo" fit="contain" style="width: 60px; height: 40px" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination v-show="selectBrandDialog.total > 0" v-model:page="selectBrandDialog.pageNum" v-model:limit="selectBrandDialog.pageSize" :total="selectBrandDialog.total" @pagination="getBrandList" />
+      <template #footer>
+        <el-button type="primary" @click="confirmSelectBrands">确 定</el-button>
+        <el-button @click="selectBrandDialog.visible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 导入商品对话框 -->
+    <el-dialog v-model="importProductDialog.visible" title="导入商品" width="500px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item label="商品编号">
+          <el-input v-model="importProductDialog.productNos" type="textarea" :rows="5" placeholder="请输入商品编号,多个用逗号隔开" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="confirmImportProducts">确 定</el-button>
+        <el-button @click="importProductDialog.visible = false">取 消</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="StyleDesign" lang="ts">
+import { listSiteFloor, getSiteFloor, addSiteFloor, updateSiteFloor, delSiteFloor } from '@/api/product/siteFloor';
+import { SiteFloorVO, SiteFloorForm, SiteFloorQuery } from '@/api/product/siteFloor/types';
+import { listSiteFloorLink, addSiteFloorLink, delSiteFloorLink } from '@/api/product/siteFloorLink';
+import { listProduct } from '@/api/product/base';
+import { listBrand } from '@/api/product/brand';
+import { useRouter, useRoute } from 'vue-router';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const route = useRoute();
+
+// 路由参数
+const siteId = computed(() => route.query.siteId as string | undefined);
+const siteName = computed(() => route.query.siteName as string | undefined);
+
+const floorList = ref<SiteFloorVO[]>([]);
+const loading = ref(false);
+const total = ref(0);
+const dialogVisible = ref(false);
+const dialogTitle = ref('添加楼层');
+const submitLoading = ref(false);
+
+const queryParams = ref<SiteFloorQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  name: undefined,
+  siteId: siteId.value
+});
+
+const formRef = ref<ElFormInstance>();
+const form = ref<SiteFloorForm>({
+  name: undefined,
+  link: undefined,
+  imageUrl: undefined,
+  isShow: '1',
+  sort: 0,
+  siteId: siteId.value
+});
+
+const rules = {
+  name: [{ required: true, message: '请输入楼层名称', trigger: 'blur' }],
+  isShow: [{ required: true, message: '请选择是否显示', trigger: 'change' }]
+};
+
+// 配置商品弹框
+const productDialog = reactive({
+  visible: false,
+  loading: false,
+  floorId: null as string | number | null,
+  floorName: ''
+});
+const linkedProducts = ref<any[]>([]);
+
+// 配置品牌弹框
+const brandDialog = reactive({
+  visible: false,
+  loading: false,
+  floorId: null as string | number | null,
+  floorName: ''
+});
+const linkedBrands = ref<any[]>([]);
+
+// 选择商品弹框
+const selectProductDialog = reactive({
+  visible: false,
+  loading: false,
+  productNo: '',
+  productName: '',
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+const productList = ref<any[]>([]);
+const selectedProducts = ref<any[]>([]);
+
+// 选择品牌弹框
+const selectBrandDialog = reactive({
+  visible: false,
+  loading: false,
+  keyword: '',
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+const brandList = ref<any[]>([]);
+const selectedBrands = ref<any[]>([]);
+
+// 导入商品弹框
+const importProductDialog = reactive({
+  visible: false,
+  productNos: ''
+});
+
+/** 获取楼层列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listSiteFloor({ ...queryParams.value, siteId: siteId.value });
+    floorList.value = res.rows;
+    total.value = res.total;
+  } catch (error) {
+    console.error('获取楼层列表失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置 */
+const resetQuery = () => {
+  queryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    siteId: siteId.value
+  };
+  getList();
+};
+
+/** 返回 */
+const handleBack = () => {
+  router.push('/customerOperation/vipSite');
+};
+
+/** 添加楼层 */
+const handleAdd = () => {
+  form.value = { name: undefined, link: undefined, imageUrl: undefined, isShow: '1', sort: 0, siteId: siteId.value };
+  dialogTitle.value = '添加楼层';
+  dialogVisible.value = true;
+  nextTick(() => formRef.value?.clearValidate());
+};
+
+/** 编辑楼层 */
+const handleEdit = async (row: SiteFloorVO) => {
+  try {
+    const res = await getSiteFloor(row.id);
+    console.log('res.data', res.data);
+    form.value = { ...res.data };
+    dialogTitle.value = '编辑楼层';
+    dialogVisible.value = true;
+    nextTick(() => formRef.value?.clearValidate());
+  } catch (error) {
+    proxy?.$modal.msgError('获取楼层信息失败');
+  }
+};
+
+/** 切换显示状态 */
+const handleToggleShow = async (row: SiteFloorVO) => {
+  const newIsShow = row.isShow === '1' ? '0' : '1';
+  const text = newIsShow === '1' ? '显示' : '隐藏';
+  try {
+    await proxy?.$modal.confirm(`确认要将楼层"${row.name}"设置为"${text}"吗?`);
+    await updateSiteFloor({ id: row.id, isShow: newIsShow, siteId: row.siteId });
+    proxy?.$modal.msgSuccess('操作成功');
+    getList();
+  } catch {}
+};
+
+/** 删除楼层 */
+const handleDelete = async (row: SiteFloorVO) => {
+  try {
+    await proxy?.$modal.confirm(`确认要删除楼层"${row.name}"吗?`);
+    await delSiteFloor(row.id);
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  } catch {}
+};
+
+/** 提交表单 */
+const handleSubmit = async () => {
+  await formRef.value?.validate();
+  submitLoading.value = true;
+  try {
+    if (form.value.id) {
+      await updateSiteFloor(form.value as SiteFloorForm);
+      proxy?.$modal.msgSuccess('修改成功');
+    } else {
+      await addSiteFloor(form.value as SiteFloorForm);
+      proxy?.$modal.msgSuccess('添加成功');
+    }
+    dialogVisible.value = false;
+    getList();
+  } catch (error) {
+    console.error('提交失败:', error);
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+/** 配置商品 */
+const handleConfigProduct = (row: SiteFloorVO) => {
+  productDialog.floorId = row.id;
+  productDialog.floorName = row.name;
+  productDialog.visible = true;
+  getLinkedProducts();
+};
+
+/** 获取已关联商品 */
+const getLinkedProducts = async () => {
+  if (!productDialog.floorId) return;
+  productDialog.loading = true;
+  try {
+    const res = await listSiteFloorLink({ floorId: productDialog.floorId, type: 1, pageNum: 1, pageSize: 1000 });
+    const links = res.rows || [];
+    if (links.length === 0) {
+      linkedProducts.value = [];
+      return;
+    }
+    const productIds = links.map((item: any) => item.productId).filter(Boolean);
+    if (productIds.length > 0) {
+      const productRes = await listProduct({ ids: productIds.join(','), pageSize: 1000 });
+      const productMap = new Map<string, any>((productRes.rows || []).map((p: any) => [String(p.id), p]));
+      linkedProducts.value = links.map((item: any) => {
+        const p = productMap.get(String(item.productId)) || {};
+        return { ...item, productName: p.itemName || item.productName || '', productImage: p.productImage || item.productImage || '', price: p.minSellingPrice || p.marketPrice || '' };
+      });
+    } else {
+      linkedProducts.value = links;
+    }
+  } catch (error) {
+    console.error('加载关联商品失败', error);
+  } finally {
+    productDialog.loading = false;
+  }
+};
+
+/** 删除关联商品 */
+const handleRemoveLinkedProduct = (row: any) => {
+  proxy?.$modal.confirm('是否确认删除该商品?').then(() => {
+    delSiteFloorLink(row.id).then(() => {
+      proxy?.$modal.msgSuccess('删除成功');
+      getLinkedProducts();
+    });
+  });
+};
+
+/** 新增商品 */
+const handleAddProduct = () => {
+  selectProductDialog.productNo = '';
+  selectProductDialog.productName = '';
+  selectProductDialog.pageNum = 1;
+  selectProductDialog.visible = true;
+  getProductList();
+};
+
+/** 导入商品 */
+const handleImportProduct = () => {
+  importProductDialog.productNos = '';
+  importProductDialog.visible = true;
+};
+
+/** 确认导入商品 */
+const confirmImportProducts = async () => {
+  const input = importProductDialog.productNos.trim();
+  if (!input) {
+    proxy?.$modal.msgWarning('请输入商品编号');
+    return;
+  }
+  const productNos = input.split(/[,,\s\n]+/).map((s: string) => s.trim()).filter(Boolean);
+  if (productNos.length === 0) {
+    proxy?.$modal.msgWarning('请输入有效的商品编号');
+    return;
+  }
+  const existingProductNos = linkedProducts.value.map((item: any) => item.productNo);
+  const newProductNos = productNos.filter((no: string) => !existingProductNos.includes(no));
+  if (newProductNos.length === 0) {
+    proxy?.$modal.msgWarning('所有商品编号已存在,请勿重复添加');
+    return;
+  }
+  try {
+    const productRes = await listProduct({ productNos: newProductNos.join(','), pageSize: 1000 });
+    const productMap = new Map<string, any>((productRes.rows || []).map((p: any) => [p.productNo, p]));
+    for (const productNo of newProductNos) {
+      const product = productMap.get(productNo);
+      if (!product) {
+        console.warn(`商品编号 ${productNo} 不存在`);
+        continue;
+      }
+      await addSiteFloorLink({ floorId: productDialog.floorId, type: 1, productId: product.id, productNo, sort: 0, status: '0' });
+    }
+    const skipped = productNos.length - newProductNos.length;
+    proxy?.$modal.msgSuccess(skipped > 0 ? `导入成功,${skipped}个商品已存在被跳过` : '导入成功');
+    importProductDialog.visible = false;
+    getLinkedProducts();
+  } catch (error) {
+    proxy?.$modal.msgError('导入失败');
+  }
+};
+
+/** 获取商品列表 */
+const getProductList = async () => {
+  selectProductDialog.loading = true;
+  try {
+    const res = await listProduct({
+      productNo: selectProductDialog.productNo,
+      keyword: selectProductDialog.productName,
+      pageNum: selectProductDialog.pageNum,
+      pageSize: selectProductDialog.pageSize
+    });
+    productList.value = res.rows || [];
+    selectProductDialog.total = res.total || 0;
+  } catch (error) {
+    console.error('加载商品列表失败', error);
+  } finally {
+    selectProductDialog.loading = false;
+  }
+};
+
+/** 搜索商品 */
+const searchProducts = () => {
+  selectProductDialog.pageNum = 1;
+  getProductList();
+};
+
+/** 商品选择变化 */
+const handleProductSelectionChange = (selection: any[]) => {
+  selectedProducts.value = selection;
+};
+
+/** 确认选择商品 */
+const confirmSelectProducts = async () => {
+  if (selectedProducts.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择商品');
+    return;
+  }
+  const existingProductNos = linkedProducts.value.map((item: any) => item.productNo);
+  const duplicates = selectedProducts.value.filter((p: any) => existingProductNos.includes(p.productNo));
+  if (duplicates.length > 0) {
+    const names = duplicates.map((p: any) => p.itemName || p.productNo).join('、');
+    proxy?.$modal.msgWarning(`商品 ${names} 已存在,请勿重复添加`);
+    return;
+  }
+  try {
+    for (const product of selectedProducts.value) {
+      await addSiteFloorLink({ floorId: productDialog.floorId, type: 1, productId: product.id, productNo: product.productNo, sort: 0, status: '0' });
+    }
+    proxy?.$modal.msgSuccess('添加成功');
+    selectProductDialog.visible = false;
+    getLinkedProducts();
+  } catch (error) {
+    proxy?.$modal.msgError('添加失败');
+  }
+};
+
+/** 配置品牌 */
+const handleConfigBrand = (row: SiteFloorVO) => {
+  brandDialog.floorId = row.id;
+  brandDialog.floorName = row.name;
+  brandDialog.visible = true;
+  getLinkedBrands();
+};
+
+/** 获取已关联品牌 */
+const getLinkedBrands = async () => {
+  if (!brandDialog.floorId) return;
+  brandDialog.loading = true;
+  try {
+    const res = await listSiteFloorLink({ floorId: brandDialog.floorId, type: 2, pageNum: 1, pageSize: 1000 });
+    const links = res.rows || [];
+    if (links.length === 0) {
+      linkedBrands.value = [];
+      return;
+    }
+    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 || '' };
+    });
+  } catch (error) {
+    console.error('加载关联品牌失败', error);
+  } finally {
+    brandDialog.loading = false;
+  }
+};
+
+/** 删除关联品牌 */
+const handleRemoveLinkedBrand = (row: any) => {
+  proxy?.$modal.confirm('是否确认删除该品牌?').then(() => {
+    delSiteFloorLink(row.id).then(() => {
+      proxy?.$modal.msgSuccess('删除成功');
+      getLinkedBrands();
+    });
+  });
+};
+
+/** 新增品牌 */
+const handleAddBrand = () => {
+  selectBrandDialog.keyword = '';
+  selectBrandDialog.pageNum = 1;
+  selectBrandDialog.visible = true;
+  getBrandList();
+};
+
+/** 获取品牌列表 */
+const getBrandList = async () => {
+  selectBrandDialog.loading = true;
+  try {
+    const res = await listBrand({ keyword: selectBrandDialog.keyword, pageNum: selectBrandDialog.pageNum, pageSize: selectBrandDialog.pageSize });
+    brandList.value = res.rows || [];
+    selectBrandDialog.total = res.total || 0;
+  } catch (error) {
+    console.error('加载品牌列表失败', error);
+  } finally {
+    selectBrandDialog.loading = false;
+  }
+};
+
+/** 搜索品牌 */
+const searchBrands = () => {
+  selectBrandDialog.pageNum = 1;
+  getBrandList();
+};
+
+/** 品牌选择变化 */
+const handleBrandSelectionChange = (selection: any[]) => {
+  selectedBrands.value = selection;
+};
+
+/** 确认选择品牌 */
+const confirmSelectBrands = async () => {
+  if (selectedBrands.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择品牌');
+    return;
+  }
+  const existingBrandNos = linkedBrands.value.map((item: any) => item.brandNo);
+  const duplicates = selectedBrands.value.filter((b: any) => existingBrandNos.includes(b.brandNo));
+  if (duplicates.length > 0) {
+    const names = duplicates.map((b: any) => b.brandName || b.brandNo).join('、');
+    proxy?.$modal.msgWarning(`品牌 ${names} 已存在,请勿重复添加`);
+    return;
+  }
+  try {
+    for (const brand of selectedBrands.value) {
+      await addSiteFloorLink({ floorId: brandDialog.floorId, type: 2, brandId: brand.id, brandNo: brand.brandNo, sort: 0, status: '0' });
+    }
+    proxy?.$modal.msgSuccess('添加成功');
+    selectBrandDialog.visible = false;
+    getLinkedBrands();
+  } catch (error) {
+    proxy?.$modal.msgError('添加失败');
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 8 - 4
src/views/platform/industrial/brandFloor/index.vue

@@ -156,7 +156,8 @@
     <!-- 选择商品对话框 -->
     <el-dialog v-model="selectProductDialog.visible" title="选择商品" width="900px" append-to-body>
       <div class="select-dialog-header">
-        <el-input v-model="selectProductDialog.keyword" placeholder="请输入商品编号或名称搜索" style="width: 300px" />
+        <el-input v-model="selectProductDialog.productNo" placeholder="请输入商品编号" style="width: 200px" clearable />
+        <el-input v-model="selectProductDialog.productName" placeholder="请输入商品名称" style="width: 200px" clearable />
         <el-button type="primary" @click="searchProducts">搜 索</el-button>
       </div>
       <el-table v-loading="selectProductDialog.loading" :data="productList" border @selection-change="handleProductSelectionChange">
@@ -305,7 +306,8 @@ const linkedBrands = ref<any[]>([]);
 const selectProductDialog = reactive({
   visible: false,
   loading: false,
-  keyword: '',
+  productNo: '',
+  productName: '',
   pageNum: 1,
   pageSize: 10,
   total: 0
@@ -493,7 +495,8 @@ const handleRemoveLinked = (row: any) => {
 
 // 新增商品
 const handleAddProduct = () => {
-  selectProductDialog.keyword = '';
+  selectProductDialog.productNo = '';
+  selectProductDialog.productName = '';
   selectProductDialog.pageNum = 1;
   selectProductDialog.visible = true;
   getProductList();
@@ -567,7 +570,8 @@ const getProductList = async () => {
   selectProductDialog.loading = true;
   try {
     const res = await listProduct({
-      keyword: selectProductDialog.keyword,
+      productNo: selectProductDialog.productNo,
+      keyword: selectProductDialog.productName,
       pageNum: selectProductDialog.pageNum,
       pageSize: selectProductDialog.pageSize
     });