dictData.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. <template>
  2. <div class="dict-data-page">
  3. <!-- 面包屑导航 -->
  4. <NPageHeader style="margin-bottom: 16px" @back="handleBack">
  5. <template #title>
  6. <span>字典数据管理</span>
  7. <NTag v-if="dictTypeName" type="info" size="small" style="margin-left: 12px">
  8. {{ dictTypeName }}
  9. </NTag>
  10. </template>
  11. <template #subtitle>
  12. <span v-if="currentDictType">字典类型:{{ currentDictType }}</span>
  13. </template>
  14. </NPageHeader>
  15. <AiCrudPage
  16. ref="crudRef"
  17. api="/system/dict/data"
  18. :api-config="apiConfig"
  19. :load-detail-on-edit="true"
  20. :search-schema="searchSchema"
  21. :columns="tableColumns"
  22. :edit-schema="editSchema"
  23. :public-params="publicParams"
  24. :before-load-list="handleBeforeLoadList"
  25. :before-render-list="handleBeforeRenderList"
  26. row-key="dictCode"
  27. add-button-text="新增字典数据"
  28. :before-submit="handleBeforeSubmit"
  29. :before-delete="handleBeforeDelete"
  30. :before-render-form="handleBeforeRenderForm"
  31. :before-render-detail="handleBeforeRenderDetail"
  32. :edit-grid-cols="2"
  33. :show-pagination="!isTreeMode"
  34. :table-props="tableProps"
  35. :lazy="true"
  36. @add="handleToolbarAdd"
  37. @submit-success="handleSubmitSuccess"
  38. >
  39. <template #toolbar-end>
  40. <n-button v-if="isTreeMode" size="small" @click="toggleExpandAll">
  41. {{ expandAll ? '折叠全部' : '展开全部' }}
  42. </n-button>
  43. </template>
  44. <template #toolbar-right-start>
  45. <n-radio-group :value="viewMode" size="small" @update:value="handleViewModeChange">
  46. <n-radio-button v-for="item in viewModeOptions" :key="item.value" :value="item.value">
  47. {{ item.label }}
  48. </n-radio-button>
  49. </n-radio-group>
  50. </template>
  51. </AiCrudPage>
  52. </div>
  53. </template>
  54. <script setup>
  55. import { NPageHeader, NTag } from 'naive-ui'
  56. import { computed, h, nextTick, onMounted, ref } from 'vue'
  57. import { useRoute, useRouter } from 'vue-router'
  58. import { AiCrudPage } from '@/components/ai-form'
  59. import SystemTableCell from '@/components/common/SystemTableCell.vue'
  60. import { DICT_TAG_TYPE_OPTIONS } from '@/constants/dict-options'
  61. import { request } from '@/utils'
  62. defineOptions({ name: 'DictData', title: '字典数据' })
  63. const ROOT_PARENT_CODE = 0
  64. const router = useRouter()
  65. const route = useRoute()
  66. const crudRef = ref(null)
  67. // 当前字典类型
  68. const currentDictType = ref('')
  69. const dictTypeName = ref('')
  70. const parentDictOptions = ref([createRootOption()])
  71. const allDictData = ref([])
  72. const pendingParentDictCode = ref(ROOT_PARENT_CODE)
  73. const editingDictCode = ref(null)
  74. const latestListParams = ref({})
  75. const viewMode = ref('list')
  76. const expandAll = ref(true)
  77. const expandedKeys = ref([])
  78. const viewModeOptions = [
  79. { label: '平铺列表', value: 'list' },
  80. { label: '树形结构', value: 'tree' },
  81. ]
  82. // 字典状态选项
  83. const statusOptions = [
  84. { label: '正常', value: 1 },
  85. { label: '禁用', value: 0 },
  86. ]
  87. // 是否默认选项
  88. const isDefaultOptions = [
  89. { label: '是', value: 'Y' },
  90. { label: '否', value: 'N' },
  91. ]
  92. const isTreeMode = computed(() => viewMode.value === 'tree')
  93. const apiConfig = computed(() => ({
  94. list: isTreeMode.value ? 'get@/system/dict/data/list' : 'get@/system/dict/data/page',
  95. detail: 'post@/system/dict/data/getById',
  96. add: 'post@/system/dict/data/add',
  97. update: 'post@/system/dict/data/edit',
  98. delete: 'post@/system/dict/data/removeBatch',
  99. }))
  100. // 公共查询参数
  101. const publicParams = computed(() => {
  102. return currentDictType.value ? { dictType: currentDictType.value } : {}
  103. })
  104. const dictLabelMap = computed(() => {
  105. const map = new Map()
  106. allDictData.value.forEach((item) => {
  107. map.set(item.dictCode, item.dictLabel)
  108. })
  109. return map
  110. })
  111. const tableProps = computed(() => {
  112. if (!isTreeMode.value) {
  113. return {}
  114. }
  115. return {
  116. indent: 24,
  117. expandOnClick: true,
  118. expandedRowKeys: expandedKeys.value,
  119. onUpdateExpandedRowKeys: handleExpandedKeysUpdate,
  120. }
  121. })
  122. // 搜索表单配置
  123. const searchSchema = [
  124. {
  125. field: 'dictLabel',
  126. label: '字典标签',
  127. type: 'input',
  128. props: {
  129. placeholder: '请输入字典标签',
  130. },
  131. },
  132. {
  133. field: 'dictValue',
  134. label: '字典键值',
  135. type: 'input',
  136. props: {
  137. placeholder: '请输入字典键值',
  138. },
  139. },
  140. {
  141. field: 'dictStatus',
  142. label: '状态',
  143. type: 'select',
  144. props: {
  145. placeholder: '请选择状态',
  146. options: statusOptions,
  147. },
  148. },
  149. ]
  150. // 表格列配置
  151. const tableColumns = computed(() => [
  152. {
  153. prop: 'dictLabel',
  154. label: '字典项',
  155. minWidth: 190,
  156. render: row => h(SystemTableCell, {
  157. title: row.dictLabel,
  158. subtitle: row.dictValue,
  159. interactive: true,
  160. tooltip: `查看字典项:${row.dictLabel || row.dictValue || '-'}`,
  161. onActivate: () => crudRef.value?.showDetail(row),
  162. }),
  163. },
  164. {
  165. prop: 'parentDictCode',
  166. label: '上级节点',
  167. width: 160,
  168. render: row => getParentDictLabel(row.parentDictCode),
  169. },
  170. {
  171. prop: 'dictSort',
  172. label: '排序',
  173. width: 100,
  174. },
  175. {
  176. prop: 'dictStatus',
  177. label: '状态',
  178. width: 100,
  179. render: (row) => {
  180. return h(NTag, { type: row.dictStatus === 1 ? 'success' : 'error', size: 'small' }, { default: () => row.dictStatus === 1 ? '正常' : '禁用' },
  181. )
  182. },
  183. },
  184. {
  185. prop: 'isDefault',
  186. label: '是否默认',
  187. width: 100,
  188. render: (row) => {
  189. return h(NTag, { type: row.isDefault === 'Y' ? 'success' : 'default', size: 'small' }, { default: () => row.isDefault === 'Y' ? '是' : '否' },
  190. )
  191. },
  192. },
  193. {
  194. prop: 'listClass',
  195. label: '标签类型',
  196. width: 120,
  197. render: (row) => {
  198. if (!row.listClass)
  199. return '-'
  200. const typeMap = {
  201. default: { text: '默认', type: 'default' },
  202. success: { text: '成功', type: 'success' },
  203. info: { text: '信息', type: 'info' },
  204. warning: { text: '警告', type: 'warning' },
  205. error: { text: '错误', type: 'error' },
  206. // 兼容旧的命名
  207. primary: { text: '信息', type: 'info' },
  208. danger: { text: '错误', type: 'error' },
  209. }
  210. const config = typeMap[row.listClass] || { text: row.listClass, type: 'default' }
  211. // 如果是 default 类型,显示普通文字
  212. if (config.type === 'default') {
  213. return h('span', config.text)
  214. }
  215. // 其他类型显示标签
  216. return h(NTag, { type: config.type, size: 'small' }, { default: () => config.text })
  217. },
  218. },
  219. {
  220. prop: 'cssClass',
  221. label: '样式属性',
  222. width: 120,
  223. },
  224. {
  225. prop: 'remark',
  226. label: '备注',
  227. minWidth: 180,
  228. },
  229. {
  230. prop: 'createTime',
  231. label: '创建时间',
  232. width: 172,
  233. },
  234. {
  235. prop: 'action',
  236. label: '操作',
  237. width: 160,
  238. fixed: 'right',
  239. actions: [
  240. { label: '新增下级', key: 'addChild', type: 'primary', onClick: handleAddChild },
  241. { label: '编辑', key: 'edit', type: 'primary', onClick: handleEdit },
  242. { label: '删除', key: 'delete', type: 'error', onClick: handleDelete },
  243. ],
  244. },
  245. ])
  246. // 编辑表单配置
  247. const editSchema = computed(() => [
  248. {
  249. type: 'divider',
  250. label: '基础信息',
  251. props: {
  252. titlePlacement: 'left',
  253. },
  254. span: 2,
  255. },
  256. {
  257. field: 'parentDictCode',
  258. label: '上级节点',
  259. type: 'treeSelect',
  260. defaultValue: ROOT_PARENT_CODE,
  261. props: {
  262. placeholder: '请选择上级节点',
  263. clearable: true,
  264. filterable: true,
  265. defaultExpandAll: true,
  266. },
  267. options: () => parentDictOptions.value,
  268. },
  269. {
  270. field: 'dictType',
  271. label: '字典类型',
  272. type: 'input',
  273. rules: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
  274. defaultValue: currentDictType.value,
  275. props: {
  276. placeholder: '字典类型',
  277. disabled: true,
  278. readonly: true,
  279. },
  280. },
  281. {
  282. field: 'dictLabel',
  283. label: '字典标签',
  284. type: 'input',
  285. rules: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
  286. props: {
  287. placeholder: '请输入字典标签',
  288. },
  289. },
  290. {
  291. field: 'dictValue',
  292. label: '字典键值',
  293. type: 'input',
  294. rules: [{ required: true, message: '请输入字典键值', trigger: 'blur' }],
  295. props: {
  296. placeholder: '请输入字典键值',
  297. },
  298. },
  299. {
  300. field: 'dictSort',
  301. label: '排序',
  302. type: 'number',
  303. defaultValue: 0,
  304. props: {
  305. placeholder: '排序值',
  306. min: 0,
  307. },
  308. },
  309. {
  310. field: 'dictStatus',
  311. label: '状态',
  312. type: 'radio',
  313. defaultValue: 1,
  314. rules: [{ required: true, type: 'number', message: '请选择状态', trigger: 'change' }],
  315. props: {
  316. options: statusOptions,
  317. },
  318. },
  319. {
  320. field: 'isDefault',
  321. label: '是否默认',
  322. type: 'radio',
  323. defaultValue: 'N',
  324. props: {
  325. options: isDefaultOptions,
  326. },
  327. },
  328. {
  329. type: 'divider',
  330. label: '扩展信息',
  331. props: {
  332. titlePlacement: 'left',
  333. },
  334. span: 2,
  335. },
  336. {
  337. field: 'listClass',
  338. label: '标签类型',
  339. type: 'select',
  340. defaultValue: 'default',
  341. props: {
  342. placeholder: '请选择标签类型',
  343. options: DICT_TAG_TYPE_OPTIONS,
  344. },
  345. },
  346. {
  347. field: 'cssClass',
  348. label: '样式属性',
  349. type: 'input',
  350. props: {
  351. placeholder: '请输入样式属性(可选)',
  352. },
  353. },
  354. {
  355. field: 'remark',
  356. label: '备注',
  357. type: 'textarea',
  358. span: 2,
  359. props: {
  360. placeholder: '请输入备注',
  361. rows: 3,
  362. },
  363. },
  364. ])
  365. async function loadParentDictOptions(currentDictCode = null) {
  366. if (!currentDictType.value) {
  367. parentDictOptions.value = [createRootOption()]
  368. allDictData.value = []
  369. return
  370. }
  371. try {
  372. const res = await request.get('/system/dict/data/list', {
  373. params: {
  374. dictType: currentDictType.value,
  375. },
  376. })
  377. if (res.code === 200) {
  378. allDictData.value = Array.isArray(res.data) ? res.data : []
  379. const excludedDictCodes = collectExcludedDictCodes(allDictData.value, currentDictCode)
  380. const availableDictData = allDictData.value.filter(item => !excludedDictCodes.has(item.dictCode))
  381. const treeData = buildDictTree(availableDictData)
  382. parentDictOptions.value = [
  383. createRootOption(),
  384. ...convertToTreeSelectOptions(treeData),
  385. ]
  386. }
  387. }
  388. catch (error) {
  389. console.error('加载上级字典选项失败:', error)
  390. parentDictOptions.value = [createRootOption()]
  391. }
  392. }
  393. function createRootOption() {
  394. return {
  395. label: '顶级节点',
  396. value: ROOT_PARENT_CODE,
  397. key: ROOT_PARENT_CODE,
  398. }
  399. }
  400. function isRootParentCode(parentDictCode) {
  401. return parentDictCode === null
  402. || parentDictCode === undefined
  403. || parentDictCode === ''
  404. || Number(parentDictCode) === ROOT_PARENT_CODE
  405. }
  406. function normalizeParentDictCode(parentDictCode) {
  407. return isRootParentCode(parentDictCode) ? ROOT_PARENT_CODE : parentDictCode
  408. }
  409. function buildDictTree(list) {
  410. const nodeMap = new Map()
  411. const tree = []
  412. list.forEach((item) => {
  413. nodeMap.set(item.dictCode, {
  414. ...item,
  415. children: [],
  416. })
  417. })
  418. nodeMap.forEach((node) => {
  419. const parentDictCode = normalizeParentDictCode(node.parentDictCode)
  420. if (!isRootParentCode(parentDictCode) && nodeMap.has(parentDictCode) && parentDictCode !== node.dictCode) {
  421. nodeMap.get(parentDictCode).children.push(node)
  422. }
  423. else {
  424. tree.push(node)
  425. }
  426. })
  427. sortDictTree(tree)
  428. return tree
  429. }
  430. function sortDictTree(list) {
  431. list.sort((left, right) => {
  432. const leftSort = Number(left.dictSort ?? 0)
  433. const rightSort = Number(right.dictSort ?? 0)
  434. if (leftSort !== rightSort) {
  435. return leftSort - rightSort
  436. }
  437. return Number(left.dictCode ?? 0) - Number(right.dictCode ?? 0)
  438. })
  439. list.forEach((item) => {
  440. if (item.children && item.children.length > 0) {
  441. sortDictTree(item.children)
  442. }
  443. else {
  444. delete item.children
  445. }
  446. })
  447. }
  448. function convertToTreeSelectOptions(list) {
  449. return list.map(item => ({
  450. label: item.dictLabel,
  451. value: item.dictCode,
  452. key: item.dictCode,
  453. children: item.children && item.children.length > 0
  454. ? convertToTreeSelectOptions(item.children)
  455. : undefined,
  456. }))
  457. }
  458. function collectExcludedDictCodes(list, currentDictCode) {
  459. if (!currentDictCode) {
  460. return new Set()
  461. }
  462. const childrenMap = new Map()
  463. list.forEach((item) => {
  464. const parentDictCode = normalizeParentDictCode(item.parentDictCode)
  465. if (!isRootParentCode(parentDictCode)) {
  466. const children = childrenMap.get(parentDictCode) || []
  467. children.push(item.dictCode)
  468. childrenMap.set(parentDictCode, children)
  469. }
  470. })
  471. const excluded = new Set([currentDictCode])
  472. const queue = [currentDictCode]
  473. while (queue.length > 0) {
  474. const parentDictCode = queue.shift()
  475. const children = childrenMap.get(parentDictCode) || []
  476. children.forEach((dictCode) => {
  477. if (!excluded.has(dictCode)) {
  478. excluded.add(dictCode)
  479. queue.push(dictCode)
  480. }
  481. })
  482. }
  483. return excluded
  484. }
  485. function filterTreeModeList(list) {
  486. if (!isTreeMode.value) {
  487. return list
  488. }
  489. const { dictLabel, dictValue, dictStatus } = latestListParams.value
  490. return list.filter((item) => {
  491. const labelMatched = !dictLabel || String(item.dictLabel || '').includes(dictLabel)
  492. const valueMatched = !dictValue || String(item.dictValue || '').includes(dictValue)
  493. const statusMatched = dictStatus === null
  494. || dictStatus === undefined
  495. || dictStatus === ''
  496. || Number(item.dictStatus) === Number(dictStatus)
  497. return labelMatched && valueMatched && statusMatched
  498. })
  499. }
  500. function getAllKeys(list, keys = []) {
  501. list.forEach((item) => {
  502. keys.push(item.dictCode)
  503. if (item.children && item.children.length > 0) {
  504. getAllKeys(item.children, keys)
  505. }
  506. })
  507. return keys
  508. }
  509. function getParentDictLabel(parentDictCode) {
  510. if (isRootParentCode(parentDictCode)) {
  511. return '顶级节点'
  512. }
  513. return dictLabelMap.value.get(parentDictCode)
  514. || dictLabelMap.value.get(Number(parentDictCode))
  515. || parentDictCode
  516. }
  517. function handleBeforeLoadList(params) {
  518. latestListParams.value = { ...params }
  519. return params
  520. }
  521. function handleBeforeRenderList(list) {
  522. if (!isTreeMode.value) {
  523. expandedKeys.value = []
  524. return list
  525. }
  526. const treeList = buildDictTree(filterTreeModeList(list))
  527. expandedKeys.value = expandAll.value ? getAllKeys(treeList) : []
  528. return treeList
  529. }
  530. // 表单渲染前处理(新增时设置默认值)
  531. function handleBeforeRenderForm(data) {
  532. if (!data) {
  533. return {
  534. dictType: currentDictType.value,
  535. parentDictCode: pendingParentDictCode.value,
  536. }
  537. }
  538. editingDictCode.value = data.dictCode || null
  539. return data
  540. }
  541. function handleBeforeRenderDetail(data) {
  542. return {
  543. ...data,
  544. parentDictCode: normalizeParentDictCode(data?.parentDictCode),
  545. }
  546. }
  547. // 提交前处理
  548. function handleBeforeSubmit(formData) {
  549. if (formData.dictCode && Number(formData.dictCode) === Number(formData.parentDictCode)) {
  550. window.$message.warning('上级节点不能选择自己')
  551. return false
  552. }
  553. if (currentDictType.value) {
  554. formData.dictType = currentDictType.value
  555. }
  556. formData.parentDictCode = isRootParentCode(formData.parentDictCode) ? null : formData.parentDictCode
  557. return formData
  558. }
  559. // 返回
  560. function handleBack() {
  561. router.push('/system/dictType')
  562. }
  563. function handleExpandedKeysUpdate(keys) {
  564. expandedKeys.value = keys
  565. if (!isTreeMode.value) {
  566. return
  567. }
  568. const tableData = crudRef.value?.getTableData() || []
  569. const allKeys = getAllKeys(tableData)
  570. expandAll.value = allKeys.length > 0 && keys.length === allKeys.length
  571. }
  572. function toggleExpandAll() {
  573. expandAll.value = !expandAll.value
  574. if (expandAll.value) {
  575. const tableData = crudRef.value?.getTableData() || []
  576. expandedKeys.value = getAllKeys(tableData)
  577. }
  578. else {
  579. expandedKeys.value = []
  580. }
  581. }
  582. function handleViewModeChange(value) {
  583. if (viewMode.value === value) {
  584. return
  585. }
  586. viewMode.value = value
  587. if (!isTreeMode.value) {
  588. expandedKeys.value = []
  589. }
  590. nextTick(() => {
  591. crudRef.value?.loadList()
  592. })
  593. }
  594. async function handleBeforeDelete(rows = []) {
  595. const dictCodes = rows
  596. .map(row => row?.dictCode)
  597. .filter(dictCode => dictCode !== null && dictCode !== undefined && dictCode !== '')
  598. if (dictCodes.length === 0) {
  599. window.$message.warning('请选择要删除的字典数据')
  600. return false
  601. }
  602. window.$dialog.warning({
  603. title: '确认删除',
  604. content: `确定要删除选中的 ${dictCodes.length} 条字典数据吗?删除后将无法恢复!`,
  605. positiveText: '删除',
  606. negativeText: '取消',
  607. onPositiveClick: async () => {
  608. try {
  609. const res = await request.post('/system/dict/data/removeBatch', dictCodes)
  610. if (res.code === 200) {
  611. window.$message.success('删除成功')
  612. crudRef.value?.clearSelection()
  613. await loadParentDictOptions()
  614. crudRef.value?.refresh()
  615. }
  616. }
  617. catch {
  618. window.$message.error('删除失败')
  619. }
  620. },
  621. })
  622. return false
  623. }
  624. async function handleToolbarAdd() {
  625. pendingParentDictCode.value = ROOT_PARENT_CODE
  626. editingDictCode.value = null
  627. await loadParentDictOptions()
  628. }
  629. async function handleAddChild(row) {
  630. pendingParentDictCode.value = row.dictCode
  631. editingDictCode.value = null
  632. await loadParentDictOptions()
  633. crudRef.value?.showAdd()
  634. await nextTick()
  635. pendingParentDictCode.value = ROOT_PARENT_CODE
  636. }
  637. // 编辑
  638. async function handleEdit(row) {
  639. editingDictCode.value = row.dictCode || null
  640. await loadParentDictOptions(editingDictCode.value)
  641. crudRef.value?.showEdit(row)
  642. }
  643. // 删除
  644. function handleDelete(row) {
  645. window.$dialog.warning({
  646. title: '确认删除',
  647. content: '确定要删除该字典数据吗?删除后将无法恢复!',
  648. positiveText: '确定',
  649. negativeText: '取消',
  650. onPositiveClick: async () => {
  651. try {
  652. const res = await request.post('/system/dict/data/remove', null, {
  653. params: { dictCode: row.dictCode },
  654. })
  655. if (res.code === 200) {
  656. window.$message.success('删除成功')
  657. await loadParentDictOptions()
  658. crudRef.value?.refresh()
  659. }
  660. }
  661. catch {
  662. window.$message.error('删除失败')
  663. }
  664. },
  665. })
  666. }
  667. async function handleSubmitSuccess() {
  668. pendingParentDictCode.value = ROOT_PARENT_CODE
  669. editingDictCode.value = null
  670. await loadParentDictOptions()
  671. }
  672. // 初始化
  673. onMounted(async () => {
  674. // 从路由参数获取字典类型
  675. if (route.query.dictType) {
  676. currentDictType.value = route.query.dictType
  677. dictTypeName.value = route.query.dictName || ''
  678. }
  679. await loadParentDictOptions()
  680. // 延迟加载列表数据,确保 publicParams 已更新
  681. nextTick(() => {
  682. crudRef.value?.loadList()
  683. })
  684. })
  685. </script>
  686. <style scoped>
  687. .dict-data-page {
  688. height: 100%;
  689. display: flex;
  690. flex-direction: column;
  691. }
  692. .dict-data-page :deep(.ai-crud-page) {
  693. flex: 1;
  694. }
  695. </style>