修复商品分类树名称展示
This commit is contained in:
45
docs/操作日志_商品分类树名称显示修复_20260815.md
Normal file
45
docs/操作日志_商品分类树名称显示修复_20260815.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 商品分类树名称显示修复操作日志
|
||||
|
||||
操作时间:2026-08-15
|
||||
|
||||
操作类型:修改、扩展
|
||||
|
||||
影响模块:平台总后台商品分类与平台菜单共享树页面
|
||||
|
||||
## 操作前状态
|
||||
|
||||
树组件已将接口 `name` 字段配置为标题来源,但自定义标题插槽错误读取原始节点中不存在的 `title` 字段,导致中文业务名称为空,页面只剩系统唯一标识的末尾 12 位和操作按钮。
|
||||
|
||||
## 具体操作
|
||||
|
||||
1. 树节点主标题改为读取接口原始节点的 `name` 字段。
|
||||
2. 保留现有系统唯一标识复制、父子层级、编辑、启停和归档行为。
|
||||
3. 在平台资源中文展示契约检查中增加树节点名称断言,并禁止回退到错误的 `node.title`。
|
||||
4. 按确认方案将共享树页升级为树形表格,增加名称、排序、状态、系统标识和操作列。
|
||||
5. 唯一标识由 12 位缩略正文降级为“复制标识”入口,完整值仍可通过提示和剪贴板获取。
|
||||
6. 浏览器回归发现 Arco 声明式表格列缺少 `columns` 插槽导致页面空白;补齐插槽并增加静态回归断言。
|
||||
|
||||
## 操作后状态
|
||||
|
||||
商品分类和平台菜单均以中文业务名称作为树节点主标题,并通过表格列明确表达排序和状态;系统唯一标识作为次要复制入口展示。商品分类保留编辑、启停和归档操作,平台菜单继续保持只读。
|
||||
|
||||
## 代码变更
|
||||
|
||||
- `frontend/platform_admin/src/views/shared/TreePage.vue`:修复树节点主标题字段并补充中文说明。
|
||||
- `frontend/platform_admin/src/components/IdentityText.vue`:扩展可选复制入口文案,默认行为保持兼容。
|
||||
- `frontend/platform_admin/scripts/check-resource-display-contracts.mjs`:增加树节点中文名称回归检查。
|
||||
- `docs/项目文档_平台资源中文展示统一_v1.0.md`:补充 v1.1 变更记录。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `pnpm run resource-display-contracts:check`:通过。
|
||||
- `pnpm run type:check`:通过。
|
||||
- `pnpm run build`:通过,树形表格改造后 Vite 成功转换 2621 个模块并生成生产构建。
|
||||
- 变更文件定向 Biome 检查:退出码为 0;现有 Vue 模板变量识别仍产生非阻断警告,本次未扩大为全仓格式化。
|
||||
- 本地浏览器回归:修复前表格容器存在但表头数为 0;补齐 `columns` 插槽后表头数为 5,可正常显示空状态,控制台无错误。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 修改仅影响树节点主标题文本,不改变接口、数据库、树层级、权限或业务动作。
|
||||
- 商品分类和平台菜单接口均提供 `name` 字段,兼容现有两类树形资源。
|
||||
- 静态回归检查可防止后续再次误用标题插槽参数。
|
||||
@@ -61,3 +61,6 @@ frontend/platform_admin/
|
||||
- v1.0:建立 48 类资源中文展示契约,补齐原 19 类空字段只读资源。
|
||||
- v1.0:统一业务列优先、关系名称跳转、UUID 次要复制和响应式表格。
|
||||
- v1.0:建立固定聚合子表和完整 JSON 查看规则。
|
||||
- v1.1:修复树标题插槽误读 `title` 导致中文名称为空的问题,并增加树节点名称回归检查。
|
||||
- v1.2:树形资源统一改为树形表格,明确展示名称、排序、状态、系统标识和操作,唯一标识降级为复制入口。
|
||||
- v1.2.1:补齐 Arco 表格 `columns` 插槽,修复树形表格容器存在但列和数据不渲染的问题。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 功能描述:校验平台48类资源均具备中文展示字段,并防止列表、详情退回数据库ID或动态英文列。
|
||||
* 版本:v1.0.0
|
||||
* 版本:v1.2.1
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -23,6 +23,8 @@ const display = read('src/api/resource-display.ts');
|
||||
const detailContract = read('src/api/resource-detail-contract.ts');
|
||||
const detailPage = read('src/views/resource/ResourceDetailContent.vue');
|
||||
const listPage = read('src/views/shared/CrudListPage.vue');
|
||||
const treePage = read('src/views/shared/TreePage.vue');
|
||||
const identityText = read('src/components/IdentityText.vue');
|
||||
|
||||
const resourceNames = [...resources.matchAll(/\bdefine\('([^']+)'/g)].map(
|
||||
(match) => match[1],
|
||||
@@ -81,6 +83,37 @@ if (/title="ID"|data-index="id"/.test(listPage)) {
|
||||
throw new Error('标准资源列表不得把数据库ID作为业务列展示');
|
||||
}
|
||||
assertIncludes(listPage, 'title="系统唯一标识"', '列表必须中文标注系统唯一标识');
|
||||
assertIncludes(
|
||||
treePage,
|
||||
'<a-table',
|
||||
'树形资源必须使用带明确列标题的树形表格',
|
||||
);
|
||||
assertIncludes(
|
||||
treePage,
|
||||
'<template #columns>',
|
||||
'Arco 树形表格列必须放入 columns 插槽,避免运行时渲染为空白',
|
||||
);
|
||||
for (const columnTitle of ['分类名称', '排序', '状态', '系统标识', '操作']) {
|
||||
assertIncludes(treePage, columnTitle, `树形表格缺少必要列:${columnTitle}`);
|
||||
}
|
||||
assertIncludes(
|
||||
treePage,
|
||||
'{{ record.name }}',
|
||||
'树形表格必须以接口返回的中文业务名称作为主标题',
|
||||
);
|
||||
assertIncludes(
|
||||
treePage,
|
||||
'display-text="复制标识"',
|
||||
'树形表格不得把截断唯一标识作为高优先级正文',
|
||||
);
|
||||
assertIncludes(
|
||||
identityText,
|
||||
'displayText?: string',
|
||||
'唯一标识组件必须保留可选的低优先级复制文案能力',
|
||||
);
|
||||
if (treePage.includes('node.title')) {
|
||||
throw new Error('树节点不得读取标题插槽中不存在的 title 字段');
|
||||
}
|
||||
assertIncludes(
|
||||
listPage,
|
||||
'class="resource-table-shell"',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!-- 功能描述:紧凑展示资源唯一标识,并支持点击复制完整值。版本:v1.1.0。 -->
|
||||
<!-- 功能描述:紧凑展示资源唯一标识或复制入口,并支持点击复制完整值。版本:v1.2.0。 -->
|
||||
<template>
|
||||
<a-tooltip :content="`${value}\n点击复制完整唯一标识`">
|
||||
<button class="identity-text" type="button" :aria-label="`复制完整唯一标识 ${value}`" @click="copyIdentity">
|
||||
{{ shortValue }}
|
||||
{{ displayText ?? shortValue }}
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{ value: string }>();
|
||||
const props = defineProps<{ value: string; displayText?: string }>();
|
||||
const shortValue = computed(() => props.value.slice(-12));
|
||||
|
||||
async function copyIdentity() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- 功能描述:使用树形表格展示和维护层级资源,中文业务名称优先、系统唯一标识次要展示。版本:v1.2.1。 -->
|
||||
<template>
|
||||
<a-card :title="definition.title" :bordered="false">
|
||||
<template #extra>
|
||||
@@ -6,20 +7,54 @@
|
||||
<a-button v-if="canCreate" type="primary" @click="openCreate">新增</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-tree :data="tree" :loading="loading" :field-names="{ key: 'identity', title: 'name', children: 'children' }">
|
||||
<template #title="node">
|
||||
<a-space>
|
||||
<span>{{ node.title }}</span>
|
||||
<span v-if="node.group_code || node.path" class="tree-business-key">
|
||||
{{ node.group_code || node.path }}
|
||||
</span>
|
||||
<IdentityText :value="String(node.identity)" />
|
||||
<a-button v-if="canEdit" size="mini" @click.stop="openEdit(node)">编辑</a-button>
|
||||
<a-button v-if="canChangeStatus" size="mini" @click.stop="confirmStatus(node)">{{ node.status === 1 ? '停用' : '启用' }}</a-button>
|
||||
<a-button v-if="canArchive" size="mini" status="danger" @click.stop="confirmArchive(node)">归档</a-button>
|
||||
</a-space>
|
||||
<a-table
|
||||
class="tree-table"
|
||||
:data="tree"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="identity"
|
||||
default-expand-all-rows
|
||||
hide-expand-button-on-empty
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column :title="nameColumnTitle" data-index="name" :width="360">
|
||||
<template #cell="{ record }">
|
||||
<div class="tree-name-cell">
|
||||
<span class="tree-name">{{ record.name }}</span>
|
||||
<span v-if="record.group_code || record.path" class="tree-business-key">
|
||||
{{ record.group_code || record.path }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="排序" data-index="sort_no" :width="90" align="center">
|
||||
<template #cell="{ record }">{{ record.sort_no ?? '-' }}</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" data-index="status" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="statusColor(Number(record.status))">
|
||||
{{ recordStatusLabel(Number(record.status)) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="系统标识" :width="120">
|
||||
<template #cell="{ record }">
|
||||
<IdentityText :value="String(record.identity)" display-text="复制标识" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column v-if="canEdit || canChangeStatus || canArchive" title="操作" :width="230" fixed="right">
|
||||
<template #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button v-if="canEdit" size="mini" @click="openEdit(record)">编辑</a-button>
|
||||
<a-button v-if="canChangeStatus" size="mini" @click="confirmStatus(record)">
|
||||
{{ record.status === 1 ? '停用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button v-if="canArchive" size="mini" status="danger" @click="confirmArchive(record)">归档</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-drawer :visible="formVisible" :title="editingIdentity ? `编辑${definition.title}` : `新增${definition.title}`" :width="480" @cancel="formVisible = false" @ok="save">
|
||||
<a-form :model="form" layout="vertical">
|
||||
@@ -41,6 +76,7 @@
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
import { recordStatusLabel } from '@/api/resource-display';
|
||||
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
|
||||
import type { ResourceUiDefinition } from '@/api/resources';
|
||||
import { useUserStore } from '@/store';
|
||||
@@ -71,6 +107,9 @@ const canChangeStatus = computed(
|
||||
const canArchive = computed(
|
||||
() => props.definition.canArchive && rootAllowed.value,
|
||||
);
|
||||
const nameColumnTitle = computed(() =>
|
||||
props.definition.name === 'ec_category' ? '分类名称' : '菜单名称',
|
||||
);
|
||||
const tree = computed(() => {
|
||||
const byIdentity = new Map<string, Node>();
|
||||
const roots: Node[] = [];
|
||||
@@ -94,6 +133,13 @@ const tree = computed(() => {
|
||||
return roots;
|
||||
});
|
||||
|
||||
/** 返回树形表格状态标签颜色,名称仍由统一状态字典提供。 */
|
||||
function statusColor(status: number) {
|
||||
if (status === 1) return 'green';
|
||||
if (status === 2) return 'orange';
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
function reset(data?: Node) {
|
||||
for (const field of props.definition.fields) {
|
||||
const value = data?.[field.key];
|
||||
@@ -192,7 +238,23 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-table {
|
||||
width: 100%;
|
||||
}
|
||||
.tree-name-cell {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.tree-name {
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tree-business-key {
|
||||
flex: none;
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user