Эх сурвалжийг харах

feat(product): 添加产品方案管理模块

- 新增采购项目发布方案表单页面,支持标题、分类、封面图、价格等字段配置
- 实现采购项目增删改查功能,集成分类、场景、行业等下拉选项数据
- 添加采购项目产品关联管理功能,支持批量添加商品到分组
- 集成文件选择器组件,支持封面图和方案文件的上传与选择
- 实现游标分页功能,在Pagination组件中新增cursorMode模式
- 完善采购组、采购主题等相关API接口定义
- 添加路由配置支持产品方案相关页面导航
肖路 2 сар өмнө
parent
commit
5bcc011646

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

@@ -710,12 +710,25 @@ export interface BaseQuery extends PageQuery {
    */
   projectOrg?: string;
 
+  /**
+   * 游标分页参数:最后一条记录的ID
+   */
+  lastSeenId?: string | number;
 
+  /**
+   * 游标分页参数:第一条记录的ID
+   */
+  firstSeenId?: string | number;
 
-    /**
-     * 日期范围参数
-     */
-    params?: any;
+  /**
+   * 游标分页参数:翻页方向(0=上一页,1=下一页)
+   */
+  way?: number;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
 }
 /**
  * 状态数量统计视图对象

+ 63 - 0
src/api/product/group/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { GroupVO, GroupForm, GroupQuery } from '@/api/product/group/types';
+
+/**
+ * 查询采购组信息列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listGroup = (query?: GroupQuery): AxiosPromise<GroupVO[]> => {
+  return request({
+    url: '/product/group/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询采购组信息详细
+ * @param id
+ */
+export const getGroup = (id: string | number): AxiosPromise<GroupVO> => {
+  return request({
+    url: '/product/group/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增采购组信息
+ * @param data
+ */
+export const addGroup = (data: GroupForm) => {
+  return request({
+    url: '/product/group',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改采购组信息
+ * @param data
+ */
+export const updateGroup = (data: GroupForm) => {
+  return request({
+    url: '/product/group',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除采购组信息
+ * @param id
+ */
+export const delGroup = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/group/' + id,
+    method: 'delete'
+  });
+};

+ 120 - 0
src/api/product/group/types.ts

@@ -0,0 +1,120 @@
+export interface GroupVO {
+  /**
+   * 主键ID,自增
+   */
+  id: string | number;
+
+  /**
+   * 项目id
+   */
+  programId: string | number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage: string;
+
+  /**
+   * 封面图片路径或URLUrl
+   */
+  coverImageUrl: string;
+  /**
+   * 标题
+   */
+  title: string;
+
+  /**
+   * 副标题
+   */
+  subtitle: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+}
+
+export interface GroupForm extends BaseEntity {
+  /**
+   * 主键ID,自增
+   */
+  id?: string | number;
+
+  /**
+   * 项目id
+   */
+  programId?: string | number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+}
+
+export interface GroupQuery extends PageQuery {
+
+  /**
+   * 项目id
+   */
+  programId?: string | number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 平台标识
+   */
+  platformCode?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 63 - 0
src/api/product/program/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ProgramVO, ProgramForm, ProgramQuery } from '@/api/product/program/types';
+
+/**
+ * 查询采购项目列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listProgram = (query?: ProgramQuery): AxiosPromise<ProgramVO[]> => {
+  return request({
+    url: '/product/procurementProgram/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询采购项目详细
+ * @param id
+ */
+export const getProgram = (id: string | number): AxiosPromise<ProgramVO> => {
+  return request({
+    url: '/product/procurementProgram/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增采购项目
+ * @param data
+ */
+export const addProgram = (data: ProgramForm) => {
+  return request({
+    url: '/product/procurementProgram',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改采购项目
+ * @param data
+ */
+export const updateProgram = (data: ProgramForm) => {
+  return request({
+    url: '/product/procurementProgram',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除采购项目
+ * @param id
+ */
+export const delProgram = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/procurementProgram/' + id,
+    method: 'delete'
+  });
+};

+ 315 - 0
src/api/product/program/types.ts

@@ -0,0 +1,315 @@
+export interface ProgramVO {
+  /**
+   * 主键ID,自增
+   */
+  id: string | number;
+
+  /**
+   * 项目编号
+   */
+  programNo: string;
+
+  /**
+   * 推文标题
+   */
+  tweetsTitle: string;
+
+  /**
+   * 副标题
+   */
+  subtitle: string;
+
+  /**
+   * 描述
+   */
+  programDescribe: string;
+
+  /**
+   * 推文分类
+   */
+  tweetsCategory: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow: number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage: string;
+
+  /**
+   * 封面图片路径或URLUrl
+   */
+  coverImageUrl: string;
+  /**
+   * 图片列表
+   */
+  imageList: string;
+
+  /**
+   * 点击次数
+   */
+  clicks: number;
+
+  /**
+   * 收藏次数
+   */
+  collects: number;
+
+  /**
+   * 价格
+   */
+  price: string;
+
+  /**
+   * 适配编号
+   */
+  adaptNo: string;
+
+  /**
+   * 标签
+   */
+  lable: string;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme: string;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry: string;
+
+  /**
+   * 无效时间
+   */
+  invalidTime: string | number;
+
+  /**
+   * 文件名
+   */
+  filename: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+}
+
+export interface ProgramForm extends BaseEntity {
+  /**
+   * 主键ID,自增
+   */
+  id?: string | number;
+
+  /**
+   * 项目编号
+   */
+  programNo?: string;
+
+  /**
+   * 推文标题
+   */
+  tweetsTitle?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 描述
+   */
+  programDescribe?: string;
+
+  /**
+   * 推文分类
+   */
+  tweetsCategory?: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow?: number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 图片列表
+   */
+  imageList?: string;
+
+  /**
+   * 点击次数
+   */
+  clicks?: number;
+
+  /**
+   * 收藏次数
+   */
+  collects?: number;
+
+  /**
+   * 价格
+   */
+  price?: string;
+
+  /**
+   * 适配编号
+   */
+  adaptNo?: string;
+
+  /**
+   * 标签
+   */
+  lable?: string;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme?: string;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry?: string;
+
+  /**
+   * 无效时间
+   */
+  invalidTime?: string | number;
+
+  /**
+   * 文件名
+   */
+  filename?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+}
+
+export interface ProgramQuery extends PageQuery {
+
+  /**
+   * 项目编号
+   */
+  programNo?: string;
+
+  /**
+   * 推文标题
+   */
+  tweetsTitle?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 描述
+   */
+  programDescribe?: string;
+
+  /**
+   * 推文分类
+   */
+  tweetsCategory?: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow?: number;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 图片列表
+   */
+  imageList?: string;
+
+  /**
+   * 点击次数
+   */
+  clicks?: number;
+
+  /**
+   * 收藏次数
+   */
+  collects?: number;
+
+  /**
+   * 价格
+   */
+  price?: string;
+
+  /**
+   * 适配编号
+   */
+  adaptNo?: string;
+
+  /**
+   * 标签
+   */
+  lable?: string;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme?: string;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry?: string;
+
+  /**
+   * 无效时间
+   */
+  invalidTime?: string | number;
+
+  /**
+   * 文件名
+   */
+  filename?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 平台标识
+   */
+  platformCode?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 75 - 0
src/api/product/programProduct/index.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ProgramProductVO, ProgramProductForm, ProgramProductQuery } from '@/api/product/programProduct/types';
+import { BaseQuery, BaseVO } from '@/api/pmsProduct/base/types';
+
+/**
+ * 查询采购项目产品关联列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listProgramProduct = (query?: ProgramProductQuery): AxiosPromise<ProgramProductVO[]> => {
+  return request({
+    url: '/product/programProduct/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询采购项目产品关联详细
+ * @param id
+ */
+export const getProgramProduct = (id: string | number): AxiosPromise<ProgramProductVO> => {
+  return request({
+    url: '/product/programProduct/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增采购项目产品关联
+ * @param data
+ */
+export const addProgramProduct = (data: ProgramProductForm) => {
+  return request({
+    url: '/product/programProduct',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改采购项目产品关联
+ * @param data
+ */
+export const updateProgramProduct = (data: ProgramProductForm) => {
+  return request({
+    url: '/product/programProduct',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除采购项目产品关联
+ * @param id
+ */
+export const delProgramProduct = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/programProduct/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取分组下的商品列表
+ * */
+export const getGroupProductList = (query?: BaseQuery): AxiosPromise<BaseVO[]> => {
+  return request({
+    url: '/product/programProduct/groupProductPage',
+    method: 'get',
+    params: query
+  });
+};

+ 101 - 0
src/api/product/programProduct/types.ts

@@ -0,0 +1,101 @@
+export interface ProgramProductVO {
+  /**
+   * 主键ID,自增
+   */
+  id: string | number;
+
+  /**
+   * 组号
+   */
+  groupId: string | number;
+
+  /**
+   * 产品编号
+   */
+  productId: string | number;
+
+  /**
+   * 是否为主产品(例如:1-是,0-否)
+   */
+  isMain: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+}
+
+export interface ProgramProductForm extends BaseEntity {
+  /**
+   * 主键ID,自增
+   */
+  id?: string | number;
+
+  /**
+   * 组号
+   */
+  groupId?: string | number;
+
+  /**
+   * 产品编号
+   */
+  productId?: string | number;
+
+  /**
+   * 是否为主产品(例如:1-是,0-否)
+   */
+  isMain?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+}
+
+export interface ProgramProductQuery extends PageQuery {
+
+  /**
+   * 组号
+   */
+  groupId?: string | number;
+
+  /**
+   * 产品编号
+   */
+  productId?: string | number;
+
+  /**
+   * 是否为主产品(例如:1-是,0-否)
+   */
+  isMain?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 平台标识
+   */
+  platformCode?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 63 - 0
src/api/product/topics/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TopicsVO, TopicsForm, TopicsQuery } from '@/api/product/topics/types';
+
+/**
+ * 查询采购主题列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listTopics = (query?: TopicsQuery): AxiosPromise<TopicsVO[]> => {
+  return request({
+    url: '/product/topics/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询采购主题详细
+ * @param id
+ */
+export const getTopics = (id: string | number): AxiosPromise<TopicsVO> => {
+  return request({
+    url: '/product/topics/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增采购主题
+ * @param data
+ */
+export const addTopics = (data: TopicsForm) => {
+  return request({
+    url: '/product/topics',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改采购主题
+ * @param data
+ */
+export const updateTopics = (data: TopicsForm) => {
+  return request({
+    url: '/product/topics',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除采购主题
+ * @param id
+ */
+export const delTopics = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/product/topics/' + id,
+    method: 'delete'
+  });
+};

+ 375 - 0
src/api/product/topics/types.ts

@@ -0,0 +1,375 @@
+export interface TopicsVO {
+  /**
+   * 主键ID,自增
+   */
+  id: string | number;
+
+  /**
+   * 采购主题编号
+   */
+  procurementTopicsId: string | number;
+
+  /**
+   * 标题
+   */
+  title: string;
+
+  /**
+   * 推文类型
+   */
+  tweetType: string;
+
+  /**
+   * 推文类别
+   */
+  tweetCategory: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow: number;
+
+  /**
+   * 发布时间
+   */
+  releaseTime: string;
+
+  /**
+   * 专属客户
+   */
+  exclusiveClient: string;
+
+  /**
+   * 内容
+   */
+  content: string;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage: string;
+
+  /**
+   * 封面图片路径或URLUrl
+   */
+  coverImageUrl: string;
+  /**
+   * 副标题
+   */
+  subtitle: string;
+
+  /**
+   * 点赞数
+   */
+  praise: number;
+
+  /**
+   * 发布状态(例如:0-草稿,1-已发布)
+   */
+  releaseStatus: number;
+
+  /**
+   * 适配编号
+   */
+  adaptNo: string;
+
+  /**
+   * 价格
+   */
+  price: string;
+
+  /**
+   * 标签
+   */
+  lable: string;
+
+  /**
+   * 产品数量
+   */
+  productCount: number;
+
+  /**
+   * 是否首页展示(例如:1-是,0-否)
+   */
+  isHome: number;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme: string;
+
+  /**
+   * 是否相关(例如:1-是,0-否)
+   */
+  isRelevant: number;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry: string;
+
+  /**
+   * 描述
+   */
+  topicsDescribe: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+}
+
+export interface TopicsForm extends BaseEntity {
+  /**
+   * 主键ID,自增
+   */
+  id?: string | number;
+
+  /**
+   * 采购主题编号
+   */
+  procurementTopicsId?: string | number;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 推文类型
+   */
+  tweetType?: string;
+
+  /**
+   * 推文类别
+   */
+  tweetCategory?: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow?: number;
+
+  /**
+   * 发布时间
+   */
+  releaseTime?: string;
+
+  /**
+   * 专属客户
+   */
+  exclusiveClient?: string;
+
+  /**
+   * 内容
+   */
+  content?: string;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 点赞数
+   */
+  praise?: number;
+
+  /**
+   * 发布状态(例如:0-草稿,1-已发布)
+   */
+  releaseStatus?: number;
+
+  /**
+   * 适配编号
+   */
+  adaptNo?: string;
+
+  /**
+   * 价格
+   */
+  price?: string;
+
+  /**
+   * 标签
+   */
+  lable?: string;
+
+  /**
+   * 产品数量
+   */
+  productCount?: number;
+
+  /**
+   * 是否首页展示(例如:1-是,0-否)
+   */
+  isHome?: number;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme?: string;
+
+  /**
+   * 是否相关(例如:1-是,0-否)
+   */
+  isRelevant?: number;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry?: string;
+
+  /**
+   * 描述
+   */
+  topicsDescribe?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+}
+
+export interface TopicsQuery extends PageQuery {
+
+  /**
+   * 采购主题编号
+   */
+  procurementTopicsId?: string | number;
+
+  /**
+   * 标题
+   */
+  title?: string;
+
+  /**
+   * 推文类型
+   */
+  tweetType?: string;
+
+  /**
+   * 推文类别
+   */
+  tweetCategory?: string;
+
+  /**
+   * 是否显示(例如:1-显示,0-不显示)
+   */
+  isShow?: number;
+
+  /**
+   * 发布时间
+   */
+  releaseTime?: string;
+
+  /**
+   * 专属客户
+   */
+  exclusiveClient?: string;
+
+  /**
+   * 内容
+   */
+  content?: string;
+
+  /**
+   * 封面图片路径或URL
+   */
+  coverImage?: string;
+
+  /**
+   * 副标题
+   */
+  subtitle?: string;
+
+  /**
+   * 点赞数
+   */
+  praise?: number;
+
+  /**
+   * 发布状态(例如:0-草稿,1-已发布)
+   */
+  releaseStatus?: number;
+
+  /**
+   * 适配编号
+   */
+  adaptNo?: string;
+
+  /**
+   * 价格
+   */
+  price?: string;
+
+  /**
+   * 标签
+   */
+  lable?: string;
+
+  /**
+   * 产品数量
+   */
+  productCount?: number;
+
+  /**
+   * 是否首页展示(例如:1-是,0-否)
+   */
+  isHome?: number;
+
+  /**
+   * 上传方案
+   */
+  uploadScheme?: string;
+
+  /**
+   * 是否相关(例如:1-是,0-否)
+   */
+  isRelevant?: number;
+
+  /**
+   * 适配行业
+   */
+  adaptIndustry?: string;
+
+  /**
+   * 描述
+   */
+  topicsDescribe?: string;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+
+  /**
+   * 平台标识
+   */
+  platformCode?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

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

@@ -1,6 +1,18 @@
 <template>
   <div :class="{ hidden: hidden }" class="pagination-container">
+    <!-- 游标分页模式 -->
+    <div v-if="cursorMode" class="cursor-pagination">
+      <span class="pagination-info">每页 {{ pageSize }} 条</span>
+      <el-button :disabled="currentPage <= 1" size="small" @click="handlePrevPage">上一页</el-button>
+      <span class="page-number">第 {{ currentPage }} 页</span>
+      <el-button :disabled="!hasMore" size="small" @click="handleNextPage">下一页</el-button>
+      <el-select v-model="pageSize" class="page-size-select" size="small" @change="handleSizeChange">
+        <el-option v-for="size in pageSizes" :key="size" :label="`${size} 条/页`" :value="size" />
+      </el-select>
+    </div>
+    <!-- 普通分页模式 -->
     <el-pagination
+      v-else
       v-model:current-page="currentPage"
       v-model:page-size="pageSize"
       :background="background"
@@ -29,10 +41,16 @@ const props = defineProps({
   background: propTypes.bool.def(true),
   autoScroll: propTypes.bool.def(true),
   hidden: propTypes.bool.def(false),
-  float: propTypes.string.def('right')
+  float: propTypes.string.def('right'),
+  // 游标分页模式
+  cursorMode: propTypes.bool.def(false),
+  // 是否还有更多数据(游标分页)
+  hasMore: propTypes.bool.def(true),
+  // 翻页方向(游标分页)
+  way: propTypes.number
 });
 
-const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
+const emit = defineEmits(['update:page', 'update:limit', 'update:way', 'pagination']);
 const currentPage = computed({
   get() {
     return props.page;
@@ -64,6 +82,28 @@ function handleCurrentChange(val: number) {
     scrollTo(0, 800);
   }
 }
+
+// 游标分页:上一页
+function handlePrevPage() {
+  if (currentPage.value <= 1) return;
+  emit('update:way', 0); // 0表示上一页
+  currentPage.value = currentPage.value - 1;
+  emit('pagination', { page: currentPage.value, limit: pageSize.value, way: 0 });
+  if (props.autoScroll) {
+    scrollTo(0, 800);
+  }
+}
+
+// 游标分页:下一页
+function handleNextPage() {
+  if (!props.hasMore) return;
+  emit('update:way', 1); // 1表示下一页
+  currentPage.value = currentPage.value + 1;
+  emit('pagination', { page: currentPage.value, limit: pageSize.value, way: 1 });
+  if (props.autoScroll) {
+    scrollTo(0, 800);
+  }
+}
 </script>
 
 <style lang="scss" scoped>
@@ -75,4 +115,26 @@ function handleCurrentChange(val: number) {
 .pagination-container.hidden {
   display: none;
 }
+.cursor-pagination {
+  float: v-bind(float);
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 8px 0;
+
+  .pagination-info {
+    color: #606266;
+    font-size: 14px;
+  }
+
+  .page-number {
+    color: #606266;
+    font-size: 14px;
+    margin: 0 4px;
+  }
+
+  .page-size-select {
+    width: 120px;
+  }
+}
 </style>

+ 18 - 0
src/router/index.ts

@@ -143,6 +143,24 @@ export const constantRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/product/pool/reviewDetail.vue'),
         name: 'PoolReviewDetail',
         meta: { title: '入池清单审核', activeMenu: '/product/pool', noCache: true }
+      },
+      {
+        path: 'program/form',
+        component: () => import('@/views/product/program/form.vue'),
+        name: 'ProgramForm',
+        meta: { title: '发布方案', activeMenu: '/product/program', noCache: true }
+      },
+      {
+        path: 'program/group',
+        component: () => import('@/views/product/program/programGroup.vue'),
+        name: 'ProgramGroup',
+        meta: { title: '管理商品', activeMenu: '/product/program', noCache: true }
+      },
+      {
+        path: 'program/groupProduct',
+        component: () => import('@/views/product/program/groupProduct.vue'),
+        name: 'GroupProduct',
+        meta: { title: '商品列表', activeMenu: '/product/program', noCache: true }
       }
     ]
   },

+ 317 - 0
src/views/product/productProgram/index.vue

@@ -0,0 +1,317 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="方案编号" prop="programNo">
+              <el-input v-model="queryParams.programNo" placeholder="请输入方案编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="方案标题" prop="title">
+              <el-input v-model="queryParams.title" placeholder="请输入方案标题" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="方案描述" prop="describe">
+              <el-input v-model="queryParams.describe" placeholder="请输入方案描述" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="所属分类编号或名称" prop="category">
+              <el-input v-model="queryParams.category" placeholder="请输入所属分类编号或名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="是否显示:1=是,0=否" prop="isShow">
+              <el-input v-model="queryParams.isShow" placeholder="请输入是否显示:1=是,0=否" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="所属行业编号或名称" prop="industry">
+              <el-input v-model="queryParams.industry" placeholder="请输入所属行业编号或名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="适配产品/设备编号" prop="adaptNo">
+              <el-input v-model="queryParams.adaptNo" placeholder="请输入适配产品/设备编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="标签" prop="label">
+              <el-input v-model="queryParams.label" placeholder="请输入标签" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="内部广告内容" prop="innerAdvert">
+              <el-input v-model="queryParams.innerAdvert" placeholder="请输入内部广告内容" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="平台标识" prop="platformCode">
+              <el-input v-model="queryParams.platformCode" placeholder="请输入平台标识" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:program:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:program:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:program:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:program:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="programList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="主键,自增ID" align="center" prop="id" v-if="true" />
+        <el-table-column label="方案编号" align="center" prop="programNo" />
+        <el-table-column label="方案标题" align="center" prop="title" />
+        <el-table-column label="方案描述" align="center" prop="describe" />
+        <el-table-column label="所属分类编号或名称" align="center" prop="category" />
+        <el-table-column label="是否显示:1=是,0=否" align="center" prop="isShow" />
+        <el-table-column label="封面图片URL" align="center" prop="coverImageUrl" width="100">
+          <template #default="scope">
+            <image-preview :src="scope.row.coverImageUrl" :width="50" :height="50"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="方案内容" align="center" prop="content" />
+        <el-table-column label="所属行业编号或名称" align="center" prop="industry" />
+        <el-table-column label="适配产品/设备编号" align="center" prop="adaptNo" />
+        <el-table-column label="标签" align="center" prop="label" />
+        <el-table-column label="内部广告内容" align="center" prop="innerAdvert" />
+        <el-table-column label="备注" align="center" prop="remark" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:program:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:program:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改产品解决方案/项目方案对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="programFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="方案编号" prop="programNo">
+          <el-input v-model="form.programNo" placeholder="请输入方案编号" />
+        </el-form-item>
+        <el-form-item label="方案标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入方案标题" />
+        </el-form-item>
+        <el-form-item label="方案描述" prop="describe">
+            <el-input v-model="form.describe" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="所属分类编号或名称" prop="category">
+          <el-input v-model="form.category" placeholder="请输入所属分类编号或名称" />
+        </el-form-item>
+        <el-form-item label="是否显示:1=是,0=否" prop="isShow">
+          <el-input v-model="form.isShow" placeholder="请输入是否显示:1=是,0=否" />
+        </el-form-item>
+        <el-form-item label="封面图片URL" prop="coverImage">
+          <image-upload v-model="form.coverImage"/>
+        </el-form-item>
+        <el-form-item label="方案内容">
+          <editor v-model="form.content" :min-height="192"/>
+        </el-form-item>
+        <el-form-item label="所属行业编号或名称" prop="industry">
+          <el-input v-model="form.industry" placeholder="请输入所属行业编号或名称" />
+        </el-form-item>
+        <el-form-item label="适配产品/设备编号" prop="adaptNo">
+          <el-input v-model="form.adaptNo" placeholder="请输入适配产品/设备编号" />
+        </el-form-item>
+        <el-form-item label="标签" prop="label">
+          <el-input v-model="form.label" placeholder="请输入标签" />
+        </el-form-item>
+        <el-form-item label="内部广告内容" prop="innerAdvert">
+          <el-input v-model="form.innerAdvert" placeholder="请输入内部广告内容" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Program" lang="ts">
+import { listProgram, getProgram, delProgram, addProgram, updateProgram } from '@/api/pmsProduct/program';
+import { ProgramVO, ProgramQuery, ProgramForm } from '@/api/pmsProduct/program/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const programList = ref<ProgramVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const programFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: ProgramForm = {
+  id: undefined,
+  programNo: undefined,
+  title: undefined,
+  describe: undefined,
+  category: undefined,
+  isShow: undefined,
+  coverImage: undefined,
+  content: undefined,
+  industry: undefined,
+  adaptNo: undefined,
+  label: undefined,
+  innerAdvert: undefined,
+  remark: undefined,
+}
+const data = reactive<PageData<ProgramForm, ProgramQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    programNo: undefined,
+    title: undefined,
+    describe: undefined,
+    category: undefined,
+    isShow: undefined,
+    coverImage: undefined,
+    content: undefined,
+    industry: undefined,
+    adaptNo: undefined,
+    label: undefined,
+    innerAdvert: undefined,
+    platformCode: undefined,
+    params: {
+    }
+  },
+  rules: {
+    content: [
+      { required: true, message: "方案内容不能为空", trigger: "blur" }
+    ],
+    label: [
+      { required: true, message: "标签不能为空", trigger: "blur" }
+    ],
+    innerAdvert: [
+      { required: true, message: "内部广告内容不能为空", trigger: "blur" }
+    ],
+    remark: [
+      { required: true, message: "备注不能为空", trigger: "blur" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询产品解决方案/项目方案列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listProgram(queryParams.value);
+  programList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  programFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ProgramVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加产品解决方案/项目方案";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ProgramVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getProgram(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改产品解决方案/项目方案";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  programFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateProgram(form.value).finally(() =>  buttonLoading.value = false);
+      } else {
+        await addProgram(form.value).finally(() =>  buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ProgramVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除产品解决方案/项目方案编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delProgram(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('product/program/export', {
+    ...queryParams.value
+  }, `program_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 482 - 0
src/views/product/program/form.vue

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

+ 395 - 0
src/views/product/program/groupProduct.vue

@@ -0,0 +1,395 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="never" class="mb-[10px]">
+      <div style="display: flex; align-items: center; gap: 10px;">
+        <el-button icon="ArrowLeft" @click="handleBack">返回</el-button>
+        <span style="font-size: 16px; font-weight: 500;">商品列表</span>
+      </div>
+    </el-card>
+
+    <el-card shadow="never">
+      <template #header>
+        <div style="display: flex; justify-content: space-between; align-items: center;">
+          <span style="font-size: 16px; font-weight: 500;">商品信息列表</span>
+          <div>
+            <el-button type="primary" icon="Plus" @click="handleAddProducts">添加</el-button>
+            <el-button type="primary" icon="Upload" @click="handleImportProducts">导入</el-button>
+            <el-button icon="Refresh" circle @click="getList" style="margin-left: 10px;"></el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table v-loading="loading" :data="productList" border style="width: 100%;">
+        <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
+        <el-table-column label="商品图片" align="center" width="100">
+          <template #default="scope">
+            <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="商品信息" align="center" min-width="250">
+          <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="类别" align="center" width="120">
+          <template #default="scope">
+            {{ scope.row.bottomCategoryName || scope.row.categoryName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" width="80">
+          <template #default="scope">
+            {{ scope.row.unitName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="平百售价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.minSellingPrice || scope.row.marketPrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="最低售价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.certificatePrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="标准成本" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.standardPrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="毛利率" align="center" width="80">
+          <template #default="scope">
+            {{ scope.row.tempGrossMargin || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="起订量" align="center" width="80">
+          <template #default="scope">
+            {{ scope.row.minOrderQuantity || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="是否主推" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.homeRecommended === '1' ? '是' : '否' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="120">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleSetRecommend(scope.row)">主推</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination 
+        v-show="total > 0" 
+        :total="total" 
+        v-model:page="queryParams.pageNum" 
+        v-model:limit="queryParams.pageSize" 
+        @pagination="getList" 
+      />
+    </el-card>
+
+    <!-- 商品选择弹窗 -->
+    <el-dialog
+      v-model="showProductDialog"
+      title="添加商品"
+      width="80%"
+      :close-on-click-modal="false"
+      @close="handleCloseDialog"
+    >
+      <div style="margin-bottom: 16px; display: flex; gap: 10px;">
+        <el-button type="primary" icon="Plus" :disabled="selectedProducts.length === 0" @click="handleConfirmAddProducts">
+          加入清单
+        </el-button>
+        <el-input
+          v-model="dialogQueryParams.searchText"
+          placeholder="商品名称/商品编号"
+          clearable
+          style="width: 300px;"
+          @keyup.enter="handleDialogSearch"
+        >
+          <template #append>
+            <el-button icon="Search" @click="handleDialogSearch" />
+          </template>
+        </el-input>
+      </div>
+
+      <el-table
+        v-loading="dialogLoading"
+        :data="dialogProductList"
+        border
+        style="width: 100%;"
+        @selection-change="handleProductSelection"
+      >
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
+        <el-table-column label="商品图片" align="center" width="100">
+          <template #default="scope">
+            <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="商品信息" align="center" min-width="250">
+          <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="类别" align="center" width="120">
+          <template #default="scope">
+            {{ scope.row.bottomCategoryName || scope.row.categoryName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" width="80">
+          <template #default="scope">
+            {{ scope.row.unitName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="市场价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.marketPrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="平台售价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.minSellingPrice || scope.row.marketPrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="最低售价" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.certificatePrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="标准成本" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.standardPrice || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="毛利率" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.tempGrossMargin || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="起订量" align="center" width="100">
+          <template #default="scope">
+            {{ scope.row.minOrderQuantity || '1' }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="dialogProductList.length > 0"
+        v-model:page="dialogQueryParams.pageNum"
+        v-model:limit="dialogQueryParams.pageSize"
+        v-model:way="dialogQueryParams.way"
+        :cursor-mode="true"
+        :has-more="hasMoreProducts"
+        @pagination="getProductList"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="GroupProduct" lang="ts">
+import { getGroupProductList, addProgramProduct } from '@/api/product/programProduct';
+import { BaseVO, BaseQuery } from '@/api/pmsProduct/base/types';
+import { listBase } from '@/api/pmsProduct/base';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const route = useRoute();
+
+const productList = ref<BaseVO[]>([]);
+const loading = ref(false);
+const total = ref(0);
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  groupId: route.query.groupId as string
+});
+
+// 商品选择弹窗相关
+const showProductDialog = ref(false);
+const dialogProductList = ref<BaseVO[]>([]);
+const dialogLoading = ref(false);
+const selectedProducts = ref<BaseVO[]>([]);
+const hasMoreProducts = ref(true);
+const productPageHistory = ref([]);
+
+const dialogQueryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  searchText: undefined,
+  lastSeenId: undefined,
+  firstSeenId: undefined,
+  way: undefined
+});
+
+/** 返回按钮操作 */
+const handleBack = () => {
+  router.back();
+}
+
+/** 查询商品列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await getGroupProductList(queryParams.value);
+    productList.value = res.rows || [];
+    total.value = res.total || 0;
+  } catch (error) {
+    console.error('获取商品列表失败', error);
+    productList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+/** 设置主推 */
+const handleSetRecommend = (row: BaseVO) => {
+  proxy?.$modal.msgWarning("主推功能待实现");
+}
+
+/** 删除商品 */
+const handleDelete = async (row: BaseVO) => {
+  await proxy?.$modal.confirm('是否确认删除该商品?');
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 批量添加商品 */
+const handleAddProducts = () => {
+  showProductDialog.value = true;
+  getProductList();
+}
+
+/** 导入商品 */
+const handleImportProducts = () => {
+  proxy?.$modal.msgWarning("导入功能待实现");
+}
+
+/** 获取弹窗商品列表 */
+const getProductList = async () => {
+  dialogLoading.value = true;
+  try {
+    const params = { ...dialogQueryParams.value };
+    const currentPageNum = dialogQueryParams.value.pageNum;
+
+    // 第一页不需要游标参数
+    if (currentPageNum === 1) {
+      delete params.lastSeenId;
+      delete params.firstSeenId;
+      delete params.way;
+    } else {
+      // way参数:0=上一页,1=下一页
+      if (dialogQueryParams.value.way === 0) {
+        // 上一页:使用目标页的firstId
+        const nextPageHistory = productPageHistory.value[currentPageNum];
+        if (nextPageHistory) {
+          params.firstSeenId = nextPageHistory.firstId;
+          params.way = 0;
+        }
+      } else {
+        // 下一页:使用前一页的lastId
+        const prevPageHistory = productPageHistory.value[currentPageNum - 1];
+        if (prevPageHistory) {
+          params.lastSeenId = prevPageHistory.lastId;
+          params.way = 1;
+        }
+      }
+    }
+
+    const res = await listBase(params);
+    dialogProductList.value = res.rows || [];
+
+    // 判断是否还有更多数据
+    hasMoreProducts.value = dialogProductList.value.length === dialogQueryParams.value.pageSize;
+
+    // 记录当前页的第一个id和最后一个id
+    if (dialogProductList.value.length > 0) {
+      const firstItem = dialogProductList.value[0];
+      const lastItem = dialogProductList.value[dialogProductList.value.length - 1];
+      
+      if (productPageHistory.value.length <= currentPageNum) {
+        productPageHistory.value[currentPageNum] = {
+          firstId: firstItem.id,
+          lastId: lastItem.id
+        };
+      }
+    }
+  } catch (error) {
+    console.error('获取商品列表失败:', error);
+    dialogProductList.value = [];
+  } finally {
+    dialogLoading.value = false;
+  }
+};
+
+/** 弹窗搜索 */
+const handleDialogSearch = () => {
+  dialogQueryParams.value.pageNum = 1;
+  dialogQueryParams.value.lastSeenId = undefined;
+  dialogQueryParams.value.firstSeenId = undefined;
+  productPageHistory.value = [];
+  getProductList();
+};
+
+/** 弹窗商品选择 */
+const handleProductSelection = (selection: BaseVO[]) => {
+  selectedProducts.value = selection;
+};
+
+/** 确认添加商品 */
+const handleConfirmAddProducts = async () => {
+  if (selectedProducts.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择要添加的商品');
+    return;
+  }
+  
+  try {
+    // 批量添加商品到分组
+    const groupId = route.query.groupId as string;
+    const addPromises = selectedProducts.value.map(product => {
+      return addProgramProduct({
+        groupId: groupId,
+        productId: product.id,
+        isMain: 0, // 默认非主产品
+        status: '0' // 默认状态为正常
+      });
+    });
+    
+    await Promise.all(addPromises);
+    proxy?.$modal.msgSuccess(`成功添加 ${selectedProducts.value.length} 个商品`);
+    showProductDialog.value = false;
+    selectedProducts.value = [];
+    await getList();
+  } catch (error) {
+    console.error('添加商品失败:', error);
+    proxy?.$modal.msgError('添加商品失败');
+  }
+};
+
+/** 关闭弹窗 */
+const handleCloseDialog = () => {
+  showProductDialog.value = false;
+  selectedProducts.value = [];
+  dialogQueryParams.value = {
+    pageNum: 1,
+    pageSize: 10,
+    searchText: undefined,
+    lastSeenId: undefined,
+    firstSeenId: undefined,
+    way: undefined
+  };
+  productPageHistory.value = [];
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 159 - 107
src/views/product/program/index.vue

@@ -5,34 +5,20 @@
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
             <el-form-item label="方案编号" prop="programNo">
-              <el-input v-model="queryParams.programNo" placeholder="请输入方案编号" clearable @keyup.enter="handleQuery" />
+              <el-input v-model="queryParams.programNo" placeholder="请输入采购方案编号" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="方案标题" prop="title">
-              <el-input v-model="queryParams.title" placeholder="请输入方案标题" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="推文标题" prop="tweetsTitle">
+              <el-input v-model="queryParams.tweetsTitle" placeholder="请输入推文标题" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="方案描述" prop="describe">
-              <el-input v-model="queryParams.describe" placeholder="请输入方案描述" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="所属分类编号或名称" prop="category">
-              <el-input v-model="queryParams.category" placeholder="请输入所属分类编号或名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否显示:1=是,0=否" prop="isShow">
-              <el-input v-model="queryParams.isShow" placeholder="请输入是否显示:1=是,0=否" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="所属行业编号或名称" prop="industry">
-              <el-input v-model="queryParams.industry" placeholder="请输入所属行业编号或名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="适配产品/设备编号" prop="adaptNo">
-              <el-input v-model="queryParams.adaptNo" placeholder="请输入适配产品/设备编号" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="标签" prop="label">
-              <el-input v-model="queryParams.label" placeholder="请输入标签" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="内部广告内容" prop="innerAdvert">
-              <el-input v-model="queryParams.innerAdvert" placeholder="请输入内部广告内容" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="平台标识" prop="platformCode">
-              <el-input v-model="queryParams.platformCode" placeholder="请输入平台标识" clearable @keyup.enter="handleQuery" />
+            <el-form-item label="推文分类" prop="tweetsCategory">
+              <el-select v-model="queryParams.tweetsCategory" placeholder="请选择" clearable>
+                <el-option
+                  v-for="item in purchaseCategoryOptions"
+                  :key="item.id"
+                  :label="item.categoryName"
+                  :value="item.id"
+                />
+              </el-select>
             </el-form-item>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -47,47 +33,51 @@
       <template #header>
         <el-row :gutter="10" class="mb8">
           <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:program:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:program:edit']">修改</el-button>
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" >新增</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:program:remove']">删除</el-button>
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" >修改</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:program:export']">导出</el-button>
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" >删除</el-button>
           </el-col>
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
 
       <el-table v-loading="loading" border :data="programList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="主键,自增ID" align="center" prop="id" v-if="true" />
-        <el-table-column label="方案编号" align="center" prop="programNo" />
-        <el-table-column label="方案标题" align="center" prop="title" />
-        <el-table-column label="方案描述" align="center" prop="describe" />
-        <el-table-column label="所属分类编号或名称" align="center" prop="category" />
-        <el-table-column label="是否显示:1=是,0=否" align="center" prop="isShow" />
-        <el-table-column label="封面图片URL" align="center" prop="coverImageUrl" width="100">
+        <el-table-column label="方案编号" align="center" prop="programNo" width="120" />
+        <el-table-column label="封面图片" align="center" prop="coverImage" width="100">
+          <template #default="scope">
+            <image-preview :src="scope.row.coverImage" :width="50" :height="50"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="方案主题" align="center" prop="tweetsTitle" show-overflow-tooltip />
+        <el-table-column label="方案类型" align="center" prop="tweetsCategory" width="150">
+          <template #default="scope">
+            <span>{{ getCategoryName(scope.row.tweetsCategory) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="是否显示" align="center" prop="isShow" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.isShow === 1 ? 'success' : 'info'">{{ scope.row.isShow === 1 ? '显 示' : '' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="发布时间" align="center" prop="createTime" width="180">
           <template #default="scope">
-            <image-preview :src="scope.row.coverImageUrl" :width="50" :height="50"/>
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="方案内容" align="center" prop="content" />
-        <el-table-column label="所属行业编号或名称" align="center" prop="industry" />
-        <el-table-column label="适配产品/设备编号" align="center" prop="adaptNo" />
-        <el-table-column label="标签" align="center" prop="label" />
-        <el-table-column label="内部广告内容" align="center" prop="innerAdvert" />
-        <el-table-column label="备注" align="center" prop="remark" />
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:program:edit']"></el-button>
+            <el-tooltip content="编辑方案" placement="top">
+              <el-button link type="primary" @click="handleUpdate(scope.row)" >编辑方案</el-button>
+            </el-tooltip>
+            <el-tooltip content="管理商品" placement="top">
+              <el-button link type="primary" @click="handleManageProduct(scope.row)" >管理商品</el-button>
             </el-tooltip>
             <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:program:remove']"></el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" >删 除</el-button>
             </el-tooltip>
           </template>
         </el-table-column>
@@ -95,41 +85,64 @@
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
-    <!-- 添加或修改产品解决方案/项目方案对话框 -->
+    <!-- 添加或修改采购项目对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="programFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="方案编号" prop="programNo">
-          <el-input v-model="form.programNo" placeholder="请输入方案编号" />
+        <el-form-item label="项目编号" prop="programNo">
+          <el-input v-model="form.programNo" placeholder="请输入项目编号" />
+        </el-form-item>
+        <el-form-item label="推文标题" prop="tweetsTitle">
+            <el-input v-model="form.tweetsTitle" type="textarea" placeholder="请输入内容" />
         </el-form-item>
-        <el-form-item label="方案标题" prop="title">
-          <el-input v-model="form.title" placeholder="请输入方案标题" />
+        <el-form-item label="副标题" prop="subtitle">
+            <el-input v-model="form.subtitle" type="textarea" placeholder="请输入内容" />
         </el-form-item>
-        <el-form-item label="方案描述" prop="describe">
-            <el-input v-model="form.describe" type="textarea" placeholder="请输入内容" />
+        <el-form-item label="描述" prop="programDescribe">
+            <el-input v-model="form.programDescribe" type="textarea" placeholder="请输入内容" />
         </el-form-item>
-        <el-form-item label="所属分类编号或名称" prop="category">
-          <el-input v-model="form.category" placeholder="请输入所属分类编号或名称" />
+        <el-form-item label="推文分类" prop="tweetsCategory">
+          <el-input v-model="form.tweetsCategory" placeholder="请输入推文分类" />
         </el-form-item>
-        <el-form-item label="是否显示:1=是,0=否" prop="isShow">
-          <el-input v-model="form.isShow" placeholder="请输入是否显示:1=是,0=否" />
+        <el-form-item label="是否显示" prop="isShow">
+          <el-input v-model="form.isShow" placeholder="请输入是否显示" />
         </el-form-item>
-        <el-form-item label="封面图片URL" prop="coverImage">
+        <el-form-item label="封面图片路径或URL" prop="coverImage">
           <image-upload v-model="form.coverImage"/>
         </el-form-item>
-        <el-form-item label="方案内容">
-          <editor v-model="form.content" :min-height="192"/>
+        <el-form-item label="图片列表" prop="imageList">
+            <el-input v-model="form.imageList" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+        <el-form-item label="点击次数" prop="clicks">
+          <el-input v-model="form.clicks" placeholder="请输入点击次数" />
+        </el-form-item>
+        <el-form-item label="收藏次数" prop="collects">
+          <el-input v-model="form.collects" placeholder="请输入收藏次数" />
+        </el-form-item>
+        <el-form-item label="价格" prop="price">
+          <el-input v-model="form.price" placeholder="请输入价格" />
         </el-form-item>
-        <el-form-item label="所属行业编号或名称" prop="industry">
-          <el-input v-model="form.industry" placeholder="请输入所属行业编号或名称" />
+        <el-form-item label="适配编号" prop="adaptNo">
+          <el-input v-model="form.adaptNo" placeholder="请输入适配编号" />
         </el-form-item>
-        <el-form-item label="适配产品/设备编号" prop="adaptNo">
-          <el-input v-model="form.adaptNo" placeholder="请输入适配产品/设备编号" />
+        <el-form-item label="标签" prop="lable">
+          <el-input v-model="form.lable" placeholder="请输入标签" />
         </el-form-item>
-        <el-form-item label="标签" prop="label">
-          <el-input v-model="form.label" placeholder="请输入标签" />
+        <el-form-item label="上传方案" prop="uploadScheme">
+          <el-input v-model="form.uploadScheme" placeholder="请输入上传方案" />
         </el-form-item>
-        <el-form-item label="内部广告内容" prop="innerAdvert">
-          <el-input v-model="form.innerAdvert" placeholder="请输入内部广告内容" />
+        <el-form-item label="适配行业" prop="adaptIndustry">
+          <el-input v-model="form.adaptIndustry" placeholder="请输入适配行业" />
+        </el-form-item>
+        <el-form-item label="无效时间" prop="invalidTime">
+          <el-date-picker clearable
+            v-model="form.invalidTime"
+            type="datetime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            placeholder="请选择无效时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="文件名" prop="filename">
+          <el-input v-model="form.filename" placeholder="请输入文件名" />
         </el-form-item>
         <el-form-item label="备注" prop="remark">
             <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
@@ -146,12 +159,15 @@
 </template>
 
 <script setup name="Program" lang="ts">
-import { listProgram, getProgram, delProgram, addProgram, updateProgram } from '@/api/pmsProduct/program';
-import { ProgramVO, ProgramQuery, ProgramForm } from '@/api/pmsProduct/program/types';
+import { listProgram, getProgram, delProgram, addProgram, updateProgram } from '@/api/product/program/index';
+import { ProgramVO, ProgramQuery, ProgramForm } from '@/api/product/program/types';
+import { listPurchaseCategory } from '@/api/globalSetting/purchaseCategory';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
 
 const programList = ref<ProgramVO[]>([]);
+const purchaseCategoryOptions = ref<any[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -171,16 +187,23 @@ const dialog = reactive<DialogOption>({
 const initFormData: ProgramForm = {
   id: undefined,
   programNo: undefined,
-  title: undefined,
-  describe: undefined,
-  category: undefined,
+  tweetsTitle: undefined,
+  subtitle: undefined,
+  programDescribe: undefined,
+  tweetsCategory: undefined,
   isShow: undefined,
   coverImage: undefined,
-  content: undefined,
-  industry: undefined,
+  imageList: undefined,
+  clicks: undefined,
+  collects: undefined,
+  price: undefined,
   adaptNo: undefined,
-  label: undefined,
-  innerAdvert: undefined,
+  lable: undefined,
+  uploadScheme: undefined,
+  adaptIndustry: undefined,
+  invalidTime: undefined,
+  filename: undefined,
+  status: undefined,
   remark: undefined,
 }
 const data = reactive<PageData<ProgramForm, ProgramQuery>>({
@@ -189,29 +212,45 @@ const data = reactive<PageData<ProgramForm, ProgramQuery>>({
     pageNum: 1,
     pageSize: 10,
     programNo: undefined,
-    title: undefined,
-    describe: undefined,
-    category: undefined,
+    tweetsTitle: undefined,
+    subtitle: undefined,
+    programDescribe: undefined,
+    tweetsCategory: undefined,
     isShow: undefined,
     coverImage: undefined,
-    content: undefined,
-    industry: undefined,
+    imageList: undefined,
+    clicks: undefined,
+    collects: undefined,
+    price: undefined,
     adaptNo: undefined,
-    label: undefined,
-    innerAdvert: undefined,
+    lable: undefined,
+    uploadScheme: undefined,
+    adaptIndustry: undefined,
+    invalidTime: undefined,
+    filename: undefined,
+    status: undefined,
     platformCode: undefined,
     params: {
     }
   },
   rules: {
-    content: [
-      { required: true, message: "方案内容不能为空", trigger: "blur" }
+    price: [
+      { required: true, message: "价格不能为空", trigger: "blur" }
+    ],
+    uploadScheme: [
+      { required: true, message: "上传方案不能为空", trigger: "blur" }
+    ],
+    adaptIndustry: [
+      { required: true, message: "适配行业不能为空", trigger: "blur" }
     ],
-    label: [
-      { required: true, message: "标签不能为空", trigger: "blur" }
+    invalidTime: [
+      { required: true, message: "无效时间不能为空", trigger: "blur" }
     ],
-    innerAdvert: [
-      { required: true, message: "内部广告内容不能为空", trigger: "blur" }
+    filename: [
+      { required: true, message: "文件名不能为空", trigger: "blur" }
+    ],
+    status: [
+      { required: true, message: "状态不能为空", trigger: "change" }
     ],
     remark: [
       { required: true, message: "备注不能为空", trigger: "blur" }
@@ -221,7 +260,20 @@ const data = reactive<PageData<ProgramForm, ProgramQuery>>({
 
 const { queryParams, form, rules } = toRefs(data);
 
-/** 查询产品解决方案/项目方案列表 */
+/** 查询采购分类列表 */
+const getPurchaseCategoryList = async () => {
+  const res = await listPurchaseCategory({ isShow: 1 });
+  purchaseCategoryOptions.value = res.rows || [];
+}
+
+/** 根据ID获取分类名称 */
+const getCategoryName = (id: string | number) => {
+  if (!id) return '';
+  const category = purchaseCategoryOptions.value.find(item => String(item.id) === String(id));
+  return category ? category.categoryName : id;
+}
+
+/** 查询采购项目列表 */
 const getList = async () => {
   loading.value = true;
   const res = await listProgram(queryParams.value);
@@ -263,19 +315,13 @@ const handleSelectionChange = (selection: ProgramVO[]) => {
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = "添加产品解决方案/项目方案";
+  router.push({ path: '/product/program/form' });
 }
 
 /** 修改按钮操作 */
 const handleUpdate = async (row?: ProgramVO) => {
-  reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getProgram(_id);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = "修改产品解决方案/项目方案";
+  const _id = row?.id || ids.value[0];
+  router.push({ path: '/product/program/form', query: { id: _id } });
 }
 
 /** 提交按钮 */
@@ -297,8 +343,8 @@ const submitForm = () => {
 
 /** 删除按钮操作 */
 const handleDelete = async (row?: ProgramVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除产品解决方案/项目方案编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  const _ids: string | number | Array<string | number> = row?.id || ids.value[0];
+  await proxy?.$modal.confirm('是否确认删除采购项目编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
   await delProgram(_ids);
   proxy?.$modal.msgSuccess("删除成功");
   await getList();
@@ -311,7 +357,13 @@ const handleExport = () => {
   }, `program_${new Date().getTime()}.xlsx`)
 }
 
+/** 管理商品操作 */
+const handleManageProduct = (row: ProgramVO) => {
+  router.push({ path: '/product/program/group', query: { programId: row.id } });
+}
+
 onMounted(() => {
+  getPurchaseCategoryList();
   getList();
 });
 </script>

+ 263 - 0
src/views/product/program/programGroup.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="never" class="mb-[10px]">
+      <div style="display: flex; align-items: center; gap: 10px;">
+        <el-button icon="ArrowLeft" @click="handleBack">返回</el-button>
+        <span style="font-size: 16px; font-weight: 500;">管理商品</span>
+      </div>
+    </el-card>
+    <el-card shadow="never">
+      <template #header>
+        <div style="display: flex; justify-content: space-between; align-items: center;">
+          <span style="font-size: 16px; font-weight: 500;">商品分组信息列表</span>
+          <div>
+            <el-button type="primary" icon="Plus" @click="handleAdd" >添加</el-button>
+            <el-button icon="Refresh" circle @click="getList" style="margin-left: 10px;"></el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table v-loading="loading" :data="groupList" style="width: 100%;">
+        <el-table-column label="编号" align="center" prop="id" width="120" />
+        <el-table-column label="图片" align="center" prop="coverImageUrl" width="150">
+          <template #default="scope">
+            <image-preview :src="scope.row.coverImage" :width="80" :height="80"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="标题" align="center" prop="title" />
+        <el-table-column label="描述" align="center" prop="subtitle" />
+        <el-table-column label="关联商品数" align="center" prop="productCount" width="120">
+          <template #default="scope">
+            {{ scope.row.productCount || 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="200">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleUpdate(scope.row)" >编辑</el-button>
+            <el-button link type="primary" @click="handleAddProduct(scope.row)" >添加商品</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改采购组信息对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="groupFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="图片" prop="coverImage">
+          <div>
+            <el-button type="primary" @click="showFileSelector = true">
+              {{ form.coverImage ? '更换图片' : '选择图片' }}
+            </el-button>
+            <div v-if="form.coverImage" style="margin-top: 10px;">
+              <image-preview :src="form.coverImage" :width="100" :height="100"/>
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入标题" />
+        </el-form-item>
+        <el-form-item label="副标题" prop="subtitle">
+          <el-input v-model="form.subtitle" placeholder="请输入副标题" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 文件选择器 -->
+    <FileSelector
+      v-model="showFileSelector"
+      :allowed-types="[1]"
+      :multiple="false"
+      @confirm="handleFileConfirm"
+    />
+  </div>
+</template>
+
+<script setup name="Group" lang="ts">
+import { listGroup, getGroup, delGroup, addGroup, updateGroup } from '@/api/product/group';
+import { GroupVO, GroupQuery, GroupForm } from '@/api/product/group/types';
+import FileSelector from '@/components/FileSelector/index.vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const router = useRouter();
+const route = useRoute();
+
+const groupList = ref<GroupVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(false);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const showFileSelector = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const groupFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: GroupForm = {
+  id: undefined,
+  programId: undefined,
+  coverImage: undefined,
+  title: undefined,
+  subtitle: undefined,
+  status: undefined,
+  remark: undefined,
+}
+const data = reactive<PageData<GroupForm, GroupQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    programId: route.query.programId as string,
+    coverImage: undefined,
+    title: undefined,
+    subtitle: undefined,
+    status: undefined,
+    platformCode: undefined,
+    params: {
+    }
+  },
+  rules: {
+    coverImage: [
+      { required: true, message: "封面图片路径或URL不能为空", trigger: "blur" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 返回按钮操作 */
+const handleBack = () => {
+  router.back();
+}
+
+/** 查询采购组信息列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listGroup(queryParams.value);
+  groupList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  groupFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: GroupVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 添加商品操作 */
+const handleAddProduct = (row: GroupVO) => {
+  router.push({
+    path: '/product/program/groupProduct',
+    query: {
+      groupId: row.id,
+      programId: route.query.programId
+    }
+  });
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加采购组信息";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: GroupVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getGroup(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改采购组信息";
+}
+
+/** 文件选择确认 */
+const handleFileConfirm = (files: any[]) => {
+  if (files && files.length > 0) {
+    form.value.coverImage = files[0].url;
+  }
+};
+
+/** 提交按钮 */
+const submitForm = () => {
+  groupFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      // 确保 programId 从路由中获取
+      const submitData = {
+        ...form.value,
+        programId: route.query.programId as string
+      };
+      if (form.value.id) {
+        await updateGroup(submitData).finally(() =>  buttonLoading.value = false);
+      } else {
+        await addGroup(submitData).finally(() =>  buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: GroupVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除采购组信息编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delGroup(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('product/group/export', {
+    ...queryParams.value
+  }, `group_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 247 - 0
src/views/product/programProduct/index.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="组号" prop="groupId">
+              <el-input v-model="queryParams.groupId" placeholder="请输入组号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="产品编号" prop="productId">
+              <el-input v-model="queryParams.productId" placeholder="请输入产品编号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="是否为主产品" prop="isMain">
+              <el-input v-model="queryParams.isMain" placeholder="请输入是否为主产品" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="平台标识" prop="platformCode">
+              <el-input v-model="queryParams.platformCode" placeholder="请输入平台标识" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['product:programProduct:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['product:programProduct:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['product:programProduct:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['product:programProduct:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="programProductList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="主键ID,自增" align="center" prop="id" v-if="true" />
+        <el-table-column label="组号" align="center" prop="groupId" />
+        <el-table-column label="产品编号" align="center" prop="productId" />
+        <el-table-column label="是否为主产品" align="center" prop="isMain" />
+        <el-table-column label="状态" align="center" prop="status" />
+        <el-table-column label="备注" align="center" prop="remark" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['product:programProduct:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['product:programProduct:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改采购项目产品关联对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="programProductFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="组号" prop="groupId">
+          <el-input v-model="form.groupId" placeholder="请输入组号" />
+        </el-form-item>
+        <el-form-item label="产品编号" prop="productId">
+          <el-input v-model="form.productId" placeholder="请输入产品编号" />
+        </el-form-item>
+        <el-form-item label="是否为主产品" prop="isMain">
+          <el-input v-model="form.isMain" placeholder="请输入是否为主产品" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="ProgramProduct" lang="ts">
+import { listProgramProduct, getProgramProduct, delProgramProduct, addProgramProduct, updateProgramProduct } from '@/api/product/programProduct';
+import { ProgramProductVO, ProgramProductQuery, ProgramProductForm } from '@/api/product/programProduct/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const programProductList = ref<ProgramProductVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const programProductFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: ProgramProductForm = {
+  id: undefined,
+  groupId: undefined,
+  productId: undefined,
+  isMain: undefined,
+  status: undefined,
+  remark: undefined,
+}
+const data = reactive<PageData<ProgramProductForm, ProgramProductQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    groupId: undefined,
+    productId: undefined,
+    isMain: undefined,
+    status: undefined,
+    platformCode: undefined,
+    params: {
+    }
+  },
+  rules: {
+    productId: [
+      { required: true, message: "产品编号不能为空", trigger: "blur" }
+    ],
+    status: [
+      { required: true, message: "状态不能为空", trigger: "change" }
+    ],
+    remark: [
+      { required: true, message: "备注不能为空", trigger: "blur" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询采购项目产品关联列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listProgramProduct(queryParams.value);
+  programProductList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  programProductFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ProgramProductVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加采购项目产品关联";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ProgramProductVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getProgramProduct(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改采购项目产品关联";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  programProductFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateProgramProduct(form.value).finally(() =>  buttonLoading.value = false);
+      } else {
+        await addProgramProduct(form.value).finally(() =>  buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ProgramProductVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除采购项目产品关联编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delProgramProduct(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('product/programProduct/export', {
+    ...queryParams.value
+  }, `programProduct_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 433 - 0
src/views/product/topics/index.vue

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