Files
nanxiisletAdmin/src/views/settings/dict/index.vue
2025-12-28 22:12:08 +08:00

499 lines
14 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="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>