feat(module-management): 导航状态化阶段一——分 section 脏检查 + tab 脏点

- useModuleConfig 新增 dirtySections(逐段 JSON.stringify 对比),hasModuleChanges
  改为其派生;先以整树对比语义为基准写等价断言(含「加载补全默认行即脏」的
  既有怪癖),新 spec module-dirty-sections 7 用例全绿
- index.vue #label 插槽给脏 tab 渲染 6px 圆点(title/aria-label,不只靠颜色),
  ui/Tabs 组件零改动;groups 由列表/表单两面板共享编辑,两 tab 联动标脏
- 计划文档 docs/module-management-tab-status-plan.md(横排保留、不做分组为已决策)
This commit is contained in:
oneao committed 2026-08-30 20:06:36 +08:00
1 parent 05d0c7f304
commit 4f89f373e6
4 files changed
+297 -4

No files matched your search

@@ -114,12 +114,32 @@ export function useModuleConfig(deps) {
})
/* ================= 脏检查 ================= */
// 是否有未保存的修改(用于「重载」按钮的确认提示)
const hasModuleChanges = computed(() => {
if (!drafts.value || !originals.value) return false
return JSON.stringify(drafts.value) !== JSON.stringify(originals.value)
// 分 section 脏检查:逐段 JSON.stringify 对比(与整树对比判定结果等价——
// draft/original 均由 loadModuleConfig 以同一组键构造,整树不同 ⟺ 存在段不同)。
// 已知怪癖(既有行为,保留):加载时补全的默认列表行只写 drafts(见 loadModuleConfig),
// 此时 listConfig 打开即「脏」——与原整树对比一致,不得顺手修复。
const SECTION_KEYS = [
'module',
'fields',
'groups',
'listConfig',
'editConfig',
'queryConfig',
'autoCode',
'powers',
]
const dirtySections = computed(() => {
const dirty = new Set()
if (!drafts.value || !originals.value) return dirty
for (const key of SECTION_KEYS) {
if (JSON.stringify(drafts.value[key]) !== JSON.stringify(originals.value[key])) dirty.add(key)
}
return dirty
})
// 是否有未保存的修改(用于切换保护与「重载」按钮的确认提示)
const hasModuleChanges = computed(() => dirtySections.value.size > 0)
function onModuleSelect(keys) {
if (!keys.length) return
const nextId = String(keys[0])
@@ -369,6 +389,8 @@ export function useModuleConfig(deps) {
const missingQueryCount = missingConfigCount('queryConfig')
return {
hasModuleChanges,
dirtySections,
reloadModule,
onModuleSelect,
onFieldsUpdate,
@@ -189,6 +189,26 @@ const configNavItems = computed(() =>
/* 编辑设计器:form/table 两面板 props/emits 契约一致,用动态组件消除模板重复 */
const activeEditPanel = computed(() => (editMode.value === 'table' ? ModuleTableEditPanel : ModuleFormEditPanel))
/* tab 脏点:tab key → drafts section 映射。groups 在列表/表单两个面板都可编辑,
分组变化时两个 tab 都标脏;多语言 tab 的数据独立保存(不走 drafts),不参与脏检查 */
const TAB_SECTION_MAP = {
basic: ['module'],
fields: ['fields'],
list: ['listConfig', 'groups'],
edit: ['editConfig', 'groups'],
query: ['queryConfig'],
'auto-code': ['autoCode'],
powers: ['powers'],
}
const dirtyTabKeys = computed(() => {
const keys = new Set()
for (const [tabKey, sections] of Object.entries(TAB_SECTION_MAP)) {
if (sections.some((section) => dirtySections.value.has(section))) keys.add(tabKey)
}
return keys
})
/* ================= composables ================= */
const { saveModule } = useModuleSave({
selectedModule,
@@ -296,6 +316,7 @@ const {
onFieldsUpdate,
onAutoCodeUpdate,
syncFields,
dirtySections,
missingQueryCount,
fillMissingConfigs,
} = useModuleConfig({
@@ -447,6 +468,14 @@ loadModuleTree()
>
<template #label="{ item }">
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
<!-- 脏点:有未保存修改的 tab 在图标后显示实心圆点(形状+title,不只靠颜色) -->
<span
v-if="dirtyTabKeys.has(item.key)"
class="mm-config-nav__dirty"
role="img"
title="有未保存修改"
aria-label="有未保存修改"
></span>
<span class="mm-config-nav__label">{{ item.label }}</span>
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
</template>
@@ -826,6 +855,15 @@ loadModuleTree()
color: var(--fms-text-secondary);
}
/* 脏点:作为 label 插槽的 flex 子元素排在图标后,间距由 .tabs-tab 的 gap 统一管理 */
.mm-config-nav__dirty {
flex: none;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fms-primary);
}
.mm-config-nav .tabs-tab-active .mm-config-nav__icon {
color: var(--fms-primary);
}