feat(module-management): 导航状态化阶段三——概览配置完成度 checklist

- 新组件 ModuleOverviewChecklist:字段/表单/列表/查询/权限按依赖顺序排列,
  状态以文案表达(缺 N/已配置 N,不只靠颜色),空态给引导文案
- 条目点击与 Enter 跳转对应分节;查询缺配提供「一键补充」
  (复用 fillMissingConfigs,不冒泡为跳转);仅数据/查询模块渲染
- index.vue 补 fieldsEnabledCount/listVisibleCount/powersCount 派生统计;
  表单/查询缺配复用阶段二/既有的 missingEditCount/missingQueryCount
- 新 spec module-overview-checklist 7 用例:条目显隐、缺配/完整/空态文案、
  跳转与一键补充事件
This commit is contained in:
oneao committed 2026-08-30 20:10:53 +08:00
1 parent 6bfa0d816f
commit 29f98494af
3 files changed
+273

No files matched your search

@@ -0,0 +1,173 @@
<script setup>
/**
* 概览配置完成度清单:从 drafts 派生各分节配置状态,条目按依赖顺序排列
* (字段 → 表单 → 列表 → 查询 → 权限),隐式表达配置路径,不做向导式交互。
* 点击条目跳转对应分节(navigate);查询缺配可直接一键补充(fill-missing)。
* 状态不只靠颜色表达:文案本身描述缺配/完整(「缺 N」「已配置 N」)。
*/
import { computed } from 'vue'
import { Button } from '@/components/ui'
const props = defineProps({
/** 模块类型:data/query 才渲染清单,分类模块由父级直接不渲染 */
moduleType: { type: String, default: null },
fieldsEnabledCount: { type: Number, default: 0 },
/** 表单设计缺配(仅 data 模块有此项) */
editMissingCount: { type: Number, default: 0 },
listVisibleCount: { type: Number, default: 0 },
queryMissingCount: { type: Number, default: 0 },
powersCount: { type: Number, default: 0 },
})
const emit = defineEmits(['navigate', 'fill-missing'])
const isData = computed(() => props.moduleType === 'data')
const rows = computed(() => {
const list = []
list.push({
key: 'fields',
label: '字段定义',
status: props.fieldsEnabledCount ? 'ok' : 'hint',
text: props.fieldsEnabledCount ? `已启用 ${props.fieldsEnabledCount} 个字段` : '尚未定义启用字段,先去定义字段',
})
if (isData.value) {
list.push({
key: 'edit',
label: '表单设计',
status: props.editMissingCount ? 'warn' : 'ok',
text: props.editMissingCount
? `缺 ${props.editMissingCount} 项未布局`
: '启用字段均已布局',
})
}
// 列表行加载时自动补全,无「缺失」概念,仅展示数量
list.push({
key: 'list',
label: '列表配置',
status: 'neutral',
text: props.listVisibleCount ? `${props.listVisibleCount} 列可见` : '暂无可见列',
})
list.push({
key: 'query',
label: '查询配置',
status: props.queryMissingCount ? 'warn' : 'ok',
text: props.queryMissingCount ? `缺 ${props.queryMissingCount} 项,可一键补充` : '查询配置完整',
action: props.queryMissingCount ? '一键补充' : '',
})
list.push({
key: 'powers',
label: '权限',
status: props.powersCount ? 'ok' : 'neutral',
text: props.powersCount ? `已配置 ${props.powersCount} 项` : '尚未配置权限',
})
return list
})
</script>
<template>
<section class="mm-checklist" aria-label="配置完成度">
<div class="mm-checklist__title">配置完成度</div>
<ul class="mm-checklist__list">
<li
v-for="row in rows"
:key="row.key"
class="mm-checklist__item"
:class="`is-${row.status}`"
role="button"
tabindex="0"
:title="`前往${row.label}`"
@click="emit('navigate', row.key)"
@keydown.enter.prevent="emit('navigate', row.key)"
>
<span class="mm-checklist__label">{{ row.label }}</span>
<span class="mm-checklist__text">{{ row.text }}</span>
<Button
v-if="row.action"
type="link"
class="mm-checklist__action"
@click.stop="emit('fill-missing')"
>
{{ row.action }}
</Button>
</li>
</ul>
</section>
</template>
<style lang="scss" scoped>
.mm-checklist {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.mm-checklist__title {
color: var(--fms-text-secondary);
font-size: 13px;
font-weight: 600;
}
.mm-checklist__list {
display: flex;
flex-direction: column;
gap: 2px;
margin: 0;
padding: 0;
list-style: none;
}
/* 条目整行可点击跳转;hover 仅给轻微反馈,不与右侧操作按钮抢焦点 */
.mm-checklist__item {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 4px 8px;
border-radius: var(--fms-control-radius);
cursor: pointer;
transition: background 150ms ease;
&:hover,
&:focus-visible {
background: var(--fms-secondary);
outline: none;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--fms-primary);
}
}
.mm-checklist__label {
flex: none;
width: 72px;
color: var(--fms-text);
}
.mm-checklist__text {
flex: 1;
min-width: 0;
color: var(--fms-text-secondary);
}
.mm-checklist__action {
flex: none;
padding: 0;
}
/* 状态通过文案表达(缺 N / 已配置 N),颜色仅作辅助 */
.mm-checklist__item.is-warn .mm-checklist__text {
color: var(--fms-danger, #d95757);
}
.mm-checklist__item.is-ok .mm-checklist__text {
color: var(--fms-text-secondary);
}
.mm-checklist__item.is-hint .mm-checklist__text {
color: var(--fms-text);
font-weight: 500;
}
</style>
@@ -40,6 +40,7 @@ import { useModuleTree, findModuleNode } from './composables/useModuleTree'
import { useModuleConfig } from './composables/useModuleConfig'
import { useModuleSave } from './composables/useModuleSave'
import ModuleBasicPanel from './components/ModuleBasicPanel.vue'
import ModuleOverviewChecklist from './components/ModuleOverviewChecklist.vue'
import ModuleFieldsPanel from './components/ModuleFieldsPanel.vue'
import ModuleListConfigPanel from './components/ModuleListConfigPanel.vue'
import ModuleFormEditPanel from './components/form-edit/FormEditPanel.vue'
@@ -215,6 +216,16 @@ const dirtyTabKeys = computed(() => {
return keys
})
/* 概览 checklist 统计(由 drafts 派生):字段/列表/权限计数,表单与查询缺配复用
useModuleConfig 的 missingEditCount / missingQueryCount */
const fieldsEnabledCount = computed(
() => (drafts.value?.fields || []).filter((field) => Number(field.b_canuse) === 1).length,
)
const listVisibleCount = computed(
() => (drafts.value?.listConfig || []).filter((row) => Number(row.b_visible) === 1).length,
)
const powersCount = computed(() => (drafts.value?.powers || []).length)
/* ================= composables ================= */
const { saveModule } = useModuleSave({
selectedModule,
@@ -491,6 +502,18 @@ loadModuleTree()
<div class="mm-body__inner">
<div v-if="drafts" class="mm-config-content">
<div v-show="activeSection === 'basic'" class="mm-config-pane">
<!-- 完成度清单仅数据/查询模块渲染,条目点击跳转对应分节 -->
<ModuleOverviewChecklist
v-if="isDataModule || isQueryModule"
:module-type="moduleType"
:fields-enabled-count="fieldsEnabledCount"
:edit-missing-count="missingEditCount"
:list-visible-count="listVisibleCount"
:query-missing-count="missingQueryCount"
:powers-count="powersCount"
@navigate="(key) => (activeSection = key)"
@fill-missing="() => fillMissingConfigs(['queryConfig'])"
/>
<ModuleBasicPanel
ref="basicPanelRef"
:data="drafts?.module"
@@ -0,0 +1,77 @@
/**
* 概览完成度清单(ModuleOverviewChecklist)测试:
* - 条目随模块类型显隐(data 含表单设计,query 不含)
* - 缺配/完整/空态文案(状态不只靠颜色,文案本身是断言对象)
* - 条目点击与键盘 Enter 跳转对应分节;「一键补充」不冒泡为跳转
*/
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import ModuleOverviewChecklist from '@/views/system/module-management/components/ModuleOverviewChecklist.vue'
function mountList(props) {
return mount(ModuleOverviewChecklist, {
props: {
moduleType: 'data',
fieldsEnabledCount: 5,
editMissingCount: 0,
listVisibleCount: 5,
queryMissingCount: 0,
powersCount: 3,
...props,
},
})
}
function rowOf(wrapper, label) {
return wrapper.findAll('.mm-checklist__item').find((item) => item.text().includes(label))
}
describe('ModuleOverviewChecklist', () => {
it('data 模块渲染全部条目,按依赖顺序排列', () => {
const wrapper = mountList({})
const labels = wrapper.findAll('.mm-checklist__label').map((el) => el.text())
expect(labels).toEqual(['字段定义', '表单设计', '列表配置', '查询配置', '权限'])
})
it('query 模块不渲染表单设计条目', () => {
const wrapper = mountList({ moduleType: 'query' })
const labels = wrapper.findAll('.mm-checklist__label').map((el) => el.text())
expect(labels).toEqual(['字段定义', '列表配置', '查询配置', '权限'])
})
it('缺配状态以文案表达:表单缺 N、查询缺 N 且提供一键补充', () => {
const wrapper = mountList({ editMissingCount: 2, queryMissingCount: 3 })
expect(rowOf(wrapper, '表单设计').text()).toContain('缺 2 项未布局')
const queryRow = rowOf(wrapper, '查询配置')
expect(queryRow.text()).toContain('缺 3 项')
expect(queryRow.find('button').exists()).toBe(true)
})
it('完整状态文案与无补充按钮', () => {
const wrapper = mountList({})
expect(rowOf(wrapper, '表单设计').text()).toContain('均已布局')
expect(rowOf(wrapper, '查询配置').text()).toContain('完整')
expect(wrapper.find('button').exists()).toBe(false)
})
it('空态提示:无启用字段时引导先定义字段', () => {
const wrapper = mountList({ fieldsEnabledCount: 0, editMissingCount: 0, listVisibleCount: 0, powersCount: 0 })
expect(rowOf(wrapper, '字段定义').text()).toContain('先去定义字段')
expect(rowOf(wrapper, '列表配置').text()).toContain('暂无可见列')
expect(rowOf(wrapper, '权限').text()).toContain('尚未配置权限')
})
it('条目点击与 Enter 均触发 navigate,携带对应分节 key', async () => {
const wrapper = mountList({})
await rowOf(wrapper, '字段定义').trigger('click')
await rowOf(wrapper, '查询配置').trigger('keydown', { key: 'Enter' })
expect(wrapper.emitted('navigate')).toEqual([['fields'], ['query']])
})
it('一键补充按钮触发 fill-missing 且不触发 navigate', async () => {
const wrapper = mountList({ queryMissingCount: 3 })
await rowOf(wrapper, '查询配置').find('button').trigger('click')
expect(wrapper.emitted('fill-missing')).toHaveLength(1)
expect(wrapper.emitted('navigate')).toBeUndefined()
})
})