From b9c3a64c3404f419516895d7769348c09474d0a2 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 11:06:21 +0800 Subject: [PATCH] feat: resource management page + 404 fallback + header fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 资源管理:树形表格 + type 联动表单(menu/button/api 字段显隐) - ty-form 新增 when 条件字段显隐;ty-crud 表单模型改 reactive - 抽出 src/utils/tree.ts(buildTree/filterTree/collectDescendants),组织页复用 - 404 兜底:catch-all 路由 [...all].vue - 顶栏去掉横向菜单底边框;修复切 Tab 顶部一级菜单高亮不跟随 Co-Authored-By: Claude Opus 4.8 --- src/components/ty-crud.vue | 2 +- src/components/ty-form.vue | 16 +- src/components/ty-header.vue | 25 +- src/pages/[...all].vue | 40 +++ src/pages/system/org/index.vue | 81 +----- src/pages/system/resource/index.vue | 404 ++++++++++++++++++++++++++++ src/types/crud.ts | 2 + src/utils/tree.ts | 72 +++++ stats.html | 2 +- typed-router.d.ts | 26 ++ 10 files changed, 590 insertions(+), 80 deletions(-) create mode 100644 src/pages/[...all].vue create mode 100644 src/pages/system/resource/index.vue create mode 100644 src/utils/tree.ts diff --git a/src/components/ty-crud.vue b/src/components/ty-crud.vue index 66d5a04..976ec37 100644 --- a/src/components/ty-crud.vue +++ b/src/components/ty-crud.vue @@ -136,7 +136,7 @@ const selectedRows = ref[]>([]) const query = reactive>(buildModel(props.searchSchema)) function buildModel(schema?: JsonSchema): Record { - const model: Record = {} + const model: Record = reactive({}) if (!schema) return model for (const [key, prop] of Object.entries(schema.properties)) { if (prop.default !== undefined) model[key] = prop.default diff --git a/src/components/ty-form.vue b/src/components/ty-form.vue index 76d5705..3afb23a 100644 --- a/src/components/ty-form.vue +++ b/src/components/ty-form.vue @@ -122,17 +122,25 @@ function onEnter(): void { if (props.enterToSubmit) emit('submit') } -/** 按 mode 过滤 showOn 字段。 */ +/** 按 mode 过滤 showOn 字段,并应用 when 条件(依赖字段值联动显隐)。 */ const visibleProperties = computed(() => { const result: Record = {} for (const [key, prop] of Object.entries(props.schema.properties)) { - if (!props.mode || !prop.showOn || prop.showOn.includes(props.mode)) { - result[key] = prop - } + if (props.mode && prop.showOn && !prop.showOn.includes(props.mode)) continue + if (prop.when && !matchesWhen(props.modelValue, prop.when)) continue + result[key] = prop } return result }) +/** when 条件匹配:依赖字段值等于 equals 或属于 in。 */ +function matchesWhen(model: Record, when: { field: string; equals?: unknown; in?: unknown[] }): boolean { + const value = model[when.field] + if (when.equals !== undefined) return value === when.equals + if (when.in) return when.in.includes(value) + return true +} + const rules = computed(() => { const result: FormRules = {} for (const [key, prop] of Object.entries(props.schema.properties)) { diff --git a/src/components/ty-header.vue b/src/components/ty-header.vue index 9d27a5e..c9bc150 100644 --- a/src/components/ty-header.vue +++ b/src/components/ty-header.vue @@ -16,7 +16,7 @@ class="ty-header__nav" mode="horizontal" :ellipsis="false" - :default-active="activePath" + :default-active="activeIndex" @select="onNavSelect" > + + diff --git a/src/pages/system/org/index.vue b/src/pages/system/org/index.vue index e666748..a0b71d2 100644 --- a/src/pages/system/org/index.vue +++ b/src/pages/system/org/index.vue @@ -44,6 +44,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import type { JsonSchema, TableColumn } from '@/types/crud' import TyForm from '@/components/ty-form.vue' import { showDialog } from '@/utils/dialog' +import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree' const STATUS_MAP = { enabled: { label: '启用', type: 'success' }, @@ -62,9 +63,7 @@ interface OrgRow { createdAt: number } -interface OrgNode extends OrgRow { - children: OrgNode[] -} +type OrgNode = TreeNode const now = Date.now() const day = 24 * 60 * 60 * 1000 @@ -112,59 +111,18 @@ const searchSchema: JsonSchema = { }, } -// ---- 树构建 / 过滤 ---- -function buildTree(rows: OrgRow[]): OrgNode[] { - const map = new Map() - for (const r of rows) map.set(r.code, { ...r, children: [] }) - const roots: OrgNode[] = [] - for (const node of map.values()) { - const parent = node.parentCode ? map.get(node.parentCode) : undefined - if (parent) parent.children.push(node) - else roots.push(node) - } - const sortRec = (nodes: OrgNode[]): void => { - nodes.sort((a, b) => a.sort - b.sort || a.code.localeCompare(b.code)) - for (const n of nodes) sortRec(n.children) - } - sortRec(roots) - return roots -} - -/** 过滤:保留匹配节点 + 其祖先 + 其整棵子树。 */ -function filterTree(rows: OrgRow[], name: string, code: string, status: string): OrgRow[] { - if (!name && !code && !status) return rows - const byCode = new Map(rows.map((r) => [r.code, r])) - const matches = rows.filter( - (r) => (!name || r.name.includes(name)) && (!code || r.code.includes(code)) && (!status || r.status === status), - ) - const kept = new Set() - for (const m of matches) { - // 祖先链 - let cur: OrgRow | undefined = m - while (cur) { - kept.add(cur.code) - cur = cur.parentCode ? byCode.get(cur.parentCode) : undefined - } - // 子树 - const stack = [m.code] - while (stack.length) { - const c = stack.pop()! - for (const r of rows) { - if (r.parentCode === c) { - kept.add(r.code) - stack.push(r.code) - } - } - } - } - return rows.filter((r) => kept.has(r.code)) -} - const searchQuery = reactive({ name: '', code: '', status: '' }) const appliedQuery = reactive({ name: '', code: '', status: '' }) const displayTree = computed(() => - buildTree(filterTree(orgs.value, appliedQuery.name.trim(), appliedQuery.code.trim(), appliedQuery.status)), + buildTree( + filterTree(orgs.value, (r) => { + const name = appliedQuery.name.trim() + const code = appliedQuery.code.trim() + const status = appliedQuery.status + return (!name || r.name.includes(name)) && (!code || r.code.includes(code)) && (!status || r.status === status) + }), + ), ) function handleSearch(): void { @@ -234,21 +192,6 @@ function delay(fn: () => T, ms = 300): Promise { return new Promise((resolve) => setTimeout(() => resolve(fn()), ms)) } -function collectDescendants(code: string): string[] { - const result: string[] = [] - const stack = [code] - while (stack.length) { - const c = stack.pop()! - for (const o of orgs.value) { - if (o.parentCode === c) { - result.push(o.code) - stack.push(o.code) - } - } - } - return result -} - async function openCreate(): Promise { const data = await showDialog({ component: TyForm, @@ -332,7 +275,7 @@ async function openEdit(row: Record): Promise { async function handleDelete(row: Record): Promise { const o = row as OrgRow await ElMessageBox.confirm(`确认删除组织「${o.name}」及其所有下级组织?`, '提示', { type: 'warning' }) - const codes = new Set([o.code, ...collectDescendants(o.code)]) + const codes = new Set([o.code, ...collectDescendants(orgs.value, o.code)]) orgs.value = orgs.value.filter((x) => !codes.has(x.code)) ElMessage.success('删除成功') } @@ -348,7 +291,7 @@ async function batchDelete(rows: Record[]): Promise { const codes = new Set() for (const r of rows) { codes.add(String(r.code)) - for (const c of collectDescendants(String(r.code))) codes.add(c) + for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c) } orgs.value = orgs.value.filter((o) => !codes.has(o.code)) selectedRows.value = [] diff --git a/src/pages/system/resource/index.vue b/src/pages/system/resource/index.vue new file mode 100644 index 0000000..e08ebcb --- /dev/null +++ b/src/pages/system/resource/index.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/types/crud.ts b/src/types/crud.ts index 75442bb..e058144 100644 --- a/src/types/crud.ts +++ b/src/types/crud.ts @@ -30,6 +30,8 @@ export interface JsonSchemaProperty { treeData?: Record[] /** format=tree-select 时的树字段映射。 */ treeProps?: { label?: string; value?: string; children?: string } + /** 字段显示条件:仅当依赖字段值匹配时显示(如 type=menu 时显示 path)。 */ + when?: { field: string; equals?: unknown; in?: unknown[] } } /** JSON Schema 对象(表单 schema)。 */ diff --git a/src/utils/tree.ts b/src/utils/tree.ts new file mode 100644 index 0000000..4963cad --- /dev/null +++ b/src/utils/tree.ts @@ -0,0 +1,72 @@ +/** 树操作工具:用于 code/parentCode 组织的扁平树数据。 */ + +export interface FlatNode { + code: string + parentCode: string | null + sort?: number +} + +/** 树节点:扁平节点 + children。 */ +export type TreeNode = T & { children: TreeNode[] } + +/** 由扁平列表构建树(按 sort + code 升序)。 */ +export function buildTree(rows: T[]): TreeNode[] { + const map = new Map>() + for (const r of rows) map.set(r.code, { ...r, children: [] }) + const roots: TreeNode[] = [] + for (const node of map.values()) { + const parent = node.parentCode ? map.get(node.parentCode) : undefined + if (parent) parent.children.push(node) + else roots.push(node) + } + const sortRec = (nodes: TreeNode[]): void => { + nodes.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.code.localeCompare(b.code)) + for (const n of nodes) sortRec(n.children) + } + sortRec(roots) + return roots +} + +/** 过滤:保留匹配节点 + 其祖先 + 其整棵子树。 */ +export function filterTree(rows: T[], match: (row: T) => boolean): T[] { + const matched = rows.filter(match) + if (matched.length === rows.length) return rows + const byCode = new Map(rows.map((r) => [r.code, r])) + const kept = new Set() + for (const m of matched) { + // 祖先链 + let cur: T | undefined = m + while (cur) { + kept.add(cur.code) + cur = cur.parentCode ? byCode.get(cur.parentCode) : undefined + } + // 子树 + const stack = [m.code] + while (stack.length) { + const c = stack.pop()! + for (const r of rows) { + if (r.parentCode === c) { + kept.add(r.code) + stack.push(r.code) + } + } + } + } + return rows.filter((r) => kept.has(r.code)) +} + +/** 收集某节点及其全部后代 code。 */ +export function collectDescendants(rows: T[], code: string): string[] { + const result: string[] = [] + const stack = [code] + while (stack.length) { + const c = stack.pop()! + for (const r of rows) { + if (r.parentCode === c) { + result.push(r.code) + stack.push(r.code) + } + } + } + return result +} diff --git a/stats.html b/stats.html index acf2dc2..d6cb0c9 100644 --- a/stats.html +++ b/stats.html @@ -4930,7 +4930,7 @@ var drawChart = (function (exports) {