【前端】字典界面样式更新,左侧支持搜索

This commit is contained in:
俞宝山 2025-12-09 21:38:19 +08:00
parent 02d1198a96
commit de6ab3fcf2
2 changed files with 266 additions and 125 deletions

View File

@ -1,97 +1,120 @@
<template>
<XnResizablePanel direction="row" :initial-size="300" :min-size="200" :max-size="500" :md="0">
<template #left>
<a-tree
v-if="treeData.length > 0"
v-model:expandedKeys="defaultExpandedKeys"
:tree-data="treeData"
:field-names="treeFieldNames"
@select="treeSelect"
/>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</template>
<template #right>
<a-form ref="searchFormRef" :model="searchFormState">
<a-row :gutter="10">
<a-col :xs="24" :sm="8" :md="8" :lg="0" :xl="0">
<a-form-item label="请选择上级字典:" name="parentId">
<a-tree-select
v-model:value="searchFormState.parentId"
class="xn-wd"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
placeholder="请选择上级字典"
<div class="dict-container">
<XnResizablePanel
:initialSize="300"
:minSize="250"
:maxSize="600"
:bottomGap="10"
:leftPadding="12"
:rightPadding="24"
>
<template #left>
<!-- 左侧面板 -->
<div class="dict-left-panel">
<a-spin :spinning="cardLoading">
<div class="dict-left-header">
<a-radio-group
v-model:value="categoryType"
button-style="solid"
class="dict-type-radio"
@change="typeChange"
>
<a-radio-button value="FRM">系统字典</a-radio-button>
<a-radio-button value="BIZ">业务字典</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="treeSearchKey"
placeholder="搜索字典"
class="dict-tree-search"
allow-clear
:tree-data="treeData"
:field-names="{
children: 'children',
label: 'name',
value: 'id'
}"
selectable="false"
tree-line
@search="onTreeSearch"
/>
</div>
<div class="dict-tree-wrapper">
<a-tree
v-if="treeData.length > 0"
v-model:expandedKeys="defaultExpandedKeys"
:tree-data="treeData"
:field-names="treeFieldNames"
block-node
:auto-expand-parent="autoExpandParent"
@select="treeSelect"
>
<template #title="{ dictLabel }">
<span v-if="dictLabel.indexOf(treeSearchKey) > -1">
{{ dictLabel.substr(0, dictLabel.indexOf(treeSearchKey)) }}
<span style="color: #f50">{{ treeSearchKey }}</span>
{{ dictLabel.substr(dictLabel.indexOf(treeSearchKey) + treeSearchKey.length) }}
</span>
<span v-else>{{ dictLabel }}</span>
</template>
</a-tree>
<a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" description="暂无数据" />
</div>
</a-spin>
</div>
</template>
<template #right>
<!-- 右侧面板 -->
<div class="dict-right-panel">
<!-- 搜索区域 -->
<a-form ref="searchFormRef" :model="searchFormState" layout="inline" class="search-form">
<a-form-item label="关键词" name="searchKey">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入字典名称关键词" allow-clear />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<a-form-item name="searchKey" label="关键词">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入字典名称关键词" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<a-form-item>
<a-space>
<a-button type="primary" @click="tableRef.refresh(true)">
<template #icon>
<SearchOutlined />
</template>
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="reset">
<template #icon>
<redo-outlined />
</template>
<template #icon><RedoOutlined /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:expand-row-by-click="true"
bordered
:tool-config="toolConfig"
:row-key="(record) => record.id"
:scroll="{ x: 'max-content' }"
>
<template #operator>
<a-button type="primary" @click="formRef.onOpen(undefined, categoryType, searchFormState.parentId)">
<template #icon><plus-outlined /></template>
新增
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'level'">
<a-tag color="blue" v-if="record.level">{{ record.level }}</a-tag>
<a-tag color="green" v-else>子级</a-tag>
</template>
<template v-if="column.dataIndex === 'dictLabel'">
<a-tag :color="record.dictColor">{{ record.dictLabel }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a @click="formRef.onOpen(record, categoryType)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="删除此字典与下级字典吗?" @confirm="remove(record)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</template>
</template>
</s-table>
</template>
</XnResizablePanel>
</a-form>
<!-- 表格区域 -->
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:expand-row-by-click="true"
bordered
:tool-config="toolConfig"
:row-key="(record) => record.id"
:scroll="{ x: 'max-content' }"
>
<template #operator>
<a-space>
<a-button type="primary" @click="formRef.onOpen(undefined, categoryType, searchFormState.parentId)">
<template #icon><PlusOutlined /></template>
新增字典
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'level'">
<a-tag color="processing" v-if="record.level">{{ record.level }}</a-tag>
<a-tag color="success" v-else>子级</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a @click="formRef.onOpen(record, categoryType)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除此字典及其下级字典吗?" @confirm="remove(record)" placement="topRight">
<a-button type="link" danger size="small" style="padding: 0">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</s-table>
</div>
</template>
</XnResizablePanel>
</div>
<Form ref="formRef" @successful="formSuccessful()" />
</template>
@ -100,50 +123,63 @@
import dictApi from '@/api/dev/dictApi'
import Form from './form.vue'
import tool from '@/utils/tool'
import XnResizablePanel from '@/components/XnResizablePanel/index.vue'
const props = defineProps({
type: {
type: String,
default: 'FRM'
}
})
const columns = [
const columns = ref([
{
title: '字典名称',
dataIndex: 'dictLabel'
dataIndex: 'dictLabel',
width: 200
},
{
title: '字典值',
dataIndex: 'dictValue'
dataIndex: 'dictValue',
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortCode'
dataIndex: 'sortCode',
width: 100,
align: 'center'
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
fixed: 'right'
fixed: 'right',
width: 150
}
]
const categoryType = computed(() => {
return props.type
})
])
const categoryType = ref('FRM')
const treeSearchKey = ref('')
const autoExpandParent = ref(true)
// tableDOM
const tableRef = ref(null)
const formRef = ref()
const cardLoading = ref(true)
const searchFormRef = ref()
const searchFormState = ref({})
//
let defaultExpandedKeys = ref([])
const treeData = ref([])
//
const treeDataOrigin = ref([])
// treeNode title,key,children
const treeFieldNames = { children: 'children', title: 'dictLabel', key: 'id' }
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
// Promise
const loadData = (parameter) => {
loadTreeData()
parameter.category = categoryType.value
return dictApi.dictPage(Object.assign(parameter, searchFormState.value)).then((data) => {
if (data.records) {
@ -168,39 +204,103 @@
return data
})
}
//
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
//
const typeChange = () => {
cardLoading.value = true
treeSearchKey.value = ''
loadTreeData()
//
searchFormState.value.parentId = undefined
const index = columns.value.findIndex((f) => f.title === '层级')
if (index !== -1) {
columns.value.splice(index, 1)
}
tableRef.value.refresh(true)
}
//
const loadTreeData = () => {
const param = {
category: categoryType.value
}
dictApi.dictTree(param).then((data) => {
if (data) {
treeData.value = data
}
})
dictApi
.dictTree(param)
.then((res) => {
if (res) {
treeData.value = res
treeDataOrigin.value = res
} else {
treeData.value = []
treeDataOrigin.value = []
}
})
.finally(() => {
cardLoading.value = false
})
}
//
loadTreeData()
//
const onTreeSearch = () => {
if (!treeSearchKey.value) {
treeData.value = treeDataOrigin.value
return
}
//
// AntDVTree
//
//
autoExpandParent.value = true
// keykey
const expanded = []
const getParentKeys = (data, key) => {
data.forEach((item) => {
if (item.children) {
if (JSON.stringify(item.children).indexOf(key) > -1) {
expanded.push(item.id)
}
getParentKeys(item.children, key)
}
})
}
getParentKeys(treeDataOrigin.value, treeSearchKey.value)
defaultExpandedKeys.value = [...new Set(expanded)]
}
//
watch(treeSearchKey, () => {
onTreeSearch()
})
//
const treeSelect = (selectedKeys) => {
if (selectedKeys && selectedKeys.length > 0) {
searchFormState.value.parentId = selectedKeys.toString()
if (!columns.find((f) => f.title === '层级')) {
columns.splice(2, 0, {
if (!columns.value.find((f) => f.title === '层级')) {
columns.value.splice(2, 0, {
title: '层级',
dataIndex: 'level',
width: 100
width: 100,
align: 'center'
})
}
} else {
delete searchFormState.value.parentId
columns.splice(2, 1)
const index = columns.value.findIndex((f) => f.title === '层级')
if (index !== -1) {
columns.value.splice(index, 1)
}
}
tableRef.value.refresh(true)
}
//
const remove = (record) => {
let params = [
@ -211,13 +311,17 @@
dictApi.dictDelete(params).then(() => {
tableRef.value.refresh()
refreshStoreDict()
loadTreeData() //
})
}
//
const formSuccessful = () => {
tableRef.value.refresh()
refreshStoreDict()
loadTreeData() // /
}
// store
const refreshStoreDict = () => {
dictApi.dictTree().then((res) => {
@ -227,11 +331,67 @@
</script>
<style scoped lang="less">
// XnResizablePanel
:deep(.panel-left) {
padding: 0 !important;
}
:deep(.panel-right) {
padding: 0 !important;
.dict-container {
height: 100%; /* 适配常见的顶部导航高度,保证铺满但不溢出 */
.dict-left-panel {
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
overflow: hidden; /* 防止外层滚动 */
:deep(.ant-spin-nested-loading) {
height: 100%;
display: flex;
flex-direction: column;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
}
}
.dict-left-header {
flex-shrink: 0;
margin-bottom: 12px;
text-align: center;
.dict-type-radio {
width: 100%;
margin-bottom: 12px;
display: flex;
:deep(.ant-radio-button-wrapper) {
flex: 1;
text-align: center;
}
}
.dict-tree-search {
width: 100%;
}
}
.dict-tree-wrapper {
flex: 1;
overflow-y: auto;
/* 隐藏滚动条但保留滚动功能 */
&::-webkit-scrollbar {
display: none;
}
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
}
.dict-right-panel {
overflow: hidden;
display: flex;
flex-direction: column;
.search-form {
margin-bottom: 16px;
flex-shrink: 0;
}
}
}
</style>

View File

@ -1,26 +1,7 @@
<template>
<a-card :bordered="false">
<a-tabs size="large" v-model:activeKey="activeKey">
<a-tab-pane v-for="item in tabListNoTitle" :key="item.key" :tab="item.tab">
<category :type="item.key" />
</a-tab-pane>
</a-tabs>
</a-card>
<category />
</template>
<script setup name="devDict">
import Category from './category/index.vue'
const activeKey = ref('FRM')
const tabListNoTitle = ref([
{ key: 'FRM', tab: '系统字典' },
{ key: 'BIZ', tab: '业务字典' }
])
</script>
<style lang="less" scoped>
:deep(.ant-card-body) {
padding-top: 0 !important;
}
:deep(.ant-tabs-tab) {
font-size: 14px !important;
}
</style>