weixin_52219567 пре 1 месец
родитељ
комит
8c13dd9475

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

@@ -62,3 +62,11 @@ export function pcAddDiy(data: any) {
     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;
 }
 /**
  * 状态数量统计视图对象

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


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


+ 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>

+ 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>

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

@@ -160,6 +160,7 @@ const usePcdiyStore = defineStore('pcdiy', {
         newItem.realType = 1;
         newItem.realDataType = 1;
         newItem.realNumber = 5;
+        newItem.realIds = [];
         newItem.navlList = [
           {
             imageUrl: '',
@@ -332,6 +333,61 @@ const usePcdiyStore = defineStore('pcdiy', {
         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;
@@ -342,6 +398,9 @@ const usePcdiyStore = defineStore('pcdiy', {
         newItem.goodsType = 1;
         newItem.goodsIds = [];
         newItem.goodsClassify = '';
+        newItem.topCategoryId = '';
+        newItem.mediumCategoryId = '';
+        newItem.bottomCategoryId = '';
         newItem.goodsNumber = 5;
         newItem.goodsSort = 1;
         newItem.goodsBrand = '';
@@ -355,6 +414,131 @@ const usePcdiyStore = defineStore('pcdiy', {
         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);

+ 33 - 11
src/views/diy/pcEdit.vue

@@ -10,7 +10,7 @@
         </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]">正在装修:页面名字</span>
+          <span class="mr-[5px] text-[14px]">正在装修:{{ query.title || '页面名字' }}</span>
         </div>
         <div class="flex-1"></div>
         <el-button @click="preview()">保存并预览</el-button>
@@ -174,7 +174,8 @@ 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);
@@ -215,11 +216,26 @@ 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);
 //左边得组件
@@ -293,7 +309,9 @@ const collapse = ref<any>([
       {
         name: '楼层组件',
         icon: 'iconfont iconshangpinliebiaopc',
-        id: 10
+        id: 10,
+        components: markRaw(floorRef.value),
+        edit: markRaw(floorEditRef.value)
       },
       {
         name: '商品组件',
@@ -305,12 +323,16 @@ const collapse = ref<any>([
       {
         name: '多商品组',
         icon: 'iconfont iconduoshangpinzupc',
-        id: 12
+        id: 12,
+        components: markRaw(goodsListRef.value),
+        edit: markRaw(goodsListEditRef.value)
       },
       {
         name: '发现组件',
         icon: 'iconfont iconrequpc',
-        id: 13
+        id: 13,
+        components: markRaw(discoverRef.value),
+        edit: markRaw(discoverEditRef.value)
       }
     ]
   }
@@ -331,10 +353,10 @@ const preview = () => {};
 // 保存
 const save = () => {
   const datas = {
-    name: '首页',
+    name: query.title,
     siteId: '',
     clientId: '',
-    type: 1,
+    type: query.type,
     remark: '',
     previewPicUrls: '',
     property: JSON.stringify(diyStore.componentList),
@@ -347,7 +369,6 @@ const save = () => {
       }
     })
     .catch(() => {});
-  console.log(diyStore.componentList, '?????????????');
 };
 </script>
 
@@ -358,7 +379,7 @@ const save = () => {
 }
 .pcEdit-pages {
   min-height: calc(100vh - 84px);
-  min-width: 1900px;
+  // min-width: 1900px;
 
   .full-container {
     height: calc(100vh - 134px);
@@ -385,10 +406,11 @@ const save = () => {
       .preview-pages {
         margin: 30px auto;
         width: 1300px;
-        height: calc(130vh - 194px);
         background: var(--el-bg-color-page);
         overflow: auto;
-        zoom: 0.7;
+        // height: calc(130vh - 194px);
+        // zoom: 0.7;
+        height: calc(100vh - 194px);
 
         /* 为了兼容某些情况,可能还需要配合 display */
         display: inline-block;

+ 4 - 10
src/views/diy/pcEdit/article-edit.vue

@@ -15,7 +15,7 @@
             <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="showStyle">
+            <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>
@@ -60,15 +60,9 @@
     <!-- 手动选择 -->
     <el-dialog v-model="showDialog" title="选择文章">
       <div class="data-bos">
-        <el-input
-          v-model="queryParams.caseTitle"
-          placeholder="请输入服务标题"
-          clearable
-          style="width: 300px; margin-bottom: 10px"
-          @keyup.enter="handleQuery"
-        >
+        <el-input v-model="queryParams.caseTitle" placeholder="请输入服务标题" clearable style="width: 300px; margin-bottom: 10px">
           <template #append>
-            <el-button :icon="Search" />
+            <el-button @click="handleQuery" :icon="Search" />
           </template>
         </el-input>
         <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
@@ -160,7 +154,7 @@ const getList = async () => {
 };
 
 //打开弹窗
-const showStyle = () => {
+const openDialog = () => {
   showDialog.value = true;
   getList();
 };

+ 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>

+ 98 - 5
src/views/diy/pcEdit/goods-edit.vue

@@ -80,17 +80,39 @@
           <el-form-item label="商品分类" v-if="diyStore.editComponent.goodsType == 2">
             <el-tree-select
               v-model="diyStore.editComponent.goodsClassify"
-              :data="categoryOptions"
+              :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">
-            <el-input-number v-model="diyStore.editComponent.goodsNumber" :min="0" />
-            <span class="annotation3">(0代表不限)</span>
+            <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">
@@ -241,11 +263,14 @@ 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);
@@ -253,10 +278,13 @@ 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'
@@ -298,8 +326,12 @@ const getList = async () => {
 
 /** 查询分类树 */
 const getCategoryTree = async () => {
+  categoryOptions.value = [];
+  categoryOptions1.value = [];
   const res = await categoryTree();
-  categoryOptions.value = res.data || [];
+  const list = res.data || [];
+  categoryOptions.value = [...list];
+  categoryOptions1.value = [...list];
   categoryOptions.value.unshift({
     id: '',
     label: '全部'
@@ -313,7 +345,16 @@ const openDialog = () => {
 };
 
 const handleNodeClick = (data: any) => {
-  queryParams.bottomCategoryId = data.id;
+  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();
 };
 // 监听表格单行选中
@@ -352,6 +393,58 @@ const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) =
   // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
   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>

+ 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>

+ 137 - 1
src/views/diy/pcEdit/head-edit.vue

@@ -236,9 +236,16 @@
                 <el-radio :value="2">手动</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="文章数量">
+            <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">
@@ -335,6 +342,34 @@
       <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>
 
@@ -343,8 +378,23 @@ 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) {
@@ -423,6 +473,76 @@ const confirmCallBack = (res: any, type: any) => {
     }
   });
 };
+
+/** 搜索 */
+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>
@@ -446,6 +566,16 @@ const confirmCallBack = (res: any, type: any) => {
       }
     }
 
+    .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;
@@ -493,6 +623,12 @@ const confirmCallBack = (res: any, type: any) => {
     line-height: 14px;
   }
 
+  .selected {
+    line-height: 32px;
+    position: absolute;
+    left: 0px;
+  }
+
   :deep(.el-form-item__label) {
     font-weight: 400;
   }

+ 3 - 2
src/views/diy/pcEdit/textTitle-edit.vue

@@ -19,8 +19,9 @@
           <el-form-item label="标题名称" class="flex">
             <el-input v-model="diyStore.editComponent.title" placeholder="请输入标题内容" />
           </el-form-item>
-          <el-form-item label="链接地址" class="flex">
-            <el-input v-model="diyStore.editComponent.titleUrl" placeholder="请输入链接地址" />
+          <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">

+ 36 - 36
src/views/diy/pcList.vue

@@ -4,13 +4,15 @@
       <el-card shadow="hover">
         <el-form ref="queryFormRef" :model="queryParams" :inline="true">
           <el-form-item label="页面名称">
-            <el-input v-model="queryParams.title" placeholder="请输入页面名称" clearable @keyup.enter="handleQuery" />
+            <el-input v-model="queryParams.name" placeholder="请输入页面名称" clearable @keyup.enter="handleQuery" />
           </el-form-item>
           <el-form-item label="所属应用">
-            <el-input v-model="queryParams.addon_name" placeholder="请输入所属应用" clearable @keyup.enter="handleQuery" />
-          </el-form-item>
-          <el-form-item label="页面类型">
-            <el-input v-model="queryParams.type" placeholder="请输入页面类型" clearable @keyup.enter="handleQuery" />
+            <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>
@@ -19,7 +21,6 @@
         </el-form>
       </el-card>
     </div>
-
     <el-card shadow="hover">
       <template #header>
         <el-row :gutter="10" class="mb8">
@@ -33,27 +34,31 @@
           </el-col>
         </el-row>
       </template>
-
       <el-table v-loading="loading" border :data="dataList">
-        <el-table-column label="页面名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
-        <el-table-column label="所属应用" align="center" :show-overflow-tooltip="true">
-          <template #default="scope">
-            {{ scope.row.dictType }}
-          </template>
-        </el-table-column>
+        <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.dictType }}
+            {{
+              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" :show-overflow-tooltip="true">
+        <el-table-column label="状态" align="center" width="150">
           <template #default="scope">
-            {{ scope.row.dictType }}
+            {{ scope.row.isHome == 1 ? '开启' : '关闭' }}
           </template>
         </el-table-column>
-        <el-table-column label="更新时间" align="center" :show-overflow-tooltip="true">
+        <el-table-column label="更新时间" align="center" :show-overflow-tooltip="true" width="200">
           <template #default="scope">
-            {{ scope.row.dictType }}
+            {{ scope.row.updateTime }}
           </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
@@ -78,7 +83,10 @@
         </el-form-item>
         <el-form-item label="页面类型" prop="type">
           <el-select v-model="formData.type" placeholder="请选择页面类型" class="!w-full">
-            <el-option v-for="(item, key) in pageType" :label="item.title" :value="key" :key="key" />
+            <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>
@@ -94,8 +102,9 @@
 </template>
 
 <script setup lang="ts">
-import { pcDiyList, template } from '@/api/diy/index';
+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);
@@ -161,21 +170,13 @@ const handleAdd = () => {
 /** 修改按钮操作 */
 const handleUpdate = async (row?: any) => {};
 /** 删除按钮操作 */
-const handleDelete = async (row?: any) => {};
-
-const getTemplate = () => {
-  template({ mode: '', addon: '' }).then((res) => {
-    if (res.code == 200) {
-      for (const key in pageType) {
-        delete pageType[key];
-      }
-
-      for (const key in res.data) {
-        pageType[key] = res.data[key];
-      }
-    }
-  });
+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;
 
@@ -183,7 +184,7 @@ const addEvent = async (formEl: FormInstance | undefined) => {
     if (valid) {
       const query = { type: formData.type, title: formData.title };
       const url = router.resolve({
-        path: '/diy/edit',
+        path: '/diy/pcedit',
         query
       });
       window.open(url.href);
@@ -195,7 +196,6 @@ const addEvent = async (formEl: FormInstance | undefined) => {
 };
 onMounted(() => {
   getList();
-  getTemplate();
 });
 </script>
 

+ 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>

+ 75 - 23
src/views/diy/pcPages/goods.vue

@@ -41,38 +41,90 @@ const props = defineProps<{
   index: number; // 确保声明 index 为可选属性
 }>();
 const componentData = diyStore.componentList[props.index];
-const dataList = ref<any>([]);
+const dataList = ref<any>([{}, {}, {}, {}, {}]);
 
 onMounted(() => {
   getDataList();
 });
 
 const getDataList = () => {
-  dataList.value = [];
-  listBase({ pageNum: 1, pageSize: 16 }).then((res) => {
-    if (res.code == 200) {
-      dataList.value = res.rows;
+  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;
+        }
+      });
     }
-  });
-  // // 默认
-  // if (componentData.dataType == 1) {
-  //   listBase({ pageNum: 1, pageSize: 16 }).then((res) => {
-  //     if (res.code == 200) {
-  //       dataList.value = res.rows;
-  //     }
-  //   });
-  // } else {
-  //   //手动选择
-  //   listBase({ 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);
-  //     }
-  //   });
-  // }
+  } 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;';

+ 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>

+ 45 - 1
src/views/diy/pcPages/head.vue

@@ -143,7 +143,9 @@
               </div>
             </div>
             <template v-for="(item, index) in realList" :key="index">
-              <div class="real-list ellipsis" v-if="Number(index) < 7">{{ item.announcementTitle }}</div>
+              <div class="real-list ellipsis" v-if="componentData.realDataType == 2 ? true : Number(index) < componentData.realNumber">
+                {{ item.announcementTitle }}
+              </div>
             </template>
           </div>
           <div class="interests">
@@ -168,11 +170,53 @@
 <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>([
   {

+ 1 - 1
src/views/diy/pcPages/textTitle.vue

@@ -3,7 +3,7 @@
     <div :style="warpCss">
       <!-- 风格1 -->
       <div :style="boxCss" v-if="componentData.styleType == 1">
-        <div :style="titleCss">{{ componentData.title }}</div>
+        <div :style="titleCss">{{ componentData.title }}{{ componentData.titleUrl }}</div>
       </div>
       <!-- 风格2 -->
       <div :style="boxCss" class="style2 flex-row-center" v-else-if="componentData.styleType == 2">