更新功能:数据字典和定时任务
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user