feat(shopGoods): 支持商品分类多选及列表树形高度优化

- 商品模型增加 categoryIds 字段支持多分类,搜索参数同步支持多分类查询
- 商品列表新增分类列,展示商品所属分类名称
- 商品编辑及搜索组件商品分类选择改为多选,保持兼容单选主分类逻辑
- 商品分类树形列表SVG连线高度调小,提升界面紧凑度
- 优化分类图片尺寸,缩小预览图大小,提升视觉效果
- 更新用户推荐接口方法参数类型声明,增加类型安全保障
This commit is contained in:
2026-08-14 19:38:02 +08:00
parent 780606f05a
commit 053b776d9a
7 changed files with 71 additions and 28 deletions
+7
View File
@@ -17,3 +17,10 @@
- 在「分类标识」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`
- 效果:二级/三级分类前的树形引导线高度降低,整行更紧凑。
+5 -1
View File
@@ -31,8 +31,10 @@ export interface ShopGoods {
canExpress?: number;
// 商品分类
category?: string;
// 商品分类ID
// 商品分类ID(主分类,向后兼容)
categoryId?: number;
// 商品分类ID列表(方案B:可归属多个分类)
categoryIds?: number[];
parentName?: string;
categoryName?: string;
// 一级分类
@@ -174,6 +176,8 @@ export interface ShopGoodsCollectParam {
export interface ShopGoodsParam extends PageParam {
parentId?: number;
categoryId?: number;
// 方案B:按多个分类查询,与单值 categoryId 二选一,多值时优先
categoryIds?: number[];
goodsId?: number;
status?: number;
goodsName?: string;
+1 -1
View File
@@ -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
+12 -8
View File
@@ -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,20 @@
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="code">
@@ -928,12 +930,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 +1207,7 @@ const form = reactive<ShopGoods>({
canExpress: 1,
unitName: '',
categoryId: undefined,
categoryIds: [],
parentName: undefined,
categoryName: undefined,
specs: 0,
@@ -1449,14 +1453,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,6 +1796,11 @@ 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,
content: content.value,
@@ -2050,15 +2063,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();
+9
View File
@@ -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',
+2 -2
View File
@@ -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[] = [];