- 新增 dict store 与 useDict Hook(登录后缓存、登出清空) - ty-form/ty-table 支持 dictCode 自动解析下拉与标签 - 用户/角色/资源/组织/租户/审计/登录日志页去硬编码映射 - 字典管理页内置标记、value 保护、标签色编辑 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
279 lines
8.4 KiB
Vue
279 lines
8.4 KiB
Vue
<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>
|