mirror of
https://gitee.com/xiaonuobase/snowy.git
synced 2025-12-25 23:56:20 +08:00
【前端】字典界面样式更新,左侧支持搜索
This commit is contained in:
parent
02d1198a96
commit
de6ab3fcf2
@ -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
|
||||
}
|
||||
// 简单的前端搜索过滤,如果数据量大应该走后端
|
||||
// 这里暂不实现复杂的前端递归过滤,AntDV的Tree通常配合后端搜索或扁平化数据处理
|
||||
// 既然是字典树,数据量通常不大,我们只高亮匹配项,或者让用户通过视觉查找
|
||||
// 这里为了体验,我们保持树结构不变,仅高亮,且展开所有
|
||||
autoExpandParent.value = true
|
||||
// 递归获取所有包含key的节点的父级key,用于展开
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user