修复商品分类树名称展示

This commit is contained in:
czl231
2026-08-15 23:47:33 +08:00
parent cc14cb9f62
commit 0b6e7b0598
5 changed files with 160 additions and 17 deletions

View 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` 字段,兼容现有两类树形资源。
- 静态回归检查可防止后续再次误用标题插槽参数。

View File

@@ -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` 插槽,修复树形表格容器存在但列和数据不渲染的问题。

View File

@@ -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"',

View File

@@ -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() {

View File

@@ -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;
}