更新功能:数据字典和定时任务

This commit is contained in:
2026-02-18 10:43:25 +08:00
parent 6623c656f4
commit 790b3140a7
15 changed files with 2847 additions and 25 deletions

View File

@@ -0,0 +1,239 @@
<template>
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="600px">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<!-- 字典名称 -->
<a-form-item label="字典名称" name="name" required>
<a-input v-model:value="form.name" placeholder="如:用户状态" allow-clear maxlength="50" show-count />
</a-form-item>
<!-- 字典编码 -->
<a-form-item label="字典编码" name="code" required>
<a-input v-model:value="form.code" placeholder="如user_status" allow-clear :disabled="isEdit" />
<div class="form-tip">系统唯一标识只能包含字母数字下划线且必须以字母开头</div>
</a-form-item>
<!-- 排序 -->
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
<div class="form-tip">数值越小越靠前</div>
</a-form-item>
<!-- 状态 -->
<a-form-item label="状态" name="status">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
<!-- 描述 -->
<a-form-item label="描述" name="description">
<a-textarea v-model:value="form.description" placeholder="请输入字典描述" :rows="3" maxlength="200"
show-count />
</a-form-item>
</a-form>
<!-- 底部按钮 -->
<div class="dialog-footer">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
</a-space>
</div>
</a-modal>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import systemApi from '@/api/system'
// ===== Props =====
const props = defineProps({
visible: {
type: Boolean,
default: false
},
record: {
type: Object,
default: null
},
dictionaryList: {
type: Array,
default: () => []
}
})
// ===== Emits =====
const emit = defineEmits(['update:visible', 'success'])
// ===== 状态 =====
const formRef = ref(null)
const isSaving = ref(false)
const isEdit = computed(() => !!props.record?.id)
const title = computed(() => {
return isEdit.value ? '编辑字典类型' : '新增字典类型'
})
// ===== 表单数据 =====
const form = ref({
id: '',
name: '',
code: '',
description: '',
status: true,
sort: 0
})
// ===== 计算属性:状态开关 =====
const statusChecked = computed({
get: () => form.value.status === true,
set: (val) => {
form.value.status = val ? true : false
}
})
// ===== 验证规则 =====
// 编码唯一性验证
const validateCodeUnique = async (rule, value) => {
if (!value) return Promise.resolve()
// 检查编码是否已存在(编辑时排除自己)
const exists = props.dictionaryList.some(
item => item.code === value && item.id !== props.record?.id
)
if (exists) {
return Promise.reject('该编码已存在,请使用其他编码')
}
return Promise.resolve()
}
const rules = {
name: [
{ required: true, message: '请输入字典名称', trigger: 'blur' },
{ min: 2, max: 50, message: '字典名称长度在 2 到 50 个字符', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入字典编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
message: '编码格式不正确,只能包含字母、数字、下划线,且必须以字母开头',
trigger: 'blur'
},
{ validator: validateCodeUnique, trigger: 'blur' }
]
}
// ===== 方法:重置表单 =====
const resetForm = () => {
form.value = {
id: '',
name: '',
code: '',
description: '',
status: true,
sort: 0
}
formRef.value?.clearValidate()
}
// ===== 方法:设置数据(编辑时) =====
const setData = (data) => {
if (data) {
form.value = {
id: data.id || '',
name: data.name || '',
code: data.code || '',
description: data.description || '',
status: data.status !== undefined ? data.status : true,
sort: data.sort !== undefined ? data.sort : 0
}
}
}
// ===== 方法:提交表单 =====
const handleSubmit = async () => {
try {
// 验证表单
await formRef.value.validate()
isSaving.value = true
const submitData = {
name: form.value.name,
code: form.value.code,
description: form.value.description,
status: form.value.status,
sort: form.value.sort
}
let res = {}
if (isEdit.value) {
// 编辑
res = await systemApi.dictionaries.edit.put(form.value.id, submitData)
} else {
// 新增
res = await systemApi.dictionaries.add.post(submitData)
}
if (res.code === 200) {
message.success(isEdit.value ? '编辑成功' : '新增成功')
emit('success')
handleCancel()
} else {
message.error(res.message || '操作失败')
}
} catch (error) {
if (error.errorFields) {
// 表单验证失败
console.log('表单验证失败:', error)
} else {
// API 调用失败
console.error('提交失败:', error)
message.error('操作失败')
}
} finally {
isSaving.value = false
}
}
// ===== 方法:取消 =====
const handleCancel = () => {
resetForm()
emit('update:visible', false)
}
// ===== 监听 visible 变化 =====
watch(() => props.visible, (newVal) => {
if (newVal) {
// 打开弹窗时,如果有 record 则设置数据
if (props.record) {
setData(props.record)
} else {
resetForm()
}
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.form-tip {
font-size: 12px;
color: #8c8c8c;
margin-top: 4px;
line-height: 1.5;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 16px;
}
:deep(.ant-modal-body) {
max-height: 60vh;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="600px">
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<!-- 标签名称 -->
<a-form-item label="标签名称" name="label" required>
<a-input v-model:value="form.label" placeholder="如:正常" allow-clear maxlength="50" show-count />
<div class="form-tip">用于前端显示的文本</div>
</a-form-item>
<!-- 数据值 -->
<a-form-item label="数据值" name="value" required>
<a-input v-model:value="form.value" placeholder="如1" allow-clear />
<div class="form-tip">实际使用的值同一字典内必须唯一</div>
</a-form-item>
<!-- 颜色标记 -->
<a-form-item label="颜色标记" name="color">
<div style="display: flex; align-items: center">
<input v-model="form.color" type="color" style="width: 60px; height: 32px; cursor: pointer" />
<a-input v-model:value="form.color" placeholder="#1890ff" allow-clear
style="flex: 1; margin-left: 10px" />
</div>
<div class="form-tip">用于前端展示的颜色标记</div>
</a-form-item>
<!-- 是否默认 -->
<a-form-item label="默认项" name="is_default">
<a-switch v-model:checked="isDefaultChecked" checked-children="" un-checked-children="" />
<div class="form-tip" v-if="form.is_default" style="color: #faad14">
设置为默认项后同一字典内的其他默认项将自动取消
</div>
</a-form-item>
<!-- 排序 -->
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
</a-form-item>
<!-- 状态 -->
<a-form-item label="状态" name="status">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
<!-- 描述 -->
<a-form-item label="描述" name="description">
<a-textarea v-model:value="form.description" placeholder="请输入描述" :rows="3" maxlength="200"
show-count />
</a-form-item>
</a-form>
<!-- 底部按钮 -->
<div class="dialog-footer">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
</a-space>
</div>
</a-modal>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import systemApi from '@/api/system'
// ===== Props =====
const props = defineProps({
visible: {
type: Boolean,
default: false
},
record: {
type: Object,
default: null
},
dictionaryId: {
type: Number,
default: null
},
itemList: {
type: Array,
default: () => []
}
})
// ===== Emits =====
const emit = defineEmits(['update:visible', 'success'])
// ===== 状态 =====
const formRef = ref(null)
const isSaving = ref(false)
const isEdit = computed(() => !!props.record?.id)
const title = computed(() => {
return isEdit.value ? '编辑字典项' : '新增字典项'
})
// ===== 表单数据 =====
const form = ref({
id: '',
label: '',
value: '',
color: '',
description: '',
is_default: false,
status: true,
sort: 0,
dictionary_id: null
})
// ===== 计算属性:状态开关 =====
const statusChecked = computed({
get: () => form.value.status === true,
set: (val) => {
form.value.status = val ? true : false
}
})
// ===== 计算属性:默认项开关 =====
const isDefaultChecked = computed({
get: () => form.value.is_default === true,
set: (val) => {
form.value.is_default = val ? true : false
}
})
// ===== 验证规则 =====
// 数据值唯一性验证
const validateValueUnique = async (rule, value) => {
if (!value) return Promise.resolve()
// 检查数据值是否已存在(同一字典内,编辑时排除自己)
const exists = props.itemList.some(
item => item.value === value && item.id !== props.record?.id
)
if (exists) {
return Promise.reject('该数据值已存在,请使用其他值')
}
return Promise.resolve()
}
const rules = {
label: [
{ required: true, message: '请输入标签名称', trigger: 'blur' },
{ min: 1, max: 50, message: '标签名称长度在 1 到 50 个字符', trigger: 'blur' }
],
value: [
{ required: true, message: '请输入数据值', trigger: 'blur' },
{ validator: validateValueUnique, trigger: 'blur' }
]
}
// ===== 方法:重置表单 =====
const resetForm = () => {
form.value = {
id: '',
label: '',
value: '',
color: '',
description: '',
is_default: false,
status: true,
sort: 0,
dictionary_id: null
}
formRef.value?.clearValidate()
}
// ===== 方法:设置数据(编辑时) =====
const setData = (data) => {
if (data) {
form.value = {
id: data.id || '',
label: data.label || '',
value: data.value || '',
color: data.color || '',
description: data.description || '',
is_default: data.is_default || false,
status: data.status !== undefined ? data.status : true,
sort: data.sort !== undefined ? data.sort : 0,
dictionary_id: data.dictionary_id || props.dictionaryId
}
}
}
// ===== 方法:提交表单 =====
const handleSubmit = async () => {
try {
// 验证表单
await formRef.value.validate()
isSaving.value = true
const submitData = {
label: form.value.label,
value: form.value.value,
color: form.value.color,
description: form.value.description,
is_default: form.value.is_default,
status: form.value.status,
sort: form.value.sort,
dictionary_id: props.dictionaryId
}
let res = {}
if (isEdit.value) {
// 编辑
res = await systemApi.dictionaryItems.edit.put(form.value.id, submitData)
} else {
// 新增
res = await systemApi.dictionaryItems.add.post(submitData)
}
if (res.code === 200) {
message.success(isEdit.value ? '编辑成功' : '新增成功')
emit('success')
handleCancel()
} else {
message.error(res.message || '操作失败')
}
} catch (error) {
if (error.errorFields) {
// 表单验证失败
console.log('表单验证失败:', error)
} else {
// API 调用失败
console.error('提交失败:', error)
message.error('操作失败')
}
} finally {
isSaving.value = false
}
}
// ===== 方法:取消 =====
const handleCancel = () => {
resetForm()
emit('update:visible', false)
}
// ===== 监听 visible 变化 =====
watch(() => props.visible, (newVal) => {
if (newVal) {
// 打开弹窗时,如果有 record 则设置数据
if (props.record) {
setData(props.record)
} else {
resetForm()
// 新增时设置 dictionary_id
form.value.dictionary_id = props.dictionaryId
}
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.form-tip {
font-size: 12px;
color: #8c8c8c;
margin-top: 4px;
line-height: 1.5;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 16px;
}
:deep(.ant-modal-body) {
max-height: 60vh;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,600 @@
<template>
<div class="pages-sidebar-layout dictionary-page">
<!-- 左侧字典类型列表 -->
<div class="left-box">
<div class="header">
<a-input v-model:value="dictionaryKeyword" placeholder="搜索字典..." allow-clear @change="handleDictionarySearch">
<template #prefix>
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
</template>
</a-input>
<a-button type="primary" size="small" style="margin-top: 12px; width: 100%" @click="handleAddDictionary">
<PlusOutlined /> 新增字典
</a-button>
</div>
<div class="body">
<!-- 字典列表 -->
<div v-if="filteredDictionaries.length > 0" class="dictionary-list">
<div v-for="item in filteredDictionaries" :key="item.id"
:class="['dictionary-item', { 'active': selectedDictionaryId === item.id }]"
@click="handleSelectDictionary(item)">
<div class="item-main">
<div class="item-name">{{ item.name }}</div>
<div class="item-code">{{ item.code }}</div>
</div>
<div class="item-meta">
<a-tag :color="item.status ? 'success' : 'default'" size="small">
{{ item.status ? '启用' : '禁用' }}
</a-tag>
<span class="item-count">{{ item.items_count || 0 }} </span>
</div>
<div class="item-actions" @click.stop>
<a-dropdown>
<a-button type="text" size="small">
<MoreOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleEditDictionary(item)">
<EditOutlined />编辑
</a-menu-item>
<a-menu-item @click="handleDeleteDictionary(item)" danger>
<DeleteOutlined />删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
<!-- 空状态 -->
<a-empty v-else-if="!dictionaryLoading" description="暂无字典类型" :image-size="80">
<a-button type="primary" @click="handleAddDictionary">
创建第一个字典
</a-button>
</a-empty>
</div>
</div>
<!-- 右侧字典项表格 -->
<div class="right-box">
<!-- 工具栏 -->
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input v-model:value="searchForm.label" placeholder="标签名称" allow-clear style="width: 140px" />
<a-input v-model:value="searchForm.value" placeholder="数据值" allow-clear style="width: 140px" />
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 100px">
<a-select-option :value="true">启用</a-select-option>
<a-select-option :value="false">禁用</a-select-option>
</a-select>
<a-button type="primary" @click="handleItemSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button @click="handleItemReset">
<template #icon><RedoOutlined /></template>
重置
</a-button>
</a-space>
</div>
<div class="right-panel">
<a-dropdown :disabled="selectedRows.length === 0">
<a-button :disabled="selectedRows.length === 0">
批量操作
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleBatchStatus">
<CheckCircleOutlined />批量启用/禁用
</a-menu-item>
<a-menu-divider />
<a-menu-item @click="handleBatchDelete" danger>
<DeleteOutlined />批量删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button type="primary" :disabled="!selectedDictionaryId" @click="handleAddItem">
<template #icon><PlusOutlined /></template>
新增
</a-button>
</div>
</div>
<!-- 表格内容 -->
<div class="table-content">
<!-- 空状态未选择字典 -->
<div v-if="!selectedDictionaryId" class="empty-state">
<a-empty description="请选择左侧字典类型后操作" :image-size="120" />
</div>
<!-- 字典项表格 -->
<scTable v-else ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
:pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="refreshTable"
@paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll">
<template #color="{ record }">
<span v-if="record.color" class="color-cell" :style="{ backgroundColor: record.color }"></span>
<span v-else>-</span>
</template>
<template #is_default="{ record }">
<a-tag v-if="record.is_default" color="orange">
<StarOutlined />默认
</a-tag>
<span v-else>-</span>
</template>
<template #status="{ record }">
<a-tag :color="record.status ? 'success' : 'error'">
{{ record.status ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="link" size="small" @click="handleEditItem(record)">
编辑
</a-button>
<a-popconfirm title="确定删除该字典项吗?" @confirm="handleDeleteItem(record)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</scTable>
</div>
</div>
</div>
<!-- 字典类型弹窗 -->
<DictionaryDialog v-if="dialog.dictionary" v-model:visible="dialog.dictionary" :record="currentDictionary"
:dictionary-list="dictionaryList" @success="handleDictionarySuccess" />
<!-- 字典项弹窗 -->
<ItemDialog v-if="dialog.item" v-model:visible="dialog.item" :record="currentItem"
:dictionary-id="selectedDictionaryId" :item-list="tableData" @success="handleItemSuccess" />
</template>
<script setup>
import { ref, reactive, onMounted, h } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
SearchOutlined,
RedoOutlined,
PlusOutlined,
DeleteOutlined,
EditOutlined,
MoreOutlined,
CheckCircleOutlined,
StarOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
import { useTable } from '@/hooks/useTable'
import systemApi from '@/api/system'
import scTable from '@/components/scTable/index.vue'
import DictionaryDialog from './components/DictionaryDialog.vue'
import ItemDialog from './components/ItemDialog.vue'
import dictionaryCache from '@/utils/dictionaryCache'
// ===== 字典列表相关 =====
const dictionaryList = ref([])
const filteredDictionaries = ref([])
const selectedDictionary = ref(null)
const selectedDictionaryId = ref(null)
const dictionaryKeyword = ref('')
const dictionaryLoading = ref(false)
// ===== 字典项相关(使用 useTable Hook=====
const {
tableRef,
searchForm,
tableData,
loading,
pagination,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handlePaginationChange,
handleSelectChange,
handleSelectAll,
refreshTable
} = useTable({
api: systemApi.dictionaryItems.list.get,
searchForm: {
dictionary_id: null,
label: '',
value: '',
status: undefined
},
columns: [],
needPagination: true,
needSelection: true,
immediateLoad: false // 不自动加载,等待选择字典
})
// 表格列配置
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
{ title: '标签名称', dataIndex: 'label', key: 'label', width: 150, ellipsis: true },
{ title: '数据值', dataIndex: 'value', key: 'value', width: 120, ellipsis: true },
{ title: '颜色', dataIndex: 'color', key: 'color', width: 100, align: 'center', slot: 'color' },
{ title: '默认项', dataIndex: 'is_default', key: 'is_default', width: 100, align: 'center', slot: 'is_default' },
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80, align: 'center' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center', slot: 'status' },
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{ title: '操作', dataIndex: 'action', key: 'action', width: 150, align: 'center', fixed: 'right', slot: 'action' }
]
const rowKey = 'id'
// ===== 弹窗状态 =====
const dialog = reactive({
dictionary: false,
item: false
})
// ===== 当前操作的数据 =====
const currentDictionary = ref(null)
const currentItem = ref(null)
// ===== 方法:加载字典列表 =====
const loadDictionaryList = async () => {
try {
dictionaryLoading.value = true
const res = await systemApi.dictionaries.all.get()
if (res.code === 200) {
dictionaryList.value = res.data || []
filteredDictionaries.value = res.data || []
} else {
message.error(res.message || '加载字典列表失败')
}
} catch (error) {
console.error('加载字典列表失败:', error)
message.error('加载字典列表失败')
} finally {
dictionaryLoading.value = false
}
}
// ===== 方法:搜索过滤字典 =====
const handleDictionarySearch = (e) => {
const keyword = e.target?.value || ''
dictionaryKeyword.value = keyword
if (!keyword) {
filteredDictionaries.value = dictionaryList.value
return
}
// 过滤字典列表(支持搜索名称和编码)
filteredDictionaries.value = dictionaryList.value.filter(dict => {
return dict.name.toLowerCase().includes(keyword.toLowerCase()) ||
dict.code.toLowerCase().includes(keyword.toLowerCase())
})
}
// ===== 方法:选择字典 =====
const handleSelectDictionary = (dictionary) => {
selectedDictionary.value = dictionary
selectedDictionaryId.value = dictionary.id
// 重置右侧搜索条件
searchForm.label = ''
searchForm.value = ''
searchForm.status = undefined
// 更新 dictionary_id
searchForm.dictionary_id = dictionary.id
// 加载字典项列表
handleItemSearch()
}
// ===== 方法:字典项搜索 =====
const handleItemSearch = () => {
if (!selectedDictionaryId.value) {
message.warning('请先选择字典类型')
return
}
searchForm.dictionary_id = selectedDictionaryId.value
handleSearch()
}
// ===== 方法:字典项重置 =====
const handleItemReset = () => {
searchForm.label = ''
searchForm.value = ''
searchForm.status = undefined
searchForm.dictionary_id = selectedDictionaryId.value
handleSearch()
}
// ===== 方法:新增字典 =====
const handleAddDictionary = () => {
currentDictionary.value = null
dialog.dictionary = true
}
// ===== 方法:编辑字典 =====
const handleEditDictionary = (dictionary) => {
currentDictionary.value = { ...dictionary }
dialog.dictionary = true
}
// ===== 方法:删除字典 =====
const handleDeleteDictionary = (dictionary) => {
const itemCount = dictionary.items_count || 0
Modal.confirm({
title: '确认删除',
content: itemCount > 0
? `确定删除字典类型"${dictionary.name}"吗?删除后该字典下的 ${itemCount} 个字典项也会被删除!`
: `确定删除字典类型"${dictionary.name}"吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
icon: h(ExclamationCircleOutlined),
onOk: async () => {
try {
const res = await systemApi.dictionaries.delete.delete(dictionary.id)
if (res.code === 200) {
message.success('删除成功')
// 如果删除的是当前选中的字典,清空右侧
if (selectedDictionaryId.value === dictionary.id) {
selectedDictionary.value = null
selectedDictionaryId.value = null
tableData.value = []
}
// 刷新字典列表
await loadDictionaryList()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除字典失败:', error)
message.error('删除失败')
}
}
})
}
// ===== 方法:新增字典项 =====
const handleAddItem = () => {
if (!selectedDictionaryId.value) {
message.warning('请先选择字典类型')
return
}
currentItem.value = null
dialog.item = true
}
// ===== 方法:编辑字典项 =====
const handleEditItem = (record) => {
currentItem.value = { ...record }
dialog.item = true
}
// ===== 方法:删除字典项 =====
const handleDeleteItem = async (record) => {
try {
const res = await systemApi.dictionaryItems.delete.delete(record.id)
if (res.code === 200) {
message.success('删除成功')
refreshTable()
// 刷新字典列表以更新项数量
loadDictionaryList()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除字典项失败:', error)
message.error('删除失败')
}
}
// ===== 方法:批量删除字典项 =====
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
message.warning('请选择要删除的字典项')
return
}
Modal.confirm({
title: '确认删除',
content: `确定删除选中的 ${selectedRows.value.length} 个字典项吗?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
icon: h(ExclamationCircleOutlined),
onOk: async () => {
try {
const ids = selectedRows.value.map(item => item.id)
const res = await systemApi.dictionaryItems.batchDelete.post({ ids })
if (res.code === 200) {
message.success('删除成功')
selectedRows.value = []
refreshTable()
loadDictionaryList()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('批量删除失败:', error)
message.error('删除失败')
}
}
})
}
// ===== 方法:批量启用/禁用字典项 =====
const handleBatchStatus = () => {
if (selectedRows.value.length === 0) {
message.warning('请选择要操作的字典项')
return
}
const newStatus = selectedRows.value[0].status ? false : true
const statusText = newStatus === 1 ? '启用' : '禁用'
Modal.confirm({
title: `确认${statusText}`,
content: `确定要${statusText}选中的 ${selectedRows.value.length} 个字典项吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map(item => item.id)
const res = await systemApi.dictionaryItems.batchStatus.post({
ids,
status: newStatus
})
if (res.code === 200) {
message.success(`${statusText}成功`)
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || '操作失败')
}
} catch (error) {
console.error('批量操作失败:', error)
message.error('操作失败')
}
}
})
}
// ===== 方法:字典操作成功回调 =====
const handleDictionarySuccess = () => {
dialog.dictionary = false
// 清理字典缓存
dictionaryCache.clearDictionary()
loadDictionaryList()
}
// ===== 方法:字典项操作成功回调 =====
const handleItemSuccess = () => {
dialog.item = false
// 清理字典缓存
dictionaryCache.clearDictionary(selectedDictionaryId.value)
refreshTable()
loadDictionaryList()
}
// ===== 生命周期 =====
onMounted(() => {
// 加载字典列表
loadDictionaryList()
})
</script>
<style scoped lang="scss">
.dictionary-page {
// 左侧字典列表样式
.left-box {
.body {
padding: 16px;
position: relative;
.dictionary-list {
.dictionary-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #f0f0f0;
background: #fff;
&:hover {
background-color: #fafafa;
border-color: #d9d9d9;
.item-actions {
opacity: 1;
}
}
&.active {
background-color: #e6f7ff;
border-color: #1890ff;
}
.item-main {
flex: 1;
min-width: 0;
.item-name {
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-code {
font-size: 12px;
color: #8c8c8c;
font-family: 'Consolas', 'Monaco', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.item-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #8c8c8c;
margin-right: 8px;
.item-count {
font-size: 12px;
}
}
.item-actions {
opacity: 0;
transition: opacity 0.2s;
}
}
}
}
}
// 右侧空状态
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #fff;
border-radius: 4px;
}
}
// 表格自定义列样式
:deep(.sc-table) {
.color-cell {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 2px;
border: 1px solid #d9d9d9;
vertical-align: middle;
}
}
</style>