239 lines
6.7 KiB
Vue
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>
|