Files
Ticket/web/src/features/system/orgs/index.vue
T
2026-04-30 10:47:26 +08:00

239 lines
6.7 KiB
Vue

<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<span class="text-sm font-semibold text-slate-700">组织架构</span>
<n-button type="primary" @click="openCreate()">新增组织</n-button>
</div>
<div class="card-body card-body-fill tree-card-body">
<n-tree
block-line
expand-on-click
:data="treeData"
key-field="id"
label-field="name"
children-field="children"
:render-switcher-icon="renderSwitcherIcon"
:render-label="renderLabel"
:expanded-keys="expandedKeys"
@update:expanded-keys="onExpanded"
/>
</div>
</section>
<n-modal
v-model:show="modal.visible"
preset="card"
:title="modal.mode === 'create' ? '新增组织' : '编辑组织'"
class="w-[520px]"
>
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="90">
<n-form-item label="上级组织">
<n-tree-select
v-model:value="form.parentId"
clearable
:options="treeSelectOptions"
placeholder="可选"
key-field="id"
label-field="name"
children-field="children"
/>
</n-form-item>
<n-form-item label="组织名称" path="name"
><n-input v-model:value="form.name"
/></n-form-item>
<n-form-item label="组织编码" path="code"
><n-input v-model:value="form.code" :disabled="modal.mode === 'edit'"
/></n-form-item>
<n-form-item label="排序"
><n-input-number v-model:value="form.sort" :min="0"
/></n-form-item>
<n-form-item label="状态">
<n-radio-group v-model:value="form.status">
<n-radio-button value="ENABLED">启用</n-radio-button>
<n-radio-button value="DISABLED">禁用</n-radio-button>
</n-radio-group>
</n-form-item>
</n-form>
<template #footer>
<div class="flex justify-end gap-2">
<n-button @click="modal.visible = false">取消</n-button>
<n-button type="primary" :loading="saveMutation.isPending.value" @click="save"
>保存</n-button
>
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { computed, h, reactive, ref } from 'vue'
import type { FormInst, FormRules, TreeOption } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag, useMessage } from 'naive-ui'
import { ChevronRight } from 'lucide-vue-next'
import { useMutation, useQuery } from '@tanstack/vue-query'
import { createOrgApi, deleteOrgApi, listOrgsApi, updateOrgApi } from '@/api/system/org'
import type { OrgTreeNode } from '@/types/system/org'
import { statusLabel, statusTagType } from '@/utils/display'
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const expandedKeys = ref<string[]>([])
const modal = reactive({ visible: false, mode: 'create' as 'create' | 'edit', id: '' })
const form = reactive({
parentId: null as string | null,
name: '',
code: '',
sort: 0,
status: 'ENABLED'
})
const rules: FormRules = {
name: [{ required: true, message: '请输入组织名称', trigger: ['blur', 'input'] }],
code: [{ required: true, message: '请输入组织编码', trigger: ['blur', 'input'] }]
}
const orgQuery = useQuery({
queryKey: ['system', 'orgs'],
queryFn: listOrgsApi
})
const treeData = computed(() => orgQuery.data.value ?? [])
const treeSelectOptions = computed(() => treeData.value as unknown as TreeOption[])
const saveMutation = useMutation({
mutationFn: async () => {
const payload = {
parentId: form.parentId ?? undefined,
name: form.name.trim(),
code: form.code.trim(),
sort: form.sort,
status: form.status
}
if (modal.mode === 'create') return createOrgApi(payload)
return updateOrgApi(modal.id, payload)
},
onSuccess: async () => {
message.success('保存成功')
modal.visible = false
await orgQuery.refetch()
expandedKeys.value = collectAllKeys(treeData.value)
}
})
const deleteMutation = useMutation({
mutationFn: deleteOrgApi,
onSuccess: async () => {
message.success('删除成功')
await orgQuery.refetch()
}
})
function openCreate(parentId?: string) {
modal.mode = 'create'
modal.id = ''
form.parentId = parentId ?? null
form.name = ''
form.code = ''
form.sort = 0
form.status = 'ENABLED'
modal.visible = true
}
function openEdit(node: OrgTreeNode) {
modal.mode = 'edit'
modal.id = node.id
form.parentId = node.parentId ?? null
form.name = node.name
form.code = node.code
form.sort = node.sort
form.status = node.status
modal.visible = true
}
async function save() {
await formRef.value?.validate()
await saveMutation.mutateAsync()
}
function renderLabel(payload: { option: unknown }) {
const node = payload.option as OrgTreeNode
return h('div', { class: 'flex w-full items-center justify-between py-2' }, [
h('div', { class: 'flex items-center gap-2' }, [
h('span', node.name),
h(
NTag,
{ size: 'small', type: statusTagType(node.status) },
{ default: () => statusLabel(node.status) }
)
]),
h(NSpace, { size: 6 }, () => [
h(
NButton,
{ size: 'small', tertiary: true, onClick: () => openCreate(node.id) },
{ default: () => '新增子级' }
),
h(
NButton,
{ size: 'small', tertiary: true, type: 'primary', onClick: () => openEdit(node) },
{ default: () => '编辑' }
),
h(
NPopconfirm,
{ onPositiveClick: () => deleteMutation.mutate(node.id) },
{
trigger: () =>
h(NButton, { size: 'small', tertiary: true, type: 'error' }, { default: () => '删除' }),
default: () => '确认删除该组织吗?'
}
)
])
])
}
function onExpanded(keys: string[]) {
expandedKeys.value = keys
}
function collectAllKeys(nodes: OrgTreeNode[]): string[] {
return nodes.flatMap((node) => [node.id, ...collectAllKeys(node.children ?? [])])
}
function renderSwitcherIcon() {
return h(ChevronRight, { size: 16, strokeWidth: 2.25, class: 'tree-switcher-icon' })
}
</script>
<style scoped>
.tree-card-body {
padding: 8px 10px 12px;
}
:deep(.n-tree-node-switcher) {
width: 28px;
height: 42px;
margin-top: 0;
display: flex;
align-items: center;
justify-content: center;
align-self: stretch;
}
:deep(.tree-switcher-icon) {
display: block;
color: #0f8f86;
}
:deep(.n-tree-node-content) {
min-height: 42px;
align-items: center;
border-radius: 14px;
}
:deep(.n-tree-node-content__text) {
flex: 1;
min-width: 0;
padding: 0;
}
</style>