This commit is contained in:
zlwu committed 2026-05-20 14:53:16 +08:00
1 parent dc31968204
commit 27ac28b8f5
58 files changed
+5960 -4960

No files matched your search

+111 -14
View File
@@ -144,6 +144,24 @@
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="上级组织" style="grid-column: 1 / -1">
<el-tree-select
v-model="parentSelectId"
:data="deptTreeSelectData"
node-key="value"
:props="{ label: 'label', children: 'children' }"
check-strictly
filterable
clearable
:disabled="isEdit"
placeholder="请选择上级组织(留空为顶级)"
class="w-full"
>
<template #label="{ value }">
{{ formatDeptPathLabel(value) }}
</template>
</el-tree-select>
</el-form-item>
<el-form-item label="组织名称" prop="deptName" style="grid-column: 1 / -1">
<el-input v-model="formData.deptName" placeholder="请输入组织名称" />
</el-form-item>
@@ -235,11 +253,15 @@
fetchDictDataOptions,
fetchUserList
} from '@/api/admin'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
defineOptions({ name: 'SystemDept' })
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.Dept.DeptItem[]>([])
/** 弹窗上级组织:全量树(不受列表关键词筛选影响) */
const deptTreeForSelect = ref<Api.Dept.DeptItem[]>([])
const deptFlatList = ref<Api.Dept.DeptItem[]>([])
const selectedRows = ref<Api.Dept.DeptItem[]>([])
const loading = ref(false)
const isExpandAll = ref(true)
@@ -273,6 +295,56 @@
const deptCategoryOptions = ref<Api.Dict.DictDataOption[]>([])
const statusOptions = ref<Api.Dict.DictDataOption[]>([])
type DeptTreeSelectNode = { value: string; label: string; children?: DeptTreeSelectNode[] }
function flattenDeptTree(nodes: Api.Dept.DeptItem[]): Api.Dept.DeptItem[] {
const out: Api.Dept.DeptItem[] = []
const walk = (list: Api.Dept.DeptItem[]) => {
for (const n of list) {
out.push({ ...n, children: undefined })
if (n.children?.length) walk(n.children)
}
}
walk(nodes || [])
return out
}
function buildDeptTreeSelectData(nodes: Api.Dept.DeptItem[]): DeptTreeSelectNode[] {
return (nodes || []).map((n) => ({
value: n.id,
label: n.deptName || '',
children: n.children?.length ? buildDeptTreeSelectData(n.children) : undefined
}))
}
const deptTreeSelectData = computed(() => buildDeptTreeSelectData(deptTreeForSelect.value))
const parentSelectId = computed({
get() {
const pid = (formData.parentId || '').trim()
return pid && pid !== '0' ? pid : undefined
},
set(val: string | undefined) {
formData.parentId = val ? val : '0'
}
})
function formatDeptPathLabel(nodeId: string | null | undefined) {
const id = String(nodeId ?? '').trim()
if (!id) return ''
return (
buildTreePathLabelFromFlat(id, deptFlatList.value, { nameKey: 'deptName' }) ||
deptFlatList.value.find((d) => d.id === id)?.deptName ||
''
)
}
function getParentDeptDisplay(parentId: string | null | undefined) {
const pid = (parentId || '').trim()
if (!pid || pid === '0') return '无(顶级组织)'
return formatDeptPathLabel(pid) || pid
}
/** 负责人:关联 sys_user,提交仍写 leader / leaderPhone 字符串 */
const leaderUserId = ref<string | null>(null)
const leaderUserOptions = ref<Api.User.UserItem[]>([])
@@ -398,6 +470,7 @@
title: '基本信息',
fields: [
{ label: '组织名称', value: viewRow.value?.deptName },
{ label: '上级组织', value: getParentDeptDisplay(viewRow.value?.parentId) },
{ label: '组织类别', value: getDeptCategoryLabel(viewRow.value?.deptCategoryCode || '') },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用', tagType: viewRow.value?.status === '1' ? 'success' : 'info' },
{ label: '负责人', value: viewRow.value?.leader },
@@ -440,6 +513,18 @@
else postorder(tableData.value)
}
async function loadDeptTreeForSelect() {
try {
const res = await fetchDeptTree()
deptTreeForSelect.value = res || []
deptFlatList.value = flattenDeptTree(deptTreeForSelect.value)
} catch (error) {
console.error('加载上级组织选项失败:', error)
deptTreeForSelect.value = []
deptFlatList.value = []
}
}
async function loadData() {
loading.value = true
try {
@@ -528,24 +613,35 @@
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
parentId: formData.parentId || undefined,
deptName: formData.deptName.trim(),
deptCategoryCode: formData.deptCategoryCode || undefined,
status: formData.status,
leader: formData.leader?.trim() || undefined,
leaderPhone: formData.leaderPhone?.trim() || undefined,
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
const leader = (formData.leader ?? '').trim()
const leaderPhone = (formData.leaderPhone ?? '').trim()
const remark = (formData.remark ?? '').trim()
if (isEdit.value && editingId.value) {
await fetchUpdateDept(editingId.value, payload)
// 编辑时须显式传空字符串,否则 undefined 不会进入请求体,后端无法清空字段
await fetchUpdateDept(editingId.value, {
deptName: formData.deptName.trim(),
deptCategoryCode: formData.deptCategoryCode || '',
status: formData.status,
leader,
leaderPhone,
orderNum: formData.orderNum,
remark
})
} else {
await fetchCreateDept(payload)
await fetchCreateDept({
parentId: formData.parentId || undefined,
deptName: formData.deptName.trim(),
deptCategoryCode: formData.deptCategoryCode || undefined,
status: formData.status,
leader: leader || undefined,
leaderPhone: leaderPhone || undefined,
orderNum: formData.orderNum,
remark: remark || undefined
})
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
await Promise.all([loadData(), loadDeptTreeForSelect()])
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
@@ -563,7 +659,7 @@
await ElMessageBox.confirm(confirmText, '提示', { type: 'warning' })
await fetchDeleteDept(row.id)
ElMessage.success('删除成功')
await loadData()
await Promise.all([loadData(), loadDeptTreeForSelect()])
} catch (error: any) {
if (error !== 'cancel') console.error('删除失败:', error)
}
@@ -576,6 +672,7 @@
onMounted(() => {
loadData()
loadDeptTreeForSelect()
loadDictOptions()
})
</script>
+187 -97
View File
@@ -204,95 +204,159 @@
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.dictName || '字典详情'"
width="900px"
:icon="Collection"
:meta="`字典类型编码:${viewRow?.dictType || ''}${viewRow?.createTime ? ' · ' + viewRow.createTime : ''}`"
:sections="viewSections"
/>
>
<template #append>
<div class="view-section dict-view-data-section">
<h3>字典数据</h3>
<div class="list-body list-wrap dict-view-table-wrap">
<el-table
class="list-table"
:data="viewDictDataList"
v-loading="viewDictDataLoading"
stripe
row-key="id"
empty-text="暂无字典数据"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="dictLabel"
label="数据标签"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="dictValue"
label="数据值"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="150"
show-overflow-tooltip
align="center"
header-align="center"
/>
</el-table>
</div>
</div>
</template>
</SystemViewDialog>
</section>
<el-dialog
v-model="dictDataDialogVisible"
:title="`${currentDictType.dictName} - 字典数据配置`"
width="900px"
class="dialog-form-layout"
class="dialog-form-layout dict-data-config-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div style="margin-bottom: 12px">
<el-button class="btn-main" type="primary" @click="handleAddDictData">
<el-icon><Plus /></el-icon>
新增数据
</el-button>
<div class="dict-data-dialog-head">
<span class="muted">字典类型编码:{{ currentDictType.dictType }}</span>
</div>
<el-table
:data="dictDataTableData"
v-loading="dictDataLoading"
stripe
row-key="id"
>
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column
prop="dictLabel"
label="数据标签"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="dictValue"
label="数据值"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="120"
show-overflow-tooltip
align="center"
header-align="center"
/>
<el-table-column
label="操作"
width="160"
fixed="right"
align="center"
header-align="center"
<div class="list-toolbar dict-data-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="handleAddDictData">
<el-icon><Plus /></el-icon>
新增数据
</el-button>
</div>
</div>
<div class="list-body list-wrap dict-data-table-wrap">
<el-table
class="list-table"
:data="dictDataTableData"
v-loading="dictDataLoading"
stripe
row-key="id"
>
<template #default="{ row }">
<el-button
class="btn-op-item primary"
@click="handleEditDictData(row)"
>
编辑
</el-button>
<el-button class="btn-op-item" @click="handleDeleteDictData(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar" style="margin-top: 12px">
<el-pagination
:current-page="dictDataPagination.page"
:page-size="dictDataPagination.pageSize"
:total="dictDataPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => (
(dictDataPagination.page = 1),
(dictDataPagination.pageSize = val),
loadDictData()
)
"
@current-change="
(val) => ((dictDataPagination.page = val), loadDictData())
"
/>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="
(index) =>
(dictDataPagination.page - 1) * dictDataPagination.pageSize + index + 1
"
/>
<el-table-column
prop="dictLabel"
label="数据标签"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="dictValue"
label="数据值"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="150"
show-overflow-tooltip
align="center"
header-align="center"
/>
<el-table-column
label="操作"
width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button
class="btn-op-item primary"
@click="handleEditDictData(row)"
>
编辑
</el-button>
<el-button class="btn-op-item" @click="handleDeleteDictData(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="dictDataPagination.page"
:page-size="dictDataPagination.pageSize"
:total="dictDataPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => (
(dictDataPagination.page = 1),
(dictDataPagination.pageSize = val),
loadDictData()
)
"
@current-change="
(val) => ((dictDataPagination.page = val), loadDictData())
"
/>
</div>
</div>
<el-dialog
@@ -385,6 +449,7 @@
const viewVisible = ref(false)
const viewRow = ref<Api.Dict.DictTypeItem | null>(null)
const viewDictDataList = ref<Api.Dict.DictDataItem[]>([])
const viewDictDataLoading = ref(false)
const formData = reactive({
dictName: '',
@@ -436,35 +501,23 @@
editingId.value = null
}
const viewSections = computed(() => {
const sections: any[] = [
{
title: '基本信息',
fields: [
{ label: '字典名称', value: viewRow.value?.dictName },
{ label: '字典类型编码', value: viewRow.value?.dictType },
{ label: '备注', value: viewRow.value?.remark, span: true },
{ label: '创建时间', value: viewRow.value?.createTime }
]
}
]
if (viewDictDataList.value.length > 0) {
sections.push({
title: '字典数据',
fields: viewDictDataList.value.map((item) => ({
label: item.dictLabel || '',
value: item.dictValue || '',
span: false
}))
})
const viewSections = computed(() => [
{
title: '基本信息',
fields: [
{ label: '字典名称', value: viewRow.value?.dictName },
{ label: '字典类型编码', value: viewRow.value?.dictType },
{ label: '备注', value: viewRow.value?.remark, span: true },
{ label: '创建时间', value: viewRow.value?.createTime }
]
}
return sections
})
])
async function handleView(row: Api.Dict.DictTypeItem) {
viewRow.value = { ...row }
viewDictDataList.value = []
viewVisible.value = true
viewDictDataLoading.value = true
try {
const res = await fetchDictDataList({
page: 1,
@@ -474,6 +527,8 @@
viewDictDataList.value = res.list || []
} catch (e) {
console.error('加载字典数据失败:', e)
} finally {
viewDictDataLoading.value = false
}
}
@@ -668,6 +723,41 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.dict-view-data-section {
padding: 18px 22px;
border-bottom: none;
h3 {
margin: 0 0 14px;
padding-left: 10px;
font-size: 14px;
font-weight: 600;
color: #1e293b;
border-left: 3px solid #10b981;
line-height: 1;
}
}
.dict-view-table-wrap {
padding-top: 0;
}
.dict-data-dialog-head {
margin-bottom: 12px;
.muted {
font-size: 13px;
color: var(--mint-text-muted);
}
}
.dict-data-toolbar {
margin-bottom: 0;
}
.dict-data-table-wrap {
padding-top: 0;
}
</style>