feat(shopGoods): 支持商品分类多选及列表树形高度优化
- 商品模型增加 categoryIds 字段支持多分类,搜索参数同步支持多分类查询 - 商品列表新增分类列,展示商品所属分类名称 - 商品编辑及搜索组件商品分类选择改为多选,保持兼容单选主分类逻辑 - 商品分类树形列表SVG连线高度调小,提升界面紧凑度 - 优化分类图片尺寸,缩小预览图大小,提升视觉效果 - 更新用户推荐接口方法参数类型声明,增加类型安全保障
This commit is contained in:
@@ -17,3 +17,10 @@
|
|||||||
- 在「分类标识」form-item 之后新增「链接地址」项,绑定 `form.path`(form 已有该字段,无需改 model/后端)。
|
- 在「分类标识」form-item 之后新增「链接地址」项,绑定 `form.path`(form 已有该字段,无需改 model/后端)。
|
||||||
- 重置逻辑 Object.assign 补上 `path: undefined`,保证新增时清空。
|
- 重置逻辑 Object.assign 补上 `path: undefined`,保证新增时清空。
|
||||||
- 注意:仅前端 UI 改动,后端表 `shop_goods_category.path` 字段已存在,无需迁移。
|
- 注意:仅前端 UI 改动,后端表 `shop_goods_category.path` 字段已存在,无需迁移。
|
||||||
|
|
||||||
|
## 商品分类列表树形连线高度调紧凑
|
||||||
|
|
||||||
|
- 问题:后台 `/shop/shopGoodsCategory` 列表中,树形层级连线 SVG 高度 40px,导致行显得太高、不紧凑。
|
||||||
|
- 改动:`src/views/shop/shopGoodsCategory/index.vue`
|
||||||
|
- `renderTreePrefix` 中 SVG 高度 `H` 从 `40` 改为 `28`,`mid` 自动为 `14`。
|
||||||
|
- 效果:二级/三级分类前的树形引导线高度降低,整行更紧凑。
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ export interface ShopGoods {
|
|||||||
canExpress?: number;
|
canExpress?: number;
|
||||||
// 商品分类
|
// 商品分类
|
||||||
category?: string;
|
category?: string;
|
||||||
// 商品分类ID
|
// 商品分类ID(主分类,向后兼容)
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
// 商品分类ID列表(方案B:可归属多个分类)
|
||||||
|
categoryIds?: number[];
|
||||||
parentName?: string;
|
parentName?: string;
|
||||||
categoryName?: string;
|
categoryName?: string;
|
||||||
// 一级分类
|
// 一级分类
|
||||||
@@ -174,6 +176,8 @@ export interface ShopGoodsCollectParam {
|
|||||||
export interface ShopGoodsParam extends PageParam {
|
export interface ShopGoodsParam extends PageParam {
|
||||||
parentId?: number;
|
parentId?: number;
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
// 方案B:按多个分类查询,与单值 categoryId 二选一,多值时优先
|
||||||
|
categoryIds?: number[];
|
||||||
goodsId?: number;
|
goodsId?: number;
|
||||||
status?: number;
|
status?: number;
|
||||||
goodsName?: string;
|
goodsName?: string;
|
||||||
|
|||||||
@@ -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>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/system/user/recommend',
|
SERVER_API_URL + '/system/user/recommend',
|
||||||
form
|
form
|
||||||
|
|||||||
@@ -52,15 +52,17 @@
|
|||||||
|
|
||||||
<a-tree-select
|
<a-tree-select
|
||||||
allow-clear
|
allow-clear
|
||||||
|
multiple
|
||||||
|
:max-tag-count="2"
|
||||||
:tree-data="navigationList"
|
:tree-data="navigationList"
|
||||||
tree-default-expand-all
|
tree-default-expand-all
|
||||||
style="width: 180px"
|
style="width: 200px"
|
||||||
:listHeight="700"
|
:listHeight="700"
|
||||||
placeholder="请选择分类"
|
placeholder="请选择分类(可多选)"
|
||||||
:value="where.categoryId || undefined"
|
:value="where.categoryIds && where.categoryIds.length ? where.categoryIds : undefined"
|
||||||
:dropdown-style="{ overflow: 'auto' }"
|
:dropdown-style="{ overflow: 'auto' }"
|
||||||
@update:value="(value?: number) => (where.categoryId = value)"
|
@update:value="(value?: number[]) => (where.categoryIds = value || [])"
|
||||||
@change="onCategoryId"
|
@change="onCategoryIds"
|
||||||
/>
|
/>
|
||||||
<a-input-search
|
<a-input-search
|
||||||
allow-clear
|
allow-clear
|
||||||
@@ -149,6 +151,7 @@
|
|||||||
status: undefined,
|
status: undefined,
|
||||||
stock: undefined,
|
stock: undefined,
|
||||||
categoryId: undefined,
|
categoryId: undefined,
|
||||||
|
categoryIds: [],
|
||||||
merchantId: undefined,
|
merchantId: undefined,
|
||||||
keywords: ''
|
keywords: ''
|
||||||
});
|
});
|
||||||
@@ -238,9 +241,10 @@
|
|||||||
reload();
|
reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 按分类查询
|
// 按分类查询(方案B:支持多选,主分类取首个)
|
||||||
const onCategoryId = (id: number) => {
|
const onCategoryIds = (ids: number[]) => {
|
||||||
where.categoryId = id;
|
where.categoryIds = ids || [];
|
||||||
|
where.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||||
emit('search', where);
|
emit('search', where);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,20 @@
|
|||||||
v-model:value="form.name"
|
v-model:value="form.name"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="商品分类" name="categoryId">
|
<a-form-item label="商品分类" name="categoryIds">
|
||||||
<a-tree-select
|
<a-tree-select
|
||||||
allow-clear
|
allow-clear
|
||||||
|
multiple
|
||||||
|
:max-tag-count="3"
|
||||||
:tree-data="navigationList"
|
:tree-data="navigationList"
|
||||||
tree-default-expand-all
|
tree-default-expand-all
|
||||||
style="width: 320px"
|
style="width: 320px"
|
||||||
placeholder="请选择分类"
|
placeholder="请选择分类(可多选)"
|
||||||
:value="form.categoryId || undefined"
|
:value="form.categoryIds && form.categoryIds.length ? form.categoryIds : undefined"
|
||||||
:listHeight="700"
|
:listHeight="700"
|
||||||
:dropdown-style="{ overflow: 'auto' }"
|
:dropdown-style="{ overflow: 'auto' }"
|
||||||
@update:value="(value?: number) => (form.categoryId = value)"
|
@update:value="(value?: number[]) => (form.categoryIds = value || [])"
|
||||||
@change="onCategoryId"
|
@change="onCategoryIds"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="商品编码" name="code">
|
<a-form-item label="商品编码" name="code">
|
||||||
@@ -928,12 +930,13 @@ const addEnsureTag = (item = '') => {
|
|||||||
ensureTagItem.value = '';
|
ensureTagItem.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择栏目
|
// 选择栏目(方案B:支持多选,主分类取列表首个,向后兼容旧字段)
|
||||||
const onCategoryId = (id: number) => {
|
const onCategoryIds = (ids: number[]) => {
|
||||||
form.categoryId = id;
|
form.categoryIds = ids || [];
|
||||||
|
form.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||||
// 💾 在新增模式下,用户手动选择栏目时也保存到本地存储
|
// 💾 在新增模式下,用户手动选择栏目时也保存到本地存储
|
||||||
if (!isUpdate.value && id) {
|
if (!isUpdate.value && ids && ids.length) {
|
||||||
saveLastCategory(id);
|
saveLastCategory(ids[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1204,6 +1207,7 @@ const form = reactive<ShopGoods>({
|
|||||||
canExpress: 1,
|
canExpress: 1,
|
||||||
unitName: '',
|
unitName: '',
|
||||||
categoryId: undefined,
|
categoryId: undefined,
|
||||||
|
categoryIds: [],
|
||||||
parentName: undefined,
|
parentName: undefined,
|
||||||
categoryName: undefined,
|
categoryName: undefined,
|
||||||
specs: 0,
|
specs: 0,
|
||||||
@@ -1449,14 +1453,18 @@ const chooseSupplier = (item: ShopMerchant) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chooseGoodsCategory = (item: ShopGoodsCategory, value: any) => {
|
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.parentName = value[0].label;
|
||||||
form.categoryName = value[1].label;
|
form.categoryName = value[1].label;
|
||||||
};
|
};
|
||||||
const chooseTakeawayCategory = (item: ShopGoodsCategory, value: any) => {
|
const chooseTakeawayCategory = (item: ShopGoodsCategory, value: any) => {
|
||||||
|
const cid = item[0] as number;
|
||||||
form.categoryParent = '店铺分类';
|
form.categoryParent = '店铺分类';
|
||||||
form.categoryChildren = value[0].label;
|
form.categoryChildren = value[0].label;
|
||||||
form.categoryId = item[0];
|
form.categoryId = cid;
|
||||||
|
form.categoryIds = [cid];
|
||||||
};
|
};
|
||||||
|
|
||||||
const chooseImage = (data: FileRecord) => {
|
const chooseImage = (data: FileRecord) => {
|
||||||
@@ -1788,6 +1796,11 @@ const save = () => {
|
|||||||
if (form.specs === 1 && goodsSpec.value && spec.value.length) {
|
if (form.specs === 1 && goodsSpec.value && spec.value.length) {
|
||||||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||||||
}
|
}
|
||||||
|
// 方案B:主分类兜底取多分类第一个,保证旧字段/旧逻辑兼容
|
||||||
|
if (form.categoryIds && form.categoryIds.length && !form.categoryId) {
|
||||||
|
form.categoryId = form.categoryIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
const formData: any = {
|
const formData: any = {
|
||||||
...form,
|
...form,
|
||||||
content: content.value,
|
content: content.value,
|
||||||
@@ -2050,15 +2063,21 @@ watch(
|
|||||||
if (form.ensureTag) {
|
if (form.ensureTag) {
|
||||||
ensureTag.value = form.ensureTag.split(',');
|
ensureTag.value = form.ensureTag.split(',');
|
||||||
}
|
}
|
||||||
// 🎯 优先级设置栏目:
|
// 🎯 优先级设置栏目(方案B:支持多分类回填):
|
||||||
// 1. 如果传入了 categoryId(从栏目页面点击添加),使用传入的
|
// 1. 如果传入了 categoryIds,使用多分类
|
||||||
// 2. 否则使用上次保存的栏目
|
// 2. 否则如果传入了 categoryId,兼容单分类
|
||||||
if (props.data.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.categoryId = props.data?.categoryId;
|
||||||
|
form.categoryIds = [props.data.categoryId];
|
||||||
} else {
|
} else {
|
||||||
const lastCategory = getLastCategory();
|
const lastCategory = getLastCategory();
|
||||||
if (lastCategory) {
|
if (lastCategory) {
|
||||||
form.categoryId = lastCategory;
|
form.categoryId = lastCategory;
|
||||||
|
form.categoryIds = [lastCategory];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await getRoleList();
|
await getRoleList();
|
||||||
|
|||||||
@@ -269,6 +269,15 @@
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
width: 300
|
width: 300
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'categoryName',
|
||||||
|
key: 'categoryName',
|
||||||
|
align: 'center',
|
||||||
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
|
customRender: ({ text }) => text || '--'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
key: 'priceGroup',
|
key: 'priceGroup',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="record.adImage"
|
v-if="record.adImage"
|
||||||
:src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
|
: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>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
const depth = depthMap.value.get(record.categoryId || 0) || 0;
|
const depth = depthMap.value.get(record.categoryId || 0) || 0;
|
||||||
if (depth === 0) return '';
|
if (depth === 0) return '';
|
||||||
const W = 20;
|
const W = 20;
|
||||||
const H = 40;
|
const H = 20;
|
||||||
const mid = H / 2;
|
const mid = H / 2;
|
||||||
// 回溯祖先链:chain[0]=顶级 ... chain[depth]=当前节点
|
// 回溯祖先链:chain[0]=顶级 ... chain[depth]=当前节点
|
||||||
const chain: number[] = [];
|
const chain: number[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user