Files
shanxia2.0/web-admin/src/views/system/dict/index.vue
T
2026-06-21 12:41:39 +08:00

834 lines
26 KiB
Vue

<!-- 字典管理 -->
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2>字典类型</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="字典名称/类型编码" clearable />
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按字典名称、类型编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@click="handleBatchDelete"
>
批量删除
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="dictName"
label="字典名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="dictType"
label="字典类型编码"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="130"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<div style="display: flex; justify-content: center; gap: 8px">
<el-button class="btn-op-item primary" @click="handleDictData(row)">
字典数据配置
</el-button>
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
<el-button class="btn-op-item">
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item command="edit">
<el-icon><Edit /></el-icon>编辑
</el-dropdown-item>
<el-dropdown-item command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="dict-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="字典名称" prop="dictName">
<el-input v-model="formData.dictName" placeholder="字典名称" />
</el-form-item>
<el-form-item label="字典类型编码" prop="dictType">
<el-input v-model="formData.dictType" placeholder="字典类型编码" :disabled="isEdit" />
</el-form-item>
<el-form-item label="备注" style="grid-column: 1 / -1">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<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="序号"
min-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="tagType"
label="标签颜色"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.tagType"
:type="row.tagType || undefined"
size="small"
>
{{ row.tagType === 'success' ? '成功' : row.tagType === 'warning' ? '警告' : row.tagType === 'danger' ? '危险' : row.tagType === 'info' ? '信息' : row.tagType === 'primary' ? '主要' : '默认' }}
</el-tag>
<span v-else class="text-gray-400">默认</span>
</template>
</el-table-column>
<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 dict-data-config-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div class="dict-data-dialog-head">
<span class="muted">字典类型编码:{{ currentDictType.dictType }}</span>
</div>
<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"
>
<el-table-column
type="index"
label="序号"
min-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="tagType"
label="标签颜色"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.tagType"
:type="row.tagType || undefined"
size="small"
>
{{ row.tagType === 'success' ? '成功' : row.tagType === 'warning' ? '警告' : row.tagType === 'danger' ? '危险' : row.tagType === 'info' ? '信息' : row.tagType === 'primary' ? '主要' : '默认' }}
</el-tag>
<span v-else class="text-gray-400">默认</span>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="150"
show-overflow-tooltip
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-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
v-model="dictDataFormVisible"
:title="dictDataFormTitle"
width="480px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
append-to-body
>
<el-form
ref="dictDataFormRef"
:model="dictDataFormData"
:rules="dictDataFormRules"
label-position="top"
class="dict-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="数据标签" prop="dictLabel">
<el-input v-model="dictDataFormData.dictLabel" placeholder="数据标签" />
</el-form-item>
<el-form-item label="数据值" prop="dictValue">
<el-input v-model="dictDataFormData.dictValue" placeholder="数据值" />
</el-form-item>
<el-form-item label="标签颜色" prop="tagType">
<el-select v-model="dictDataFormData.tagType" placeholder="请选择" clearable>
<el-option label="默认" value="">
<el-tag size="small">预览</el-tag>
</el-option>
<el-option label="成功(绿)" value="success">
<el-tag type="success" size="small">成功</el-tag>
</el-option>
<el-option label="警告(橙)" value="warning">
<el-tag type="warning" size="small">警告</el-tag>
</el-option>
<el-option label="危险(红)" value="danger">
<el-tag type="danger" size="small">危险</el-tag>
</el-option>
<el-option label="信息(灰)" value="info">
<el-tag type="info" size="small">信息</el-tag>
</el-option>
<el-option label="主要(蓝)" value="primary">
<el-tag type="primary" size="small">主要</el-tag>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" style="grid-column: 1 / -1">
<el-input
v-model="dictDataFormData.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dictDataFormVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleSubmitDictData"
:loading="dictDataSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
More,
Collection,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchDictTypeList,
fetchCreateDictType,
fetchUpdateDictType,
fetchDeleteDictType,
fetchDictDataList,
fetchCreateDictData,
fetchUpdateDictData,
fetchDeleteDictData
} from '@/api/admin'
defineOptions({ name: 'SystemDict' })
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.Dict.DictTypeItem[]>([])
const selectedRows = ref<Api.Dict.DictTypeItem[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('新增字典类型')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
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: '',
dictType: '',
orderNum: 0,
remark: ''
})
const formRules = {
dictName: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.Dict.DictTypeItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchDictTypeList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
pagination.page = 1
loadData()
}
function resetForm() {
formData.dictName = ''
formData.dictType = ''
formData.orderNum = 0
formData.remark = ''
editingId.value = null
}
const viewSections = computed(() => [
{
title: '基本信息',
fieldLayout: 'table' as const,
tableMode: 'triple' as const,
fields: [
{ label: '字典名称', value: viewRow.value?.dictName, long: false },
{ label: '字典类型编码', value: viewRow.value?.dictType, long: false },
{ label: '创建时间', value: viewRow.value?.createTime, long: false },
{ label: '备注', value: viewRow.value?.remark, long: false }
]
}
])
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,
pageSize: 100,
dictType: row.dictType
})
viewDictDataList.value = res.list || []
} catch (e) {
console.error('加载字典数据失败:', e)
} finally {
viewDictDataLoading.value = false
}
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增字典类型'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: Api.Dict.DictTypeItem) {
isEdit.value = true
dialogTitle.value = '编辑字典类型'
editingId.value = row.id
formData.dictName = row.dictName
formData.dictType = row.dictType
formData.orderNum = row.orderNum ?? 0
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
dictName: formData.dictName.trim(),
dictType: formData.dictType.trim(),
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateDictType(editingId.value, payload)
} else {
await fetchCreateDictType(payload)
}
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
function handleDropdownCommand(command: string, row: Api.Dict.DictTypeItem) {
switch (command) {
case 'view':
handleView(row)
break
case 'edit':
handleEdit(row)
break
case 'delete':
handleDelete(row)
break
}
}
async function handleDelete(row: Api.Dict.DictTypeItem) {
await ElMessageBox.confirm(`确定删除字典类型「${row.dictName}」吗?`, '提示', {
type: 'warning'
})
await fetchDeleteDictType(row.id)
await loadData()
}
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) {
ElMessage.warning('请选择要删除的字典类型')
return
}
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个字典类型吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteDictType(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
const dictDataDialogVisible = ref(false)
const dictDataTableData = ref<Api.Dict.DictDataItem[]>([])
const dictDataLoading = ref(false)
const dictDataPagination = reactive({ page: 1, pageSize: 10, total: 0 })
const currentDictType = reactive({ dictName: '', dictType: '' })
const dictDataFormVisible = ref(false)
const dictDataFormTitle = ref('新增字典数据')
const dictDataIsEdit = ref(false)
const dictDataSubmitLoading = ref(false)
const dictDataFormRef = ref()
const dictDataEditingId = ref<string | null>(null)
const dictDataFormData = reactive({
dictLabel: '',
dictValue: '',
tagType: '',
orderNum: 0,
remark: ''
})
const dictDataFormRules = {
dictLabel: [{ required: true, message: '请输入数据标签', trigger: 'blur' }],
dictValue: [{ required: true, message: '请输入数据值', trigger: 'blur' }]
}
async function loadDictData() {
dictDataLoading.value = true
try {
const res = await fetchDictDataList({
page: dictDataPagination.page,
pageSize: dictDataPagination.pageSize,
dictType: currentDictType.dictType || undefined
})
dictDataTableData.value = res.list || []
dictDataPagination.total = res.total ?? 0
} finally {
dictDataLoading.value = false
}
}
function handleDictData(row: Api.Dict.DictTypeItem) {
currentDictType.dictName = row.dictName
currentDictType.dictType = row.dictType
dictDataPagination.page = 1
dictDataDialogVisible.value = true
loadDictData()
}
function resetDictDataForm() {
dictDataFormData.dictLabel = ''
dictDataFormData.dictValue = ''
dictDataFormData.tagType = ''
dictDataFormData.orderNum = 0
dictDataFormData.remark = ''
dictDataEditingId.value = null
}
function handleAddDictData() {
dictDataIsEdit.value = false
dictDataFormTitle.value = '新增字典数据'
resetDictDataForm()
dictDataFormVisible.value = true
dictDataFormRef.value?.clearValidate?.()
}
function handleEditDictData(row: Api.Dict.DictDataItem) {
dictDataIsEdit.value = true
dictDataFormTitle.value = '编辑字典数据'
dictDataEditingId.value = row.id
dictDataFormData.dictLabel = row.dictLabel
dictDataFormData.dictValue = row.dictValue
dictDataFormData.tagType = row.tagType || ''
dictDataFormData.orderNum = row.orderNum ?? 0
dictDataFormData.remark = row.remark || ''
dictDataFormVisible.value = true
dictDataFormRef.value?.clearValidate?.()
}
async function handleSubmitDictData() {
await dictDataFormRef.value?.validate?.()
dictDataSubmitLoading.value = true
try {
const payload = {
dictLabel: dictDataFormData.dictLabel.trim(),
dictValue: dictDataFormData.dictValue.trim(),
tagType: dictDataFormData.tagType || undefined,
dictType: currentDictType.dictType,
orderNum: dictDataFormData.orderNum,
remark: dictDataFormData.remark?.trim() || undefined
}
if (dictDataIsEdit.value && dictDataEditingId.value) {
await fetchUpdateDictData(dictDataEditingId.value, payload)
} else {
await fetchCreateDictData(payload)
}
dictDataFormVisible.value = false
await loadDictData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
dictDataSubmitLoading.value = false
}
}
async function handleDeleteDictData(row: Api.Dict.DictDataItem) {
await ElMessageBox.confirm(`确定删除字典数据「${row.dictLabel}」吗?`, '提示', {
type: 'warning'
})
await fetchDeleteDictData(row.id)
await loadDictData()
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.dict-dialog-form {
: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>