499 lines
14 KiB
Vue
499 lines
14 KiB
Vue
<template>
|
||
<div class="dict-manage">
|
||
<div class="dict-types-panel">
|
||
<a-card title="字典类型" class="full-height-card" :bordered="false">
|
||
<template #extra>
|
||
<a-button type="primary" size="small" @click="openTypeModal()">
|
||
<PlusOutlined /> 新增
|
||
</a-button>
|
||
</template>
|
||
|
||
<div class="search-box">
|
||
<a-input-search
|
||
v-model:value="typeSearchKeyword"
|
||
placeholder="搜索类型名称/编码"
|
||
size="small"
|
||
@search="handleTypeSearch"
|
||
/>
|
||
</div>
|
||
|
||
<a-list
|
||
:data-source="filteredTypes"
|
||
:loading="loadingTypes"
|
||
item-layout="horizontal"
|
||
size="small"
|
||
class="type-list"
|
||
>
|
||
<template #renderItem="{ item }">
|
||
<a-list-item
|
||
class="type-item"
|
||
:class="{ active: currentType?.id === item.id }"
|
||
@click="handleSelectType(item)"
|
||
>
|
||
<a-list-item-meta>
|
||
<template #title>
|
||
<span class="type-name">{{ item.name }}</span>
|
||
</template>
|
||
<template #description>
|
||
<div class="type-code">
|
||
<span>{{ item.code }}</span>
|
||
<a-tag v-if="!item.status" color="error" size="small">停用</a-tag>
|
||
</div>
|
||
</template>
|
||
</a-list-item-meta>
|
||
<template #actions>
|
||
<a-dropdown>
|
||
<a-button type="text" size="small" @click.stop>
|
||
<MoreOutlined />
|
||
</a-button>
|
||
<template #overlay>
|
||
<a-menu>
|
||
<a-menu-item @click="openTypeModal(item)">编辑</a-menu-item>
|
||
<a-menu-item danger @click="handleDeleteType(item)">删除</a-menu-item>
|
||
</a-menu>
|
||
</template>
|
||
</a-dropdown>
|
||
</template>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</div>
|
||
|
||
<div class="dict-data-panel">
|
||
<a-card class="full-height-card" :bordered="false">
|
||
<template #title>
|
||
<div class="data-header" v-if="currentType">
|
||
<span>{{ currentType.name }}</span>
|
||
<span class="data-header-sub">({{ currentType.code }})</span>
|
||
</div>
|
||
<div v-else class="data-header">字典数据</div>
|
||
</template>
|
||
<template #extra>
|
||
<a-space v-if="currentType">
|
||
<a-button @click="loadDictData(currentType.id)">
|
||
<ReloadOutlined />
|
||
</a-button>
|
||
<a-button type="primary" @click="openDataModal()">
|
||
<PlusOutlined /> 新增数据
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
|
||
<a-empty v-if="!currentType" description="请从左侧选择一个字典类型" class="empty-placeholder" />
|
||
|
||
<a-table
|
||
v-else
|
||
:columns="dataColumns"
|
||
:data-source="dictDataList"
|
||
:loading="loadingData"
|
||
row-key="id"
|
||
:pagination="false"
|
||
size="middle"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'tag'">
|
||
<a-tag :color="record.listClass || 'default'">{{ record.label }}</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.status === 'normal'"
|
||
checked-children="正常"
|
||
un-checked-children="停用"
|
||
size="small"
|
||
@change="(checked) => handleStatusChange(record, !!checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'action'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="openDataModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除该数据项?" @confirm="handleDeleteData(record.id)">
|
||
<a-button type="link" size="small" danger>删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</a-card>
|
||
</div>
|
||
|
||
<!-- 类型弹窗 -->
|
||
<a-modal
|
||
v-model:open="typeModalVisible"
|
||
:title="isEditType ? '编辑字典类型' : '新增字典类型'"
|
||
@ok="handleTypeSubmit"
|
||
>
|
||
<a-form :model="typeForm" layout="vertical">
|
||
<a-form-item label="类型名称" required>
|
||
<a-input v-model:value="typeForm.name" placeholder="如:用户性别" />
|
||
</a-form-item>
|
||
<a-form-item label="类型编码" required>
|
||
<a-input v-model:value="typeForm.code" placeholder="如:sys_user_sex" :disabled="isEditType" />
|
||
</a-form-item>
|
||
<a-form-item label="状态">
|
||
<a-radio-group v-model:value="typeForm.status">
|
||
<a-radio :value="true">正常</a-radio>
|
||
<a-radio :value="false">停用</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="备注">
|
||
<a-textarea v-model:value="typeForm.remark" :rows="3" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 数据弹窗 -->
|
||
<a-modal
|
||
v-model:open="dataModalVisible"
|
||
:title="isEditData ? '编辑字典数据' : '新增字典数据'"
|
||
@ok="handleDataSubmit"
|
||
>
|
||
<a-form :model="dataForm" layout="vertical">
|
||
<a-form-item label="数据标签" required>
|
||
<a-input v-model:value="dataForm.label" placeholder="如:男" />
|
||
</a-form-item>
|
||
<a-form-item label="数据键值" required>
|
||
<a-input v-model:value="dataForm.value" placeholder="如:1" />
|
||
</a-form-item>
|
||
<a-form-item label="显示排序" required>
|
||
<a-input-number v-model:value="dataForm.sort" :min="0" style="width: 100%" />
|
||
</a-form-item>
|
||
<a-form-item label="回显样式">
|
||
<a-select v-model:value="dataForm.listClass">
|
||
<a-select-option value="default">默认(Default)</a-select-option>
|
||
<a-select-option value="processing">蓝色(Primary)</a-select-option>
|
||
<a-select-option value="success">绿色(Success)</a-select-option>
|
||
<a-select-option value="warning">橙色(Warning)</a-select-option>
|
||
<a-select-option value="error">红色(Error)</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="状态">
|
||
<a-radio-group v-model:value="dataForm.status">
|
||
<a-radio value="normal">正常</a-radio>
|
||
<a-radio value="disabled">停用</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="备注">
|
||
<a-textarea v-model:value="dataForm.remark" :rows="2" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, reactive, onMounted } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { PlusOutlined, ReloadOutlined, MoreOutlined } from '@ant-design/icons-vue'
|
||
|
||
interface DictType {
|
||
id: number
|
||
name: string
|
||
code: string
|
||
status: boolean
|
||
remark?: string
|
||
}
|
||
|
||
interface DictData {
|
||
id: number
|
||
typeId: number
|
||
label: string
|
||
value: string
|
||
sort: number
|
||
status: 'normal' | 'disabled'
|
||
listClass?: string
|
||
remark?: string
|
||
}
|
||
|
||
const loadingTypes = ref(false)
|
||
const typeList = ref<DictType[]>([])
|
||
const typeSearchKeyword = ref('')
|
||
const currentType = ref<DictType | null>(null)
|
||
|
||
const loadingData = ref(false)
|
||
const dictDataList = ref<DictData[]>([])
|
||
|
||
// Mock Data Initialization
|
||
const mockTypes: DictType[] = [
|
||
{ id: 1, name: '用户性别', code: 'sys_user_sex', status: true },
|
||
{ id: 2, name: '菜单状态', code: 'sys_show_hide', status: true },
|
||
{ id: 3, name: '系统开关', code: 'sys_normal_disable', status: true },
|
||
{ id: 4, name: '通知类型', code: 'sys_notice_type', status: true },
|
||
{ id: 5, name: '操作类型', code: 'sys_oper_type', status: true },
|
||
]
|
||
|
||
const mockDataStore: Record<number, DictData[]> = {
|
||
1: [
|
||
{ id: 11, typeId: 1, label: '男', value: '0', sort: 1, status: 'normal', listClass: 'processing' },
|
||
{ id: 12, typeId: 1, label: '女', value: '1', sort: 2, status: 'normal', listClass: 'error' },
|
||
{ id: 13, typeId: 1, label: '未知', value: '2', sort: 3, status: 'normal', listClass: 'default' },
|
||
],
|
||
3: [
|
||
{ id: 31, typeId: 3, label: '正常', value: '0', sort: 1, status: 'normal', listClass: 'success' },
|
||
{ id: 32, typeId: 3, label: '停用', value: '1', sort: 2, status: 'normal', listClass: 'error' },
|
||
]
|
||
}
|
||
|
||
// Logic
|
||
const filteredTypes = computed(() => {
|
||
if (!typeSearchKeyword.value) return typeList.value
|
||
const kw = typeSearchKeyword.value.toLowerCase()
|
||
return typeList.value.filter(t => t.name.toLowerCase().includes(kw) || t.code.toLowerCase().includes(kw))
|
||
})
|
||
|
||
const dataColumns = [
|
||
{ title: '字典标签', key: 'tag', width: '20%' },
|
||
{ title: '字典键值', dataIndex: 'value', key: 'value', width: '20%' },
|
||
{ title: '排序', dataIndex: 'sort', key: 'sort', sorter: (a: DictData, b: DictData) => a.sort - b.sort, width: '15%' },
|
||
{ title: '状态', key: 'status', width: '15%' },
|
||
{ title: '备注', dataIndex: 'remark', key: 'remark' },
|
||
{ title: '操作', key: 'action', width: '150px' }
|
||
]
|
||
|
||
// Modal Logic - Type
|
||
const typeModalVisible = ref(false)
|
||
const isEditType = ref(false)
|
||
const typeForm = reactive({
|
||
id: 0,
|
||
name: '',
|
||
code: '',
|
||
status: true,
|
||
remark: ''
|
||
})
|
||
|
||
function openTypeModal(item?: DictType) {
|
||
if (item) {
|
||
isEditType.value = true
|
||
Object.assign(typeForm, item)
|
||
} else {
|
||
isEditType.value = false
|
||
Object.assign(typeForm, { id: 0, name: '', code: '', status: true, remark: '' })
|
||
}
|
||
typeModalVisible.value = true
|
||
}
|
||
|
||
function handleTypeSubmit() {
|
||
if (!typeForm.name || !typeForm.code) {
|
||
message.warning('请填写完整信息')
|
||
return
|
||
}
|
||
|
||
if (isEditType.value) {
|
||
const idx = typeList.value.findIndex(t => t.id === typeForm.id)
|
||
if (idx > -1) {
|
||
typeList.value[idx] = { ...typeForm }
|
||
message.success('修改成功')
|
||
}
|
||
} else {
|
||
const newId = Math.max(...typeList.value.map(t => t.id), 0) + 1
|
||
const newType: DictType = { ...typeForm, id: newId }
|
||
typeList.value.unshift(newType)
|
||
message.success('新增成功')
|
||
handleSelectType(newType)
|
||
}
|
||
typeModalVisible.value = false
|
||
}
|
||
|
||
function handleDeleteType(item: DictType) {
|
||
typeList.value = typeList.value.filter(t => t.id !== item.id)
|
||
if (currentType.value?.id === item.id) {
|
||
currentType.value = null
|
||
dictDataList.value = []
|
||
}
|
||
message.success('删除成功')
|
||
}
|
||
|
||
// Modal Logic - Data
|
||
const dataModalVisible = ref(false)
|
||
const isEditData = ref(false)
|
||
const dataForm = reactive({
|
||
id: 0,
|
||
label: '',
|
||
value: '',
|
||
sort: 0,
|
||
status: 'normal' as 'normal' | 'disabled',
|
||
listClass: 'default',
|
||
remark: ''
|
||
})
|
||
|
||
function openDataModal(item?: any) {
|
||
if (!currentType.value) {
|
||
message.warning('请先选择字典类型')
|
||
return
|
||
}
|
||
|
||
if (item) {
|
||
isEditData.value = true
|
||
Object.assign(dataForm, item)
|
||
} else {
|
||
isEditData.value = false
|
||
Object.assign(dataForm, {
|
||
id: 0,
|
||
label: '',
|
||
value: '',
|
||
sort: 0,
|
||
status: 'normal',
|
||
listClass: 'default',
|
||
remark: ''
|
||
})
|
||
}
|
||
dataModalVisible.value = true
|
||
}
|
||
|
||
function handleDataSubmit() {
|
||
if (!dataForm.label || !dataForm.value) {
|
||
message.warning('请填写完整信息')
|
||
return
|
||
}
|
||
|
||
if (isEditData.value) {
|
||
const idx = dictDataList.value.findIndex(d => d.id === dataForm.id)
|
||
if (idx > -1) {
|
||
dictDataList.value[idx] = { ...dataForm, typeId: currentType.value!.id }
|
||
message.success('修改成功')
|
||
}
|
||
} else {
|
||
const newId = Date.now()
|
||
dictDataList.value.push({
|
||
...dataForm,
|
||
id: newId,
|
||
typeId: currentType.value!.id
|
||
})
|
||
message.success('新增成功')
|
||
}
|
||
dataModalVisible.value = false
|
||
}
|
||
|
||
function handleDeleteData(id: number) {
|
||
dictDataList.value = dictDataList.value.filter(d => d.id !== id)
|
||
message.success('删除成功')
|
||
}
|
||
|
||
function handleStatusChange(record: any, checked: boolean) {
|
||
record.status = checked ? 'normal' : 'disabled'
|
||
message.success('状态已更新')
|
||
}
|
||
|
||
// Data Loading
|
||
function loadTypes() {
|
||
loadingTypes.value = true
|
||
setTimeout(() => {
|
||
typeList.value = [...mockTypes]
|
||
loadingTypes.value = false
|
||
}, 300)
|
||
}
|
||
|
||
function handleSelectType(type: DictType) {
|
||
currentType.value = type
|
||
loadDictData(type.id)
|
||
}
|
||
|
||
function loadDictData(typeId: number) {
|
||
loadingData.value = true
|
||
// In real app, fetch by code or id
|
||
setTimeout(() => {
|
||
dictDataList.value = mockDataStore[typeId] ? [...mockDataStore[typeId]] : []
|
||
loadingData.value = false
|
||
}, 300)
|
||
}
|
||
|
||
function handleTypeSearch() {
|
||
// auto-handled by computed
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadTypes()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dict-manage {
|
||
height: 100%;
|
||
display: flex;
|
||
gap: 16px;
|
||
background-color: #f0f2f5;
|
||
}
|
||
|
||
.dict-types-panel {
|
||
width: 300px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.dict-data-panel {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.full-height-card {
|
||
height: 100%;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.ant-card-body) {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.search-box {
|
||
padding: 0 4px 12px 4px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.type-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.type-item {
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.type-item:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.type-item.active {
|
||
background-color: #e6f7ff;
|
||
}
|
||
|
||
.type-name {
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.type-code {
|
||
font-size: 12px;
|
||
color: #999;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.data-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.data-header-sub {
|
||
font-size: 13px;
|
||
color: #999;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.empty-placeholder {
|
||
margin-top: 100px;
|
||
}
|
||
</style>
|