liuyun 23e228783c feat: dict-driven rendering — useDict + component dictCode
- 新增 dict store 与 useDict Hook(登录后缓存、登出清空)
- ty-form/ty-table 支持 dictCode 自动解析下拉与标签
- 用户/角色/资源/组织/租户/审计/登录日志页去硬编码映射
- 字典管理页内置标记、value 保护、标签色编辑

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 14:29:28 +08:00

279 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="org-page">
<el-card class="org-page__search" shadow="never">
<ty-search-form :schema="searchSchema" :model-value="searchQuery" @search="handleSearch" @reset="handleReset" />
</el-card>
<el-card class="org-page__table" shadow="never">
<div class="org-page__toolbar">
<div class="org-page__toolbar-left">
<el-button type="primary" @click="openCreate">新增组织</el-button>
</div>
<div class="org-page__toolbar-right">
<el-button type="danger" :disabled="selectedRows.length === 0" @click="batchDelete(selectedRows)">
批量删除
</el-button>
</div>
</div>
<div class="org-page__table-body">
<ty-table
:columns="columns"
:data="displayTree"
selection
row-key="code"
:tree-props="{ children: 'children' }"
default-expand-all
:actions-width="180"
@selection-change="onSelectionChange"
>
<template #actions="{ row }">
<el-button link type="primary" @click="openCreateChild(row)">新增子</el-button>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</ty-table>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
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 { useRequest } from '@/composables/useRequest'
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
import { createOrg, deleteOrg, listOrgs, updateOrg, type Org } from '@/api/org'
type OrgNode = TreeNode<Org>
const { data: orgList, refresh: refreshOrgs } = useRequest((signal: AbortSignal) => listOrgs(signal))
const orgs = computed<Org[]>(() => orgList.value ?? [])
const columns: TableColumn[] = [
{ prop: 'name', label: '组织名称', minWidth: 220 },
{ prop: 'code', label: '组织编码', minWidth: 120 },
{ prop: 'leader', label: '负责人', width: 100 },
{ prop: 'sort', label: '排序', width: 70, align: 'center' },
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
]
const searchSchema: JsonSchema = {
type: 'object',
properties: {
name: { type: 'string', title: '组织名称', placeholder: '请输入名称' },
code: { type: 'string', title: '组织编码', placeholder: '请输入编码' },
status: {
type: 'string',
title: '状态',
dictCode: 'common_status',
placeholder: '全部',
},
},
}
const searchQuery = reactive({ name: '', code: '', status: '' })
const appliedQuery = reactive({ name: '', code: '', status: '' })
const displayTree = computed<OrgNode[]>(() =>
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 {
appliedQuery.name = searchQuery.name
appliedQuery.code = searchQuery.code
appliedQuery.status = searchQuery.status
}
function handleReset(): void {
appliedQuery.name = ''
appliedQuery.code = ''
appliedQuery.status = ''
}
// ---- 表单 ----
function treeSelectData(excludeCode?: string): OrgNode[] {
const tree = buildTree(orgs.value)
if (!excludeCode) return tree
const prune = (nodes: OrgNode[]): OrgNode[] =>
nodes.filter((n) => n.code !== excludeCode).map((n) => ({ ...n, children: prune(n.children) }))
return prune(tree)
}
function orgFormSchema(excludeCode?: string): JsonSchema {
return {
type: 'object',
properties: {
parentCode: {
type: 'string',
title: '上级组织',
format: 'tree-select',
treeData: treeSelectData(excludeCode),
treeProps: { label: 'name', value: 'code', children: 'children' },
placeholder: '不选则为顶级组织',
},
code: { type: 'string', title: '组织编码', placeholder: '如 rd', minLength: 2 },
name: { type: 'string', title: '组织名称', placeholder: '如 研发部' },
leader: { type: 'string', title: '负责人', placeholder: '请输入负责人' },
sort: { type: 'integer', title: '排序', minimum: 0, default: 0 },
status: {
type: 'string',
title: '状态',
dictCode: 'common_status',
default: 'enabled',
showOn: ['edit'],
},
},
required: ['code', 'name'],
}
}
function buildOrgModel(o?: Org): Record<string, any> {
return {
parentCode: o?.parentCode ?? '',
code: o?.code ?? '',
name: o?.name ?? '',
leader: o?.leader ?? '',
sort: o?.sort ?? 0,
status: o?.status ?? 'enabled',
}
}
async function openCreate(): Promise<void> {
const data = await showDialog({
component: TyForm,
props: { schema: orgFormSchema(), modelValue: buildOrgModel(), mode: 'create' },
title: '新增组织',
width: '480px',
})
if (data == null) return
await createOrg({
code: data.code,
name: data.name,
parentCode: data.parentCode || null,
leader: data.leader,
sort: data.sort ?? 0,
})
ElMessage.success('新增成功')
await refreshOrgs()
}
async function openCreateChild(parent: Record<string, any>): Promise<void> {
const model = buildOrgModel()
model.parentCode = parent.code
const data = await showDialog({
component: TyForm,
props: { schema: orgFormSchema(), modelValue: model, mode: 'create' },
title: `新增子组织(${parent.name})`,
width: '480px',
})
if (data == null) return
await createOrg({
code: data.code,
name: data.name,
parentCode: data.parentCode || parent.code,
leader: data.leader,
sort: data.sort ?? 0,
})
ElMessage.success('新增成功')
await refreshOrgs()
}
async function openEdit(row: Record<string, any>): Promise<void> {
const o = row as Org
const data = await showDialog({
component: TyForm,
props: {
schema: orgFormSchema(o.code),
modelValue: buildOrgModel(o),
mode: 'edit',
readonlyKeys: ['code'],
},
title: '编辑组织',
width: '480px',
})
if (data == null) return
await updateOrg(o.id, {
parentCode: data.parentCode || null,
name: data.name,
leader: data.leader,
sort: data.sort ?? 0,
status: data.status,
})
ElMessage.success('更新成功')
await refreshOrgs()
}
async function handleDelete(row: Record<string, any>): Promise<void> {
const o = row as Org
await ElMessageBox.confirm(`确认删除组织「${o.name}」及其所有下级组织?`, '提示', { type: 'warning' })
const codes = new Set([o.code, ...collectDescendants(orgs.value, o.code)])
const targets = orgs.value.filter((x) => codes.has(x.code))
for (const t of targets) await deleteOrg(t.id)
ElMessage.success('删除成功')
await refreshOrgs()
}
const selectedRows = ref<Record<string, any>[]>([])
function onSelectionChange(rows: Record<string, any>[]): void {
selectedRows.value = rows
}
async function batchDelete(rows: Record<string, any>[]): Promise<void> {
await ElMessageBox.confirm(`确认删除选中的 ${rows.length} 个组织及其下级?`, '提示', { type: 'warning' })
const codes = new Set<string>()
for (const r of rows) {
codes.add(String(r.code))
for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c)
}
const targets = orgs.value.filter((o) => codes.has(o.code))
for (const t of targets) await deleteOrg(t.id)
selectedRows.value = []
ElMessage.success('删除成功')
await refreshOrgs()
}
</script>
<style scoped lang="scss">
.org-page {
@apply flex h-full flex-col;
}
.org-page__search {
@apply mb-md flex-shrink-0;
}
.org-page__table {
@apply flex min-h-0 flex-1 flex-col;
:deep(.el-card__body) {
@apply flex min-h-0 flex-1 flex-col;
}
}
.org-page__toolbar {
@apply mb-md flex flex-shrink-0 items-center justify-between;
}
.org-page__toolbar-left,
.org-page__toolbar-right {
@apply flex items-center gap-2;
}
.org-page__table-body {
@apply min-h-0 flex-1;
}
</style>