Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a7b217c2b | |||
| b5c67988a6 | |||
| 053b776d9a | |||
| 780606f05a | |||
| a04a8baec7 | |||
| 6a5b7ff711 |
+2
-2
@@ -1,4 +1,4 @@
|
||||
VITE_APP_NAME=后台管理系统
|
||||
#VITE_API_URL=http://127.0.0.1:9200/api
|
||||
VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_API_URL=http://127.0.0.1:9200/api
|
||||
#VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
@@ -13,3 +13,13 @@
|
||||
- 改 `src/views/shop/shopExpressTemplateDetail/index.vue`:「配送地区」列按 region_mode 显示「不发货:xxx / 配送:xxx / 默认全国」,城市码聚合为省名显示。
|
||||
- 类型检查(vue-tsc)确认本次改动无新错误(唯一相关报错是预存的 `$router` 用法,原文件即有,未引入)。
|
||||
- 结论:黑名单现可经后台 UI 配置;SQL 直插仍作为备选(二者后台存城市码、SQL 存省份码,后端比对为编码集合 contains,都生效、互不冲突)。
|
||||
- 已生成插偏远八省黑名单完整 SQL:`offline-remote-areas-blacklist-10606.sql`(template_id=11, region_mode=2, region_ids=新疆650000/西藏540000/青海630000/宁夏640000/内蒙古150000/海南460000/黑龙江230000/甘肃620000, status=0, tenant_id=10606),含前置查重 + 后置验证语句。
|
||||
|
||||
## 微信支付报错 APPID_MCHID_NOT_MATCH(tenant 10606 下单失败)
|
||||
|
||||
- 现象:用户下单报 `创建微信支付订单失败:Wrong HttpStatusCode[400] ... APPID_MCHID_NOT_MATCH`。服务商模式 JSAPI,请求体 sp_appid=`wx541db955e7a62709`/sp_mchid=`1720296183`、sub_appid=`wxcad054ee427aaca1`/sub_mchid=`1744894035`。
|
||||
- 根因:传给微信的 `sub_appid` 与 `sub_mchid` 在微信侧不匹配(该子商户应用ID未绑定到该子商户号)。代码无 bug。
|
||||
- 4 个值来源:`gxwebsoft_core.sys_payment`(tenant 10606 微信JSAPI记录)字段 `app_id`/`mch_id`/`sub_app_id`/`sub_mch_id`;拼装在 `ShopOrderServiceImpl.java:809-912`(服务商模式)。
|
||||
- 已验证 `sub_app_id=wxcad054ee427aaca1` 就是 `guilixu-taro` 用户端小程序真实 appid(project.config.json),故 sub_appid 是对的、与买家 openid 一致。
|
||||
- 结论:问题在 `sub_app_id↔sub_mchid` 绑定被破坏——最可能(1)商户平台子商户 `1744894035` 对 appid 的授权被移除/失效;(2)`sys_payment.sub_mch_id` 被误改。
|
||||
- 修复:微信商户平台(服务商 `1720296183` 服务商平台→特约商户管理→开发配置)为子商户 `1744894035` 绑定 appid `wxcad054ee427aaca1`;并 `SELECT` 核对 `sys_payment` 的 sub_mch_id 仍为 `1744894035`。无需改代码,新订单号直接读最新配置。
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-08-14 工作日志
|
||||
|
||||
## 修复商品分类层级(10409 → 10421)
|
||||
|
||||
- 问题:guilixu-admin 后台 `/shop/shopGoodsCategory` 页面中,10409「二级分类」与 10421「特别专区」显示为同级。
|
||||
- 原因:前端按 `parentId` 渲染树形层级,说明 10409 的 `parent_id` 不是 10421(实际可能为 0)。
|
||||
- 处理:
|
||||
- 确认后端表为 `shop_goods_category`(guilixu-java / db_guilixu),关键字段 `category_id`、`parent_id`。
|
||||
- 生成修复 SQL:`fix-category-parent-10409.sql`,将 10409 的 `parent_id` 更新为 10421。
|
||||
- 同时提供了后台 UI 的临时操作方式:编辑 10409 把上级分类改为 10421。
|
||||
- 文件:`/Users/gxwebsoft/VUE/guilixu-admin/fix-category-parent-10409.sql`
|
||||
|
||||
## 商品分类编辑弹窗新增「链接地址」字段
|
||||
|
||||
- 需求:在 `shopGoodsCategoryEdit.vue` 的「分类标识」下方新增「链接地址」输入项,存到 `path` 字段。
|
||||
- 改动:`src/views/shop/shopGoodsCategory/components/shopGoodsCategoryEdit.vue`
|
||||
- 在「分类标识」form-item 之后新增「链接地址」项,绑定 `form.path`(form 已有该字段,无需改 model/后端)。
|
||||
- 重置逻辑 Object.assign 补上 `path: undefined`,保证新增时清空。
|
||||
- 注意:仅前端 UI 改动,后端表 `shop_goods_category.path` 字段已存在,无需迁移。
|
||||
|
||||
## 商品分类列表树形连线高度调紧凑
|
||||
|
||||
- 问题:后台 `/shop/shopGoodsCategory` 列表中,树形层级连线 SVG 高度 40px,导致行显得太高、不紧凑。
|
||||
- 改动:`src/views/shop/shopGoodsCategory/index.vue`
|
||||
- `renderTreePrefix` 中 SVG 高度 `H` 从 `40` 改为 `28`,`mid` 自动为 `14`。
|
||||
- 效果:二级/三级分类前的树形引导线高度降低,整行更紧凑。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 2026-08-15 工作日志
|
||||
|
||||
## 专区功能(ShopHomeSection)接口报错修复
|
||||
|
||||
- 现象:访问专区列表接口报 `NoSuchPropertyException: ShopHomeSectionParam.deleted`。
|
||||
- 根因:模板生成的 `ShopHomeSectionParam`(guilixu-java `src/main/java/com/gxwebsoft/shop/param/ShopHomeSectionParam.java`)**漏了 `deleted` 字段**;同项目 Mapper XML(`ShopHomeSectionMapper.xml`)按模板带 `<if test="param.deleted == null"> AND a.deleted = 0</if>` 逻辑删除过滤,OGNL 求值时属性不存在即抛异常。
|
||||
- 对比:`ShopGoodsParam` 有 `deleted` 字段(@QueryField EQ + @Schema),为标准写法;`ShopHomeSection` 实体本身有 `@TableLogic private Integer deleted;`,表启用逻辑删除。
|
||||
- 修复:给 `ShopHomeSectionParam` 末尾补 `private Integer deleted;`(带 @QueryField + @Schema),与模板一致。
|
||||
- 验证:重新编译部署 guilixu-java(端口 9200 本地实例)后接口恢复正常。
|
||||
- ⚠️ 同类预警:其他新生成的 Param 若同样漏 `deleted`,会报一模一样的错,补字段即可。
|
||||
|
||||
## 专区功能菜单未显示(待办)
|
||||
|
||||
- 前端页面 `src/views/special/zone/index.vue` 与 API 层 `src/api/shop/shopZone/` 已完成。
|
||||
- 但因 guilixu-admin 为动态路由(菜单由后端接口拉取),后端菜单表尚未插入「专区管理」记录,故左侧导航看不到入口。
|
||||
- 待处理:在后端菜单表(sys_menu 类)插入菜单记录(component=special/zone,tenant_id=10606),刷新/重登后生效。
|
||||
|
||||
## 专区功能交互入口约定(答疑结论)
|
||||
|
||||
- 白名单用户:专区列表每行「白名单」按钮 → 弹窗勾选用户 → 确定(PUT /{id}/users 覆盖式保存到 shop_home_section_user 表)。前端 UserSelectModal 链路完整。
|
||||
- 商品关联:专区页「商品」按钮只是**查看**该专区下商品(GET /{id}/goods,后端用 FIND_IN_SET(section_id, section_ids) 查询);**往专区加商品不在此页操作**,要去「商品管理 → 编辑商品 → 所属专区(多选)」勾选专区并保存,写入 goods.section_ids。
|
||||
- 后端接口齐全:users/goods 查询与设置、check-permission 权限校验均已实现。
|
||||
- ⚠️ 待确认:白名单弹窗当前按 isStaff=true 查后台员工用户(@/api/system/user 的 pageUsers);若受限专区面向小程序买家,需调整用户来源。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 2026-08-16 工作日志
|
||||
|
||||
## 专区白名单用户弹窗修复
|
||||
|
||||
### 1. 去掉 isStaff 限制
|
||||
- 文件:`src/views/special/zone/components/UserSelectModal.vue`
|
||||
- 改动:`pageUsers` 调用移除 `isStaff: true` 参数,使白名单搜索覆盖全部用户(不再只查后台员工)。
|
||||
|
||||
### 2. 修复勾选框不显示
|
||||
- 现象:弹窗表格数据正常但无 checkbox 勾选列,用户无法选择。
|
||||
- 根因:`rowSelection` 用 `reactive()` 包整个对象的方式在 `ele-pro-table` 中不生效;且缺少 `columnWidth` 导致勾选列无宽度不渲染。
|
||||
- 对比:项目内 `shopCoupon/index.vue` 用 `computed(() => ({ columnWidth, selectedRowKeys, onChange }))` 模式可正常工作;字典页用 `{ columnWidth: 32 }` 最简写法也可。
|
||||
- 修复:
|
||||
- `reactive` → `computed` 返回 rowSelection 对象(与 shopCoupon 一致)
|
||||
- 补 `columnWidth: 48`
|
||||
- `selectedRowKeys` 独立为 `ref<number[]>`,通过 computed 注入 rowSelection
|
||||
- 所有引用点同步更新(模板计数、confirm、watch 初始化)
|
||||
|
||||
## 专区商品抽屉改造为可编辑
|
||||
|
||||
- 需求:专区「商品」按钮打开的抽屉从只读列表升级为支持添加/移除商品。
|
||||
- 后端(guilixu-java `ShopHomeSectionController.java`)新增两个接口:
|
||||
- `POST /{id}/goods` — 添加商品到专区(接收 goodsId[],将 section_id 追加到每个 goods 的 section_ids 逗号串中,用 LinkedHashSet 去重)
|
||||
- `DELETE /{id}/goods` — 从专区移除商品(接收 goodsId[],从 section_ids 中移除该 section_id,空则置 null)
|
||||
- 前端 API(`src/api/shop/shopZone/index.ts`)新增 `addSectionGoods`、`removeSectionGoods`。
|
||||
- 前端 UI(`src/views/special/zone/index.vue`)商品抽屉改造:
|
||||
- 顶部:搜索框 + 「搜索」按钮 + 「全部添加(N)」确认按钮
|
||||
- 搜索结果区:虚线框展示候选商品列表,每行有「+添加」单条操作(自动过滤已在专区内的商品)
|
||||
- 已关联商品表格:每行加「移除」按钮(二次确认后调用 remove 接口)
|
||||
- 数据源:搜索商品复用已有 `pageShopGoods` API(@/api/shop/shopGoods)
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- 偏远地区不发货黑名单(租户 10606,全店生效)
|
||||
-- ============================================================
|
||||
-- 作用:下单时 guilixu-java 的 DeliveryRegionChecker 按 tenant_id=10606
|
||||
-- 取 region_mode=2 且 status=0 的明细,将收货地址行政区划码与
|
||||
-- region_ids 比对,命中即拦截并提示「XX暂不配送,请更换收货地址」。
|
||||
-- 注意:黑名单是租户级、全店生效,不区分商品绑定的运费模板。
|
||||
-- 自提/无需发货(deliveryType=1) 不参与此校验。
|
||||
-- 省份码(与 regions-data.json 完全一致):
|
||||
-- 新疆650000 西藏540000 青海630000 宁夏640000 内蒙古150000
|
||||
-- 海南460000 黑龙江230000 甘肃620000
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 先确认是否已有黑名单(避免重复插入)
|
||||
SELECT id, template_id, region_mode, region_ids, status, tenant_id
|
||||
FROM shop_express_template_detail
|
||||
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
|
||||
|
||||
-- 2) 若上一步无记录,再执行插入(template_id=11 即「偏远地区不发」模板)
|
||||
INSERT INTO shop_express_template_detail
|
||||
(template_id, type, province_id, city_id, region_mode, region_ids,
|
||||
first_num, first_amount, extra_amount, extra_num, status, deleted,
|
||||
tenant_id, create_time, update_time, sort_number)
|
||||
VALUES
|
||||
(11, 0, NULL, NULL, 2,
|
||||
'650000,540000,630000,640000,150000,460000,230000,620000',
|
||||
0, 0, 0, 0, 0, 0, 10606, NOW(), NOW(), 0);
|
||||
|
||||
-- 3) 验证(应能看到刚插入的这条,且 region_ids 非空)
|
||||
SELECT id, template_id, region_mode, region_ids, status, tenant_id
|
||||
FROM shop_express_template_detail
|
||||
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
|
||||
@@ -31,8 +31,10 @@ export interface ShopGoods {
|
||||
canExpress?: number;
|
||||
// 商品分类
|
||||
category?: string;
|
||||
// 商品分类ID
|
||||
// 商品分类ID(主分类,向后兼容)
|
||||
categoryId?: number;
|
||||
// 商品分类ID列表(方案B:可归属多个分类)
|
||||
categoryIds?: number[];
|
||||
parentName?: string;
|
||||
categoryName?: string;
|
||||
// 一级分类
|
||||
@@ -149,6 +151,8 @@ export interface ShopGoods {
|
||||
activityType?: number;
|
||||
// 配送方式:0送上门 1限自提
|
||||
deliveryMode?: number;
|
||||
// 所属专区(逗号分隔的 sectionId 字符串,对应 shop_goods.section_ids 列)
|
||||
sectionIds?: string;
|
||||
}
|
||||
|
||||
export interface BathSet {
|
||||
@@ -174,6 +178,8 @@ export interface ShopGoodsCollectParam {
|
||||
export interface ShopGoodsParam extends PageParam {
|
||||
parentId?: number;
|
||||
categoryId?: number;
|
||||
// 方案B:按多个分类查询,与单值 categoryId 二选一,多值时优先
|
||||
categoryIds?: number[];
|
||||
goodsId?: number;
|
||||
status?: number;
|
||||
goodsName?: string;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type {
|
||||
HomeSection,
|
||||
HomeSectionParam,
|
||||
SectionPermission,
|
||||
SectionGoods,
|
||||
SectionUser
|
||||
} from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询首页专区
|
||||
*/
|
||||
export async function pageHomeSections(params: HomeSectionParam) {
|
||||
const res = await request.get<ApiResult<PageResult<HomeSection>>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询首页专区列表
|
||||
*/
|
||||
export async function listHomeSections(params?: HomeSectionParam) {
|
||||
const res = await request.get<ApiResult<HomeSection[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询首页专区
|
||||
*/
|
||||
export async function getHomeSection(id: number) {
|
||||
const res = await request.get<ApiResult<HomeSection>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加首页专区
|
||||
*/
|
||||
export async function addHomeSection(data: HomeSection) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改首页专区
|
||||
*/
|
||||
export async function updateHomeSection(data: HomeSection) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除首页专区
|
||||
*/
|
||||
export async function removeHomeSection(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询专区白名单用户
|
||||
*/
|
||||
export async function listSectionUsers(id: number) {
|
||||
const res = await request.get<ApiResult<SectionUser[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存专区白名单用户(覆盖式)
|
||||
*/
|
||||
export async function setSectionUsers(id: number, userIds: number[]) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
|
||||
userIds
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询专区商品分页
|
||||
*/
|
||||
export async function listSectionGoods(
|
||||
id: number,
|
||||
params?: { page?: number; limit?: number }
|
||||
) {
|
||||
const res = await request.get<ApiResult<PageResult<SectionGoods>>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
{
|
||||
params: { page: 1, limit: 10, ...params }
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品到专区
|
||||
*/
|
||||
export async function addSectionGoods(id: number, goodsIds: number[]) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
goodsIds
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从专区移除商品
|
||||
*/
|
||||
export async function removeSectionGoods(id: number, goodsIds: number[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
{ data: goodsIds }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验专区访问权限
|
||||
*/
|
||||
export async function checkSectionPermission(sectionIds: number[]) {
|
||||
const res = await request.post<ApiResult<SectionPermission[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/check-permission',
|
||||
{ sectionIds }
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 首页专区
|
||||
*/
|
||||
export interface HomeSection {
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 专区标题
|
||||
title?: string;
|
||||
// 专区副标题
|
||||
subtitle?: string;
|
||||
// banner 图片URL
|
||||
banner?: string;
|
||||
// 关联分类ID(逗号分隔字符串,可选)
|
||||
categoryIds?: string;
|
||||
// 样式类型 0默认
|
||||
styleType?: number;
|
||||
// 是否受限专区 0否 1是
|
||||
restricted?: number;
|
||||
// 排序号(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 状态 0正常 1禁用
|
||||
status?: number;
|
||||
// 生效开始时间
|
||||
startTime?: string;
|
||||
// 生效结束时间
|
||||
endTime?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 是否删除 0否 1是
|
||||
deleted?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页专区搜索条件
|
||||
*/
|
||||
export interface HomeSectionParam extends PageParam {
|
||||
// 专区标题(模糊)
|
||||
title?: string;
|
||||
// 状态 0正常 1禁用
|
||||
status?: number;
|
||||
// 是否受限专区 0/1
|
||||
restricted?: number;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区权限校验结果
|
||||
*/
|
||||
export interface SectionPermission {
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 是否受限专区
|
||||
restricted?: boolean;
|
||||
// 当前用户是否允许访问
|
||||
allowed?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区商品(只读展示,字段按需取用)
|
||||
*/
|
||||
export interface SectionGoods {
|
||||
goodsId?: number;
|
||||
name?: string;
|
||||
image?: string;
|
||||
price?: string;
|
||||
salePrice?: string;
|
||||
stock?: number;
|
||||
status?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区白名单用户
|
||||
*/
|
||||
export interface SectionUser {
|
||||
id?: number;
|
||||
sectionId?: number;
|
||||
userId?: number;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ export async function updateUserStatus(userId?: number, status?: number) {
|
||||
/**
|
||||
* 修改推荐状态
|
||||
*/
|
||||
export async function updateUserRecommend(form) {
|
||||
export async function updateUserRecommend(form: { userId: number; recommend: number }) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user/recommend',
|
||||
form
|
||||
|
||||
@@ -52,15 +52,17 @@
|
||||
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
multiple
|
||||
:max-tag-count="2"
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 180px"
|
||||
style="width: 200px"
|
||||
:listHeight="700"
|
||||
placeholder="请选择分类"
|
||||
:value="where.categoryId || undefined"
|
||||
placeholder="请选择分类(可多选)"
|
||||
:value="where.categoryIds && where.categoryIds.length ? where.categoryIds : undefined"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (where.categoryId = value)"
|
||||
@change="onCategoryId"
|
||||
@update:value="(value?: number[]) => (where.categoryIds = value || [])"
|
||||
@change="onCategoryIds"
|
||||
/>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
@@ -149,6 +151,7 @@
|
||||
status: undefined,
|
||||
stock: undefined,
|
||||
categoryId: undefined,
|
||||
categoryIds: [],
|
||||
merchantId: undefined,
|
||||
keywords: ''
|
||||
});
|
||||
@@ -238,9 +241,10 @@
|
||||
reload();
|
||||
};
|
||||
|
||||
// 按分类查询
|
||||
const onCategoryId = (id: number) => {
|
||||
where.categoryId = id;
|
||||
// 按分类查询(方案B:支持多选,主分类取首个)
|
||||
const onCategoryIds = (ids: number[]) => {
|
||||
where.categoryIds = ids || [];
|
||||
where.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
|
||||
@@ -32,18 +32,31 @@
|
||||
v-model:value="form.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品分类" name="categoryId">
|
||||
<a-form-item label="商品分类" name="categoryIds">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
multiple
|
||||
:max-tag-count="3"
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 320px"
|
||||
placeholder="请选择分类"
|
||||
:value="form.categoryId || undefined"
|
||||
placeholder="请选择分类(可多选)"
|
||||
:value="form.categoryIds && form.categoryIds.length ? form.categoryIds : undefined"
|
||||
:listHeight="700"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (form.categoryId = value)"
|
||||
@change="onCategoryId"
|
||||
@update:value="(value?: number[]) => (form.categoryIds = value || [])"
|
||||
@change="onCategoryIds"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属专区" name="sectionIds">
|
||||
<a-select
|
||||
v-model:value="sectionIdList"
|
||||
mode="multiple"
|
||||
:max-tag-count="3"
|
||||
:options="zoneOptions"
|
||||
style="width: 320px"
|
||||
placeholder="请选择所属专区(可多选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品编码" name="code">
|
||||
@@ -707,6 +720,7 @@ import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
|
||||
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
|
||||
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
|
||||
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
|
||||
import {listHomeSections} from '@/api/shop/shopZone';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
@@ -857,6 +871,9 @@ const skuList = ref<ShopGoodsSku[]>([]);
|
||||
const files = ref<ItemType[]>([]);
|
||||
const goodsSpec = ref<ShopGoodsSpec>();
|
||||
const category = ref<string[]>([]);
|
||||
// 所属专区(多选)
|
||||
const sectionIdList = ref<number[]>([]);
|
||||
const zoneOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
// 批量设置
|
||||
const batchPrice = ref<number | undefined>(undefined);
|
||||
@@ -928,12 +945,13 @@ const addEnsureTag = (item = '') => {
|
||||
ensureTagItem.value = '';
|
||||
};
|
||||
|
||||
// 选择栏目
|
||||
const onCategoryId = (id: number) => {
|
||||
form.categoryId = id;
|
||||
// 选择栏目(方案B:支持多选,主分类取列表首个,向后兼容旧字段)
|
||||
const onCategoryIds = (ids: number[]) => {
|
||||
form.categoryIds = ids || [];
|
||||
form.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||
// 💾 在新增模式下,用户手动选择栏目时也保存到本地存储
|
||||
if (!isUpdate.value && id) {
|
||||
saveLastCategory(id);
|
||||
if (!isUpdate.value && ids && ids.length) {
|
||||
saveLastCategory(ids[0]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1204,6 +1222,7 @@ const form = reactive<ShopGoods>({
|
||||
canExpress: 1,
|
||||
unitName: '',
|
||||
categoryId: undefined,
|
||||
categoryIds: [],
|
||||
parentName: undefined,
|
||||
categoryName: undefined,
|
||||
specs: 0,
|
||||
@@ -1449,14 +1468,18 @@ const chooseSupplier = (item: ShopMerchant) => {
|
||||
};
|
||||
|
||||
const chooseGoodsCategory = (item: ShopGoodsCategory, value: any) => {
|
||||
form.categoryId = value[1].value;
|
||||
const cid = value[1].value as number;
|
||||
form.categoryId = cid;
|
||||
form.categoryIds = [cid];
|
||||
form.parentName = value[0].label;
|
||||
form.categoryName = value[1].label;
|
||||
};
|
||||
const chooseTakeawayCategory = (item: ShopGoodsCategory, value: any) => {
|
||||
const cid = item[0] as number;
|
||||
form.categoryParent = '店铺分类';
|
||||
form.categoryChildren = value[0].label;
|
||||
form.categoryId = item[0];
|
||||
form.categoryId = cid;
|
||||
form.categoryIds = [cid];
|
||||
};
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
@@ -1788,8 +1811,17 @@ const save = () => {
|
||||
if (form.specs === 1 && goodsSpec.value && spec.value.length) {
|
||||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||||
}
|
||||
// 方案B:主分类兜底取多分类第一个,保证旧字段/旧逻辑兼容
|
||||
if (form.categoryIds && form.categoryIds.length && !form.categoryId) {
|
||||
form.categoryId = form.categoryIds[0];
|
||||
}
|
||||
|
||||
const formData: any = {
|
||||
...form,
|
||||
// 所属专区:数字数组 -> 逗号分隔字符串
|
||||
sectionIds: sectionIdList.value.length
|
||||
? sectionIdList.value.join(',')
|
||||
: '',
|
||||
content: content.value,
|
||||
category: JSON.stringify(category.value),
|
||||
files: JSON.stringify(files.value),
|
||||
@@ -1953,8 +1985,20 @@ watch(
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await getExpressTemplateList();
|
||||
// 加载所属专区选项
|
||||
listHomeSections({})
|
||||
.then((list) => {
|
||||
zoneOptions.value = (list || []).map((z) => ({
|
||||
label: z.title || '',
|
||||
value: z.sectionId as number
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
zoneOptions.value = [];
|
||||
});
|
||||
|
||||
images.value = [];
|
||||
sectionIdList.value = [];
|
||||
category.value = [];
|
||||
files.value = [];
|
||||
videos.value = [];
|
||||
@@ -1963,6 +2007,13 @@ watch(
|
||||
ensureTagItem.value = '';
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
// 所属专区:逗号分隔字符串 -> 数字数组
|
||||
sectionIdList.value = form.sectionIds
|
||||
? form.sectionIds
|
||||
.split(',')
|
||||
.map((s) => Number(s.trim()))
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
: [];
|
||||
if (form.commissionType === undefined || form.commissionType === null) {
|
||||
form.commissionType = 10;
|
||||
}
|
||||
@@ -2050,15 +2101,21 @@ watch(
|
||||
if (form.ensureTag) {
|
||||
ensureTag.value = form.ensureTag.split(',');
|
||||
}
|
||||
// 🎯 优先级设置栏目:
|
||||
// 1. 如果传入了 categoryId(从栏目页面点击添加),使用传入的
|
||||
// 2. 否则使用上次保存的栏目
|
||||
if (props.data.categoryId) {
|
||||
// 🎯 优先级设置栏目(方案B:支持多分类回填):
|
||||
// 1. 如果传入了 categoryIds,使用多分类
|
||||
// 2. 否则如果传入了 categoryId,兼容单分类
|
||||
// 3. 否则使用上次保存的栏目
|
||||
if (props.data.categoryIds && props.data.categoryIds.length) {
|
||||
form.categoryIds = props.data.categoryIds;
|
||||
form.categoryId = props.data.categoryId ?? props.data.categoryIds[0];
|
||||
} else if (props.data.categoryId) {
|
||||
form.categoryId = props.data?.categoryId;
|
||||
form.categoryIds = [props.data.categoryId];
|
||||
} else {
|
||||
const lastCategory = getLastCategory();
|
||||
if (lastCategory) {
|
||||
form.categoryId = lastCategory;
|
||||
form.categoryIds = [lastCategory];
|
||||
}
|
||||
}
|
||||
await getRoleList();
|
||||
|
||||
@@ -269,6 +269,15 @@
|
||||
key: 'name',
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'categoryName',
|
||||
key: 'categoryName',
|
||||
align: 'center',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => text || '--'
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'priceGroup',
|
||||
|
||||
@@ -90,6 +90,14 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="链接地址" name="path">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="可选,如:/pages/goods/list?categoryId=1"
|
||||
v-model:value="form.path"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">正常</a-radio>
|
||||
@@ -286,6 +294,7 @@
|
||||
image: undefined,
|
||||
adImage: undefined,
|
||||
parentId: undefined,
|
||||
path: undefined,
|
||||
sortNumber: 100,
|
||||
comments: undefined,
|
||||
status: 0
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<img
|
||||
v-if="record.adImage"
|
||||
:src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
|
||||
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
|
||||
style="max-width: 56px; height: 28px; object-fit: cover; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@@ -176,7 +176,7 @@
|
||||
const depth = depthMap.value.get(record.categoryId || 0) || 0;
|
||||
if (depth === 0) return '';
|
||||
const W = 20;
|
||||
const H = 40;
|
||||
const H = 20;
|
||||
const mid = H / 2;
|
||||
// 回溯祖先链:chain[0]=顶级 ... chain[depth]=当前节点
|
||||
const chain: number[] = [];
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<!-- 白名单用户多选弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="820"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
title="设置专区白名单用户"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="confirm"
|
||||
>
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="userId"
|
||||
:datasource="datasource"
|
||||
:columns="columns"
|
||||
:row-selection="rowSelection"
|
||||
:pagination="true"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
v-model:value="searchText"
|
||||
placeholder="姓名/手机号/昵称"
|
||||
style="width: 220px"
|
||||
@search="reload"
|
||||
@pressEnter="reload"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
<div class="mt-2 ele-text-secondary">
|
||||
已选 {{ selectedRowKeys.length }} 人
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
ColumnItem,
|
||||
DatasourceFunction
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import { pageUsers } from '@/api/system/user';
|
||||
import { EleProTable } from 'ele-admin-pro';
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 已选用户ID
|
||||
selectedUserIds?: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', userIds: number[]): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 搜索内容
|
||||
const searchText = ref<string>('');
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'realName',
|
||||
key: 'realName',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '手机号码',
|
||||
dataIndex: 'mobile',
|
||||
key: 'mobile',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
dataIndex: 'organizationName',
|
||||
key: 'organizationName',
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
// 已选用户ID
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
|
||||
// 多选配置(与 shopCoupon 一致:computed + columnWidth)
|
||||
const rowSelection = computed(() => ({
|
||||
columnWidth: 48,
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: (string | number)[]) => {
|
||||
selectedRowKeys.value = keys.map((k) => Number(k));
|
||||
}
|
||||
}));
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
if (searchText.value) {
|
||||
where.keywords = searchText.value;
|
||||
}
|
||||
return pageUsers({
|
||||
keywords: searchText.value,
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = () => {
|
||||
tableRef?.value?.reload({ page: 1 });
|
||||
};
|
||||
|
||||
/* 确认保存 */
|
||||
const confirm = () => {
|
||||
emit('confirm', [...selectedRowKeys.value]);
|
||||
updateVisible(false);
|
||||
};
|
||||
|
||||
// 打开弹窗时初始化已选
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
selectedRowKeys.value = props.selectedUserIds
|
||||
? [...props.selectedUserIds]
|
||||
: [];
|
||||
if (tableRef.value) {
|
||||
// 等待表格挂载后刷新
|
||||
setTimeout(() => reload(), 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
@@ -0,0 +1,251 @@
|
||||
<!-- 首页专区编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="640"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:title="isUpdate ? '编辑专区' : '添加专区'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ flex: '90px' }"
|
||||
:wrapper-col="{ flex: '1' }"
|
||||
>
|
||||
<a-form-item label="专区标题" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入专区标题"
|
||||
v-model:value="form.title"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="副标题" name="subtitle">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="可选,专区副标题"
|
||||
v-model:value="form.subtitle"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="banner图" name="banner">
|
||||
<a-upload
|
||||
:before-upload="handleBannerUpload"
|
||||
:show-upload-list="false"
|
||||
accept="image/*"
|
||||
>
|
||||
<a-button v-if="!form.banner">
|
||||
<template #icon><UploadOutlined /></template>
|
||||
上传 banner
|
||||
</a-button>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<img :src="form.banner" style="width: 120px; height: 48px; object-fit: cover; border-radius: 4px; border: 1px solid #d9d9d9;" />
|
||||
<a-button type="text" danger size="small" @click.stop="form.banner = undefined">删除</a-button>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联分类" name="categoryIds">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="可选,分类ID逗号分隔,如:1,2,3"
|
||||
v-model:value="form.categoryIds"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="样式类型" name="styleType">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="99"
|
||||
style="width: 100%"
|
||||
placeholder="默认0"
|
||||
v-model:value="form.styleType"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否受限" name="restricted">
|
||||
<a-switch
|
||||
size="small"
|
||||
v-model:checked="form.restricted"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="受限专区"
|
||||
un-checked-children="普通专区"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
style="width: 100%"
|
||||
placeholder="数值越小越靠前"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch
|
||||
size="small"
|
||||
v-model:checked="form.status"
|
||||
:checked-value="0"
|
||||
:un-checked-value="1"
|
||||
checked-children="正常"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="生效开始" name="startTime">
|
||||
<a-date-picker
|
||||
show-time
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="可选,专区生效开始时间"
|
||||
:value="form.startTime || undefined"
|
||||
@update:value="(val: any) => (form.startTime = val || undefined)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="生效结束" name="endTime">
|
||||
<a-date-picker
|
||||
show-time
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="可选,专区生效结束时间"
|
||||
:value="form.endTime || undefined"
|
||||
@update:value="(val: any) => (form.endTime = val || undefined)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { addHomeSection, updateHomeSection } from '@/api/shop/shopZone';
|
||||
import { uploadFile } from '@/api/system/file';
|
||||
import type { HomeSection } from '@/api/shop/shopZone/model';
|
||||
import type { FormInstance } from 'ant-design-vue/es/form';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
data?: HomeSection | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
const form = reactive<HomeSection>({
|
||||
sectionId: undefined,
|
||||
tenantId: undefined,
|
||||
title: undefined,
|
||||
subtitle: undefined,
|
||||
banner: undefined,
|
||||
categoryIds: undefined,
|
||||
styleType: 0,
|
||||
restricted: 0,
|
||||
sortNumber: 100,
|
||||
status: 0,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
userId: undefined,
|
||||
deleted: 0,
|
||||
createTime: undefined,
|
||||
updateTime: undefined
|
||||
});
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules: Record<string, any> = {
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入专区标题',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!formRef.value) return;
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const saveOrUpdate = isUpdate.value ? updateHomeSection : addHomeSection;
|
||||
saveOrUpdate({ ...form })
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// banner 上传
|
||||
const handleBannerUpload = async (file: File) => {
|
||||
const isImage = file.type?.startsWith('image/');
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const res = await uploadFile(file);
|
||||
if (res?.path) {
|
||||
form.banner = res.path;
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '上传失败');
|
||||
}
|
||||
return false; // 阻止默认上传
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
sectionId: undefined,
|
||||
tenantId: undefined,
|
||||
title: undefined,
|
||||
subtitle: undefined,
|
||||
banner: undefined,
|
||||
categoryIds: undefined,
|
||||
styleType: 0,
|
||||
restricted: 0,
|
||||
sortNumber: 100,
|
||||
status: 0,
|
||||
startTime: undefined,
|
||||
endTime: undefined
|
||||
});
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,497 @@
|
||||
<!-- 首页专区管理 -->
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="sectionId"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-input-search
|
||||
v-model:value="where.title"
|
||||
placeholder="搜索专区标题"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@search="() => reload()"
|
||||
@pressEnter="() => reload()"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="where.status"
|
||||
placeholder="状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
@change="() => reload()"
|
||||
>
|
||||
<a-select-option :value="0">正常</a-select-option>
|
||||
<a-select-option :value="1">禁用</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
<span>添加专区</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'banner'">
|
||||
<img
|
||||
v-if="record.banner"
|
||||
:src="getCompressedImageUrl(record.banner, { width: 96, quality: 85 })"
|
||||
style="max-width: 80px; height: 32px; object-fit: cover; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'restricted'">
|
||||
<a-tag :color="record.restricted === 1 ? 'red' : 'default'">
|
||||
{{ record.restricted === 1 ? '受限专区' : '普通专区' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status === 0"
|
||||
checked-children="正常"
|
||||
un-checked-children="禁用"
|
||||
size="small"
|
||||
@change="(val: boolean) => toggleStatus(record, val)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openUsers(record)">白名单</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openGoods(record)">商品</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此专区吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ZoneEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
|
||||
<!-- 白名单用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
v-model:visible="showUser"
|
||||
:sectionId="currentSectionId"
|
||||
:selectedUserIds="currentUserIds"
|
||||
@confirm="onSaveUsers"
|
||||
/>
|
||||
|
||||
<!-- 专区商品抽屉 -->
|
||||
<a-drawer
|
||||
:width="860"
|
||||
:visible="showGoods"
|
||||
title="专区商品"
|
||||
:maskClosable="false"
|
||||
@update:visible="(v: boolean) => (showGoods = v)"
|
||||
>
|
||||
<!-- 添加商品区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-input-search
|
||||
v-model:value="goodsSearchText"
|
||||
placeholder="搜索商品名称"
|
||||
allow-clear
|
||||
style="flex: 1; max-width: 320px;"
|
||||
@search="searchGoodsToAdd"
|
||||
@pressEnter="searchGoodsToAdd"
|
||||
/>
|
||||
<a-button type="primary" :loading="goodsSearchLoading" @click="searchGoodsToAdd">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
v-if="candidateGoods.length > 0"
|
||||
title="确定将搜索结果全部添加到此专区吗?"
|
||||
@confirm="addCandidateGoods"
|
||||
>
|
||||
<a-button type="primary" ghost>
|
||||
全部添加 ({{ candidateGoods.length }})
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<!-- 候选商品列表(搜索结果,点击单条添加) -->
|
||||
<div v-if="candidateGoods.length > 0" style="margin-bottom: 12px; border: 1px dashed #d9d9d9; border-radius: 6px; padding: 8px;">
|
||||
<div style="font-size: 12px; color: #999; margin-bottom: 6px;">搜索结果(点击 + 添加)</div>
|
||||
<div
|
||||
v-for="g in candidateGoods"
|
||||
:key="g.goodsId"
|
||||
style="display: flex; align-items: center; padding: 4px 0; gap: 8px; border-bottom: 1px solid #f0f0f0;"
|
||||
>
|
||||
<img v-if="g.image" :src="g.image" style="width: 32px; height: 32px; object-fit: cover; border-radius: 4px;" />
|
||||
<span style="flex: 1; font-size: 13px;">{{ g.name }}</span>
|
||||
<span style="color: #999; font-size: 12px;">¥{{ g.price }}</span>
|
||||
<a-button type="link" size="small" @click="addOneGood(g)" style="padding: 0;">+ 添加</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已关联商品列表 -->
|
||||
<a-spin :spinning="goodsLoading">
|
||||
<a-table
|
||||
row-key="goodsId"
|
||||
:dataSource="goodsList"
|
||||
:columns="goodsColumns"
|
||||
:pagination="{
|
||||
total: goodsTotal,
|
||||
current: goodsPage,
|
||||
pageSize: 10,
|
||||
onChange: (page: number) => {
|
||||
goodsPage = page;
|
||||
loadGoods(currentSectionId);
|
||||
}
|
||||
}"
|
||||
:scroll="{ y: 380 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<img
|
||||
v-if="text"
|
||||
:src="text"
|
||||
style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-popconfirm title="确定从该专区移除此商品吗?" @confirm="removeOneGood(record)">
|
||||
<a-button type="link" danger size="small">移除</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import ZoneEdit from './components/zoneEdit.vue';
|
||||
import UserSelectModal from './components/UserSelectModal.vue';
|
||||
import { getCompressedImageUrl } from '@/utils/image';
|
||||
import {
|
||||
pageHomeSections,
|
||||
removeHomeSection,
|
||||
updateHomeSection,
|
||||
listSectionUsers,
|
||||
setSectionUsers,
|
||||
listSectionGoods,
|
||||
addSectionGoods,
|
||||
removeSectionGoods
|
||||
} from '@/api/shop/shopZone';
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods';
|
||||
import type {
|
||||
HomeSection,
|
||||
HomeSectionParam
|
||||
} from '@/api/shop/shopZone/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 当前编辑数据
|
||||
const current = ref<HomeSection | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
|
||||
// 白名单弹窗
|
||||
const showUser = ref(false);
|
||||
const currentSectionId = ref<number>(0);
|
||||
const currentUserIds = ref<number[]>([]);
|
||||
|
||||
// 商品抽屉
|
||||
const showGoods = ref(false);
|
||||
const goodsLoading = ref(false);
|
||||
const goodsList = ref<any[]>([]);
|
||||
const goodsTotal = ref(0);
|
||||
const goodsPage = ref(1);
|
||||
// 商品搜索/添加
|
||||
const goodsSearchText = ref('');
|
||||
const goodsSearchLoading = ref(false);
|
||||
const candidateGoods = ref<any[]>([]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
return pageHomeSections({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
}).then((res) => {
|
||||
return { list: res?.list || [], count: res?.count || 0 };
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'sectionId',
|
||||
key: 'sectionId',
|
||||
align: 'center',
|
||||
width: 70
|
||||
},
|
||||
{
|
||||
title: '专区标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'left',
|
||||
minWidth: 160
|
||||
},
|
||||
{
|
||||
title: 'banner',
|
||||
dataIndex: 'banner',
|
||||
key: 'banner',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '是否受限',
|
||||
dataIndex: 'restricted',
|
||||
key: 'restricted',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
customRender: ({ text }: any) => toDateString(text, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
] as unknown as ColumnItem[]);
|
||||
|
||||
// 商品表格列
|
||||
const goodsColumns = [
|
||||
{ title: 'ID', dataIndex: 'goodsId', key: 'goodsId', align: 'center', width: 70 },
|
||||
{ title: '商品名称', dataIndex: 'name', key: 'name', align: 'left' },
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
align: 'center',
|
||||
width: 80
|
||||
},
|
||||
{ title: '商城价', dataIndex: 'price', key: 'price', align: 'center', width: 90 },
|
||||
{ title: '库存', dataIndex: 'stock', key: 'stock', align: 'center', width: 80 },
|
||||
{ title: '操作', key: 'action', align: 'center', width: 80 }
|
||||
] as any[];
|
||||
|
||||
/* 搜索条件 */
|
||||
const where = ref<HomeSectionParam>({
|
||||
title: '',
|
||||
status: undefined,
|
||||
restricted: undefined,
|
||||
sortNumber: undefined
|
||||
});
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (w?: HomeSectionParam) => {
|
||||
if (w) {
|
||||
where.value = { ...where.value, ...w };
|
||||
}
|
||||
tableRef?.value?.reload({ where: where.value });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: HomeSection) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 切换状态 */
|
||||
const toggleStatus = (row: HomeSection, val: boolean) => {
|
||||
const newStatus = val ? 0 : 1;
|
||||
updateHomeSection({ ...row, status: newStatus })
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
row.status = newStatus;
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: HomeSection) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeHomeSection(row.sectionId)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 打开白名单弹窗 */
|
||||
const openUsers = (row: HomeSection) => {
|
||||
currentSectionId.value = row.sectionId || 0;
|
||||
listSectionUsers(row.sectionId || 0)
|
||||
.then((list) => {
|
||||
currentUserIds.value = (list || []).map((u) => u.userId || 0).filter(Boolean);
|
||||
showUser.value = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 保存白名单 */
|
||||
const onSaveUsers = (userIds: number[]) => {
|
||||
setSectionUsers(currentSectionId.value, userIds)
|
||||
.then((msg) => {
|
||||
message.success(msg || '白名单已保存');
|
||||
showUser.value = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 打开商品抽屉 */
|
||||
const openGoods = (row: HomeSection) => {
|
||||
currentSectionId.value = row.sectionId || 0;
|
||||
goodsPage.value = 1;
|
||||
goodsSearchText.value = '';
|
||||
candidateGoods.value = [];
|
||||
showGoods.value = true;
|
||||
loadGoods(row.sectionId || 0);
|
||||
};
|
||||
|
||||
const loadGoods = (sectionId: number) => {
|
||||
goodsLoading.value = true;
|
||||
listSectionGoods(sectionId, { page: goodsPage.value, limit: 10 })
|
||||
.then((res) => {
|
||||
goodsList.value = res?.list || [];
|
||||
goodsTotal.value = res?.count || 0;
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
goodsLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/* 搜索可添加的商品 */
|
||||
const searchGoodsToAdd = () => {
|
||||
if (!goodsSearchText.value.trim()) {
|
||||
message.warning('请输入商品名称');
|
||||
return;
|
||||
}
|
||||
goodsSearchLoading.value = true;
|
||||
pageShopGoods({ keywords: goodsSearchText.value, page: 1, limit: 20 })
|
||||
.then((res) => {
|
||||
candidateGoods.value = (res?.list || []).filter(
|
||||
(g: any) => !goodsList.value.some((eg: any) => eg.goodsId === g.goodsId)
|
||||
);
|
||||
if (candidateGoods.value.length === 0) {
|
||||
message.info('没有找到可添加的新商品');
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
})
|
||||
.finally(() => {
|
||||
goodsSearchLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/* 添加单条商品到专区 */
|
||||
const addOneGood = (good: any) => {
|
||||
addSectionGoods(currentSectionId.value, [good.goodsId])
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
candidateGoods.value = candidateGoods.value.filter((g: any) => g.goodsId !== good.goodsId);
|
||||
loadGoods(currentSectionId.value);
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量添加候选商品 */
|
||||
const addCandidateGoods = () => {
|
||||
const ids = candidateGoods.value.map((g: any) => g.goodsId);
|
||||
addSectionGoods(currentSectionId.value, ids)
|
||||
.then((msg) => {
|
||||
message.success(`已添加 ${ids.length} 个商品`);
|
||||
candidateGoods.value = [];
|
||||
loadGoods(currentSectionId.value);
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 从专区移除单条商品 */
|
||||
const removeOneGood = (record: any) => {
|
||||
removeSectionGoods(currentSectionId.value, [record.goodsId])
|
||||
.then((msg) => {
|
||||
message.success(msg);
|
||||
loadGoods(currentSectionId.value);
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 行属性:双击编辑 */
|
||||
const customRow = (record: HomeSection) => {
|
||||
return {
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user