Files
laravel_swoole/resources/admin/src/pages/system/dictionaries/index.vue

601 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>