Dictionary.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. <template>
  2. <div class="dictionary-management">
  3. <div class="page-header">
  4. <h2>字典管理</h2>
  5. <p>管理系统字典类型和字典项</p>
  6. </div>
  7. <!-- 主容器:树 + 列表 -->
  8. <div class="content-container">
  9. <!-- 左侧:字典类型树 -->
  10. <div class="tree-panel">
  11. <div class="tree-header">
  12. <h3>字典类型</h3>
  13. <div class="tree-actions">
  14. <el-button type="primary" size="small" @click="openCreateCategoryDialog">
  15. <el-icon><Plus /></el-icon>
  16. 新增
  17. </el-button>
  18. <el-button
  19. type="warning"
  20. size="small"
  21. @click="openEditCategoryDialog"
  22. :disabled="!selectedCategory"
  23. >
  24. <el-icon><Edit /></el-icon>
  25. 修改
  26. </el-button>
  27. <el-button
  28. type="danger"
  29. size="small"
  30. @click="deleteCategoryConfirm"
  31. :disabled="!selectedCategory"
  32. >
  33. <el-icon><Delete /></el-icon>
  34. 删除
  35. </el-button>
  36. </div>
  37. </div>
  38. <el-tree
  39. ref="treeRef"
  40. :data="categoryTree"
  41. node-key="category_id"
  42. :props="{ children: 'children', label: 'category_name' }"
  43. default-expand-all
  44. highlight-current
  45. @node-click="handleTreeNodeClick"
  46. v-loading="treeLoading"
  47. />
  48. </div>
  49. <!-- 右侧:字典项列表 -->
  50. <div class="list-panel">
  51. <div class="list-header">
  52. <div v-if="selectedCategory">
  53. <h3>{{ selectedCategory.category_name }} 中的字典项</h3>
  54. </div>
  55. <div v-else>
  56. <h3>请在左侧选择字典类型</h3>
  57. </div>
  58. <div class="list-actions" v-if="selectedCategory">
  59. <el-input
  60. v-model="searchKeyword"
  61. placeholder="搜索字典项"
  62. style="width: 200px; margin-right: 10px"
  63. clearable
  64. @clear="loadItems"
  65. >
  66. <template #prefix>
  67. <el-icon><Search /></el-icon>
  68. </template>
  69. </el-input>
  70. <el-select
  71. v-model="searchEnableFlag"
  72. placeholder="状态"
  73. style="width: 120px; margin-right: 10px"
  74. clearable
  75. @change="loadItems"
  76. >
  77. <el-option label="全部" value="" />
  78. <el-option label="启用" value="1" />
  79. <el-option label="禁用" value="0" />
  80. </el-select>
  81. <el-button type="primary" @click="loadItems">
  82. <el-icon><Search /></el-icon>
  83. 查询
  84. </el-button>
  85. <el-button type="primary" @click="openCreateItemDialog">
  86. <el-icon><Plus /></el-icon>
  87. 新增
  88. </el-button>
  89. <el-button
  90. type="danger"
  91. @click="batchDeleteItems"
  92. :disabled="selectedItems.length === 0"
  93. >
  94. <el-icon><Delete /></el-icon>
  95. 批量删除
  96. </el-button>
  97. </div>
  98. </div>
  99. <!-- 字典项列表 -->
  100. <el-table
  101. v-if="selectedCategory"
  102. v-loading="loading"
  103. :data="items"
  104. style="width: 100%"
  105. @selection-change="handleSelectionChange"
  106. >
  107. <el-table-column type="selection" width="55" />
  108. <el-table-column prop="dict_name" label="字典名称" min-width="150" show-overflow-tooltip />
  109. <el-table-column prop="dict_value" label="字典值" width="120" show-overflow-tooltip />
  110. <el-table-column prop="dict_desc" label="字典备注" min-width="150" show-overflow-tooltip>
  111. <template #default="{ row }">
  112. <el-tooltip v-if="row.dict_desc && row.dict_desc.length > 100" :content="row.dict_desc" placement="top">
  113. <span>{{ row.dict_desc.substring(0, 100) }}...</span>
  114. </el-tooltip>
  115. <span v-else>{{ row.dict_desc }}</span>
  116. </template>
  117. </el-table-column>
  118. <el-table-column prop="sort" label="排序" width="80" align="center" />
  119. <el-table-column prop="enable_flag" label="状态" width="100" align="center">
  120. <template #default="{ row }">
  121. <el-switch
  122. v-model="row.enable_flag"
  123. active-value="1"
  124. inactive-value="0"
  125. @change="handleStatusChange(row)"
  126. />
  127. </template>
  128. </el-table-column>
  129. <el-table-column prop="created_by_name" label="创建人" width="100" />
  130. <el-table-column prop="created_time" label="创建时间" width="160">
  131. <template #default="{ row }">
  132. {{ formatDate(row.created_time) }}
  133. </template>
  134. </el-table-column>
  135. <el-table-column prop="updated_by_name" label="修改人" width="100" />
  136. <el-table-column prop="updated_time" label="修改时间" width="160">
  137. <template #default="{ row }">
  138. {{ formatDate(row.updated_time) }}
  139. </template>
  140. </el-table-column>
  141. <el-table-column label="操作" width="150" fixed="right">
  142. <template #default="{ row }">
  143. <el-button type="warning" size="small" @click="editItem(row)">
  144. 修改
  145. </el-button>
  146. <el-button type="danger" size="small" @click="deleteItem(row)">
  147. 删除
  148. </el-button>
  149. </template>
  150. </el-table-column>
  151. </el-table>
  152. <div v-else class="empty-state">
  153. <el-empty description="请在左侧选择一个字典类型" />
  154. </div>
  155. <!-- 分页 -->
  156. <div v-if="selectedCategory" class="pagination">
  157. <el-pagination
  158. v-model:current-page="currentPage"
  159. v-model:page-size="pageSize"
  160. :page-sizes="[10, 20, 50, 100]"
  161. :total="total"
  162. layout="total, sizes, prev, pager, next, jumper"
  163. @size-change="handleSizeChange"
  164. @current-change="handleCurrentChange"
  165. />
  166. </div>
  167. </div>
  168. </div>
  169. <!-- 创建/编辑字典类型对话框 -->
  170. <el-dialog
  171. v-model="showCategoryDialog"
  172. :title="editingCategory ? '编辑字典类型' : '创建字典类型'"
  173. width="600px"
  174. @close="resetCategoryForm"
  175. >
  176. <el-form
  177. ref="categoryFormRef"
  178. :model="categoryForm"
  179. :rules="categoryRules"
  180. label-width="120px"
  181. >
  182. <el-form-item label="字典类型ID" prop="category_id" v-if="editingCategory">
  183. <el-input v-model="categoryForm.category_id" disabled />
  184. </el-form-item>
  185. <el-form-item label="字典类型编号" prop="category_no">
  186. <el-input v-model="categoryForm.category_no" placeholder="请输入字典类型编号" maxlength="512" />
  187. </el-form-item>
  188. <el-form-item label="字典类型名称" prop="category_name">
  189. <el-input v-model="categoryForm.category_name" placeholder="请输入字典类型名称" maxlength="512" />
  190. </el-form-item>
  191. <el-form-item label="字典类型备注" prop="category_desc">
  192. <el-input
  193. v-model="categoryForm.category_desc"
  194. type="textarea"
  195. :rows="3"
  196. placeholder="请输入字典类型备注"
  197. maxlength="512"
  198. show-word-limit
  199. />
  200. </el-form-item>
  201. <el-form-item label="父节点" prop="parent_field">
  202. <el-tree-select
  203. v-model="categoryForm.parent_field"
  204. :data="categoryTreeForSelect"
  205. node-key="category_id"
  206. :props="{ children: 'children', label: 'category_name' }"
  207. placeholder="请选择父节点(默认为根节点)"
  208. check-strictly
  209. clearable
  210. />
  211. </el-form-item>
  212. <el-form-item label="字典层级" prop="category_level">
  213. <el-input v-model="categoryForm.category_level" placeholder="请输入字典层级" maxlength="1" />
  214. </el-form-item>
  215. </el-form>
  216. <template #footer>
  217. <el-button @click="showCategoryDialog = false">取消</el-button>
  218. <el-button type="primary" @click="submitCategoryForm" :loading="submitting">
  219. 确定
  220. </el-button>
  221. </template>
  222. </el-dialog>
  223. <!-- 创建/编辑字典项对话框 -->
  224. <el-dialog
  225. v-model="showItemDialog"
  226. :title="editingItem ? '编辑字典项' : '创建字典项'"
  227. width="600px"
  228. @close="resetItemForm"
  229. >
  230. <el-form
  231. ref="itemFormRef"
  232. :model="itemForm"
  233. :rules="itemRules"
  234. label-width="120px"
  235. >
  236. <el-form-item label="字典名称" prop="dict_name">
  237. <el-input v-model="itemForm.dict_name" placeholder="请输入字典名称" maxlength="512" />
  238. </el-form-item>
  239. <el-form-item label="字典值" prop="dict_value">
  240. <el-input v-model="itemForm.dict_value" placeholder="请输入字典值" maxlength="512" />
  241. </el-form-item>
  242. <el-form-item label="字典备注" prop="dict_desc">
  243. <el-input
  244. v-model="itemForm.dict_desc"
  245. type="textarea"
  246. :rows="3"
  247. placeholder="请输入字典备注"
  248. maxlength="512"
  249. show-word-limit
  250. />
  251. </el-form-item>
  252. <el-form-item label="字典类型" prop="category_id">
  253. <el-tree-select
  254. v-model="itemForm.category_id"
  255. :data="categoryTree"
  256. node-key="category_id"
  257. :props="{ children: 'children', label: 'category_name' }"
  258. placeholder="请选择字典类型"
  259. check-strictly
  260. />
  261. </el-form-item>
  262. <el-form-item label="排序" prop="sort">
  263. <el-input-number v-model="itemForm.sort" :min="0" controls-position="right" />
  264. </el-form-item>
  265. <el-form-item label="启用状态" prop="enable_flag">
  266. <el-radio-group v-model="itemForm.enable_flag">
  267. <el-radio label="1">启用</el-radio>
  268. <el-radio label="0">禁用</el-radio>
  269. </el-radio-group>
  270. </el-form-item>
  271. </el-form>
  272. <template #footer>
  273. <el-button @click="showItemDialog = false">取消</el-button>
  274. <el-button type="primary" @click="submitItemForm" :loading="submitting">
  275. 确定
  276. </el-button>
  277. </template>
  278. </el-dialog>
  279. </div>
  280. </template>
  281. <script setup lang="ts">
  282. import { ref, reactive, onMounted, computed } from 'vue'
  283. import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
  284. import { Plus, Edit, Delete, Search } from '@element-plus/icons-vue'
  285. import { dictApi, type DictCategory, type DictItem, type DictCategoryForm, type DictItemForm } from '@/api/dict'
  286. // 树相关
  287. const treeRef = ref()
  288. const treeLoading = ref(false)
  289. const categoryTree = ref<DictCategory[]>([])
  290. const selectedCategory = ref<DictCategory | null>(null)
  291. // 列表相关
  292. const loading = ref(false)
  293. const items = ref<DictItem[]>([])
  294. const currentPage = ref(1)
  295. const pageSize = ref(10)
  296. const total = ref(0)
  297. const selectedItems = ref<DictItem[]>([])
  298. // 搜索相关
  299. const searchKeyword = ref('')
  300. const searchEnableFlag = ref('')
  301. // 字典类型对话框
  302. const showCategoryDialog = ref(false)
  303. const editingCategory = ref(false)
  304. const categoryFormRef = ref<FormInstance>()
  305. const categoryForm = reactive<DictCategoryForm & { category_id?: string }>({
  306. category_id: '',
  307. category_no: '',
  308. category_name: '',
  309. category_desc: '',
  310. parent_field: '0',
  311. category_level: ''
  312. })
  313. const categoryRules: FormRules = {
  314. category_name: [
  315. { required: true, message: '请输入字典类型名称', trigger: 'blur' },
  316. { max: 512, message: '字典类型名称不能超过512个字符', trigger: 'blur' }
  317. ],
  318. category_no: [
  319. { max: 512, message: '字典类型编号不能超过512个字符', trigger: 'blur' }
  320. ],
  321. category_desc: [
  322. { max: 512, message: '字典类型备注不能超过512个字符', trigger: 'blur' }
  323. ]
  324. }
  325. // 字典项对话框
  326. const showItemDialog = ref(false)
  327. const editingItem = ref(false)
  328. const itemFormRef = ref<FormInstance>()
  329. const itemForm = reactive<DictItemForm & { dict_id?: number }>({
  330. dict_id: undefined,
  331. dict_name: '',
  332. dict_value: '',
  333. dict_desc: '',
  334. category_id: '',
  335. enable_flag: '1',
  336. sort: 0
  337. })
  338. const itemRules: FormRules = {
  339. dict_name: [
  340. { required: true, message: '请输入字典名称', trigger: 'blur' },
  341. { max: 512, message: '字典名称不能超过512个字符', trigger: 'blur' }
  342. ],
  343. dict_value: [
  344. { required: true, message: '请输入字典值', trigger: 'blur' },
  345. { max: 512, message: '字典值不能超过512个字符', trigger: 'blur' }
  346. ],
  347. dict_desc: [
  348. { max: 512, message: '字典备注不能超过512个字符', trigger: 'blur' }
  349. ],
  350. category_id: [
  351. { required: true, message: '请选择字典类型', trigger: 'change' }
  352. ]
  353. }
  354. const submitting = ref(false)
  355. // 计算属性:用于选择父节点的树(排除当前编辑的节点)
  356. const categoryTreeForSelect = computed(() => {
  357. if (!editingCategory.value) {
  358. return [{ category_id: '0', category_name: '根节点', children: categoryTree.value }]
  359. }
  360. // 编辑时需要排除自己和子节点
  361. const filterNode = (nodes: DictCategory[], excludeId: string): DictCategory[] => {
  362. return nodes.filter(node => node.category_id !== excludeId).map(node => ({
  363. ...node,
  364. children: node.children ? filterNode(node.children, excludeId) : []
  365. }))
  366. }
  367. return [{
  368. category_id: '0',
  369. category_name: '根节点',
  370. children: filterNode(categoryTree.value, categoryForm.category_id || '')
  371. }]
  372. })
  373. // 加载字典类型树
  374. const loadCategoryTree = async () => {
  375. treeLoading.value = true
  376. try {
  377. const res = await dictApi.getCategoryTree()
  378. if (res.code === '000000') {
  379. categoryTree.value = res.data || []
  380. } else {
  381. ElMessage.error(res.message || '加载字典类型树失败')
  382. }
  383. } catch (error: any) {
  384. console.error('加载字典类型树失败:', error)
  385. ElMessage.error('加载字典类型树失败')
  386. } finally {
  387. treeLoading.value = false
  388. }
  389. }
  390. // 树节点点击
  391. const handleTreeNodeClick = (data: DictCategory) => {
  392. selectedCategory.value = data
  393. currentPage.value = 1
  394. searchKeyword.value = ''
  395. searchEnableFlag.value = ''
  396. loadItems()
  397. }
  398. // 加载字典项列表
  399. const loadItems = async () => {
  400. if (!selectedCategory.value) return
  401. loading.value = true
  402. try {
  403. const res = await dictApi.getItemList({
  404. category_id: selectedCategory.value.category_id,
  405. keyword: searchKeyword.value || undefined,
  406. enable_flag: searchEnableFlag.value || undefined,
  407. page: currentPage.value,
  408. page_size: pageSize.value
  409. })
  410. if (res.code === '000000') {
  411. items.value = res.data.list || []
  412. total.value = res.data.total || 0
  413. } else {
  414. ElMessage.error(res.message || '加载字典项列表失败')
  415. }
  416. } catch (error: any) {
  417. console.error('加载字典项列表失败:', error)
  418. ElMessage.error('加载字典项列表失败')
  419. } finally {
  420. loading.value = false
  421. }
  422. }
  423. // 分页相关
  424. const handleSizeChange = (val: number) => {
  425. pageSize.value = val
  426. currentPage.value = 1
  427. loadItems()
  428. }
  429. const handleCurrentChange = (val: number) => {
  430. currentPage.value = val
  431. loadItems()
  432. }
  433. // 选择变化
  434. const handleSelectionChange = (val: DictItem[]) => {
  435. selectedItems.value = val
  436. }
  437. // ==================== 字典类型操作 ====================
  438. // 打开创建字典类型对话框
  439. const openCreateCategoryDialog = () => {
  440. editingCategory.value = false
  441. resetCategoryForm()
  442. showCategoryDialog.value = true
  443. }
  444. // 打开编辑字典类型对话框
  445. const openEditCategoryDialog = () => {
  446. if (!selectedCategory.value) return
  447. editingCategory.value = true
  448. Object.assign(categoryForm, {
  449. category_id: selectedCategory.value.category_id,
  450. category_no: selectedCategory.value.category_no || '',
  451. category_name: selectedCategory.value.category_name,
  452. category_desc: selectedCategory.value.category_desc || '',
  453. parent_field: selectedCategory.value.parent_field || '0',
  454. category_level: selectedCategory.value.category_level || ''
  455. })
  456. showCategoryDialog.value = true
  457. }
  458. // 重置字典类型表单
  459. const resetCategoryForm = () => {
  460. categoryFormRef.value?.resetFields()
  461. Object.assign(categoryForm, {
  462. category_id: '',
  463. category_no: '',
  464. category_name: '',
  465. category_desc: '',
  466. parent_field: '0',
  467. category_level: ''
  468. })
  469. }
  470. // 提交字典类型表单
  471. const submitCategoryForm = async () => {
  472. if (!categoryFormRef.value) return
  473. await categoryFormRef.value.validate(async (valid) => {
  474. if (!valid) return
  475. submitting.value = true
  476. try {
  477. const data: DictCategoryForm = {
  478. category_no: categoryForm.category_no || undefined,
  479. category_name: categoryForm.category_name,
  480. category_desc: categoryForm.category_desc || undefined,
  481. parent_field: categoryForm.parent_field || '0',
  482. category_level: categoryForm.category_level || undefined
  483. }
  484. if (editingCategory.value && categoryForm.category_id) {
  485. // 更新
  486. const res = await dictApi.updateCategory(categoryForm.category_id, data)
  487. if (res.code === '000000') {
  488. ElMessage.success('字典类型更新成功')
  489. showCategoryDialog.value = false
  490. await loadCategoryTree()
  491. } else {
  492. ElMessage.error(res.message || '字典类型更新失败')
  493. }
  494. } else {
  495. // 创建
  496. const res = await dictApi.createCategory(data)
  497. if (res.code === '000000') {
  498. ElMessage.success('字典类型创建成功')
  499. showCategoryDialog.value = false
  500. await loadCategoryTree()
  501. } else {
  502. ElMessage.error(res.message || '字典类型创建失败')
  503. }
  504. }
  505. } catch (error: any) {
  506. console.error('提交字典类型失败:', error)
  507. ElMessage.error('操作失败')
  508. } finally {
  509. submitting.value = false
  510. }
  511. })
  512. }
  513. // 删除字典类型确认
  514. const deleteCategoryConfirm = () => {
  515. if (!selectedCategory.value) return
  516. ElMessageBox.confirm(
  517. `确定要删除字典类型"${selectedCategory.value.category_name}"吗?`,
  518. '删除确认',
  519. {
  520. confirmButtonText: '确定',
  521. cancelButtonText: '取消',
  522. type: 'warning'
  523. }
  524. ).then(async () => {
  525. await deleteCategory()
  526. }).catch(() => {
  527. // 取消删除
  528. })
  529. }
  530. // 删除字典类型
  531. const deleteCategory = async () => {
  532. if (!selectedCategory.value) return
  533. try {
  534. const res = await dictApi.deleteCategory(selectedCategory.value.category_id)
  535. if (res.code === '000000') {
  536. ElMessage.success('字典类型删除成功')
  537. selectedCategory.value = null
  538. items.value = []
  539. await loadCategoryTree()
  540. } else {
  541. ElMessage.error(res.message || '字典类型删除失败')
  542. }
  543. } catch (error: any) {
  544. console.error('删除字典类型失败:', error)
  545. ElMessage.error('删除失败')
  546. }
  547. }
  548. // ==================== 字典项操作 ====================
  549. // 打开创建字典项对话框
  550. const openCreateItemDialog = () => {
  551. if (!selectedCategory.value) {
  552. ElMessage.warning('请先选择字典类型')
  553. return
  554. }
  555. editingItem.value = false
  556. resetItemForm()
  557. itemForm.category_id = selectedCategory.value.category_id
  558. showItemDialog.value = true
  559. }
  560. // 编辑字典项
  561. const editItem = (row: DictItem) => {
  562. editingItem.value = true
  563. Object.assign(itemForm, {
  564. dict_id: row.dict_id,
  565. dict_name: row.dict_name,
  566. dict_value: row.dict_value,
  567. dict_desc: row.dict_desc || '',
  568. category_id: row.category_id,
  569. enable_flag: row.enable_flag,
  570. sort: row.sort || 0
  571. })
  572. showItemDialog.value = true
  573. }
  574. // 重置字典项表单
  575. const resetItemForm = () => {
  576. itemFormRef.value?.resetFields()
  577. Object.assign(itemForm, {
  578. dict_id: undefined,
  579. dict_name: '',
  580. dict_value: '',
  581. dict_desc: '',
  582. category_id: '',
  583. enable_flag: '1',
  584. sort: 0
  585. })
  586. }
  587. // 提交字典项表单
  588. const submitItemForm = async () => {
  589. if (!itemFormRef.value) return
  590. await itemFormRef.value.validate(async (valid) => {
  591. if (!valid) return
  592. submitting.value = true
  593. try {
  594. const data: DictItemForm = {
  595. dict_name: itemForm.dict_name,
  596. dict_value: itemForm.dict_value,
  597. dict_desc: itemForm.dict_desc || undefined,
  598. category_id: itemForm.category_id,
  599. enable_flag: itemForm.enable_flag,
  600. sort: itemForm.sort
  601. }
  602. if (editingItem.value && itemForm.dict_id) {
  603. // 更新
  604. const res = await dictApi.updateItem(itemForm.dict_id, data)
  605. if (res.code === '000000') {
  606. ElMessage.success('字典项更新成功')
  607. showItemDialog.value = false
  608. await loadItems()
  609. } else {
  610. ElMessage.error(res.message || '字典项更新失败')
  611. }
  612. } else {
  613. // 创建
  614. const res = await dictApi.createItem(data)
  615. if (res.code === '000000') {
  616. ElMessage.success('字典项创建成功')
  617. showItemDialog.value = false
  618. await loadItems()
  619. } else {
  620. ElMessage.error(res.message || '字典项创建失败')
  621. }
  622. }
  623. } catch (error: any) {
  624. console.error('提交字典项失败:', error)
  625. ElMessage.error('操作失败')
  626. } finally {
  627. submitting.value = false
  628. }
  629. })
  630. }
  631. // 删除字典项
  632. const deleteItem = (row: DictItem) => {
  633. ElMessageBox.confirm(
  634. `确定要删除字典项"${row.dict_name}"吗?`,
  635. '删除确认',
  636. {
  637. confirmButtonText: '确定',
  638. cancelButtonText: '取消',
  639. type: 'warning'
  640. }
  641. ).then(async () => {
  642. try {
  643. const res = await dictApi.deleteItem(row.dict_id)
  644. if (res.code === '000000') {
  645. ElMessage.success('字典项删除成功')
  646. await loadItems()
  647. } else {
  648. ElMessage.error(res.message || '字典项删除失败')
  649. }
  650. } catch (error: any) {
  651. console.error('删除字典项失败:', error)
  652. ElMessage.error('删除失败')
  653. }
  654. }).catch(() => {
  655. // 取消删除
  656. })
  657. }
  658. // 批量删除字典项
  659. const batchDeleteItems = () => {
  660. if (selectedItems.value.length === 0) {
  661. ElMessage.warning('请先选择要删除的字典项')
  662. return
  663. }
  664. ElMessageBox.confirm(
  665. `确定要删除选中的 ${selectedItems.value.length} 个字典项吗?`,
  666. '批量删除确认',
  667. {
  668. confirmButtonText: '确定',
  669. cancelButtonText: '取消',
  670. type: 'warning'
  671. }
  672. ).then(async () => {
  673. try {
  674. const dictIds = selectedItems.value.map(item => item.dict_id)
  675. const res = await dictApi.batchDeleteItems(dictIds)
  676. if (res.code === '000000') {
  677. ElMessage.success(res.message || '批量删除成功')
  678. selectedItems.value = []
  679. await loadItems()
  680. } else {
  681. ElMessage.error(res.message || '批量删除失败')
  682. }
  683. } catch (error: any) {
  684. console.error('批量删除字典项失败:', error)
  685. ElMessage.error('批量删除失败')
  686. }
  687. }).catch(() => {
  688. // 取消删除
  689. })
  690. }
  691. // 切换字典项状态
  692. const handleStatusChange = async (row: DictItem) => {
  693. try {
  694. const res = await dictApi.toggleItemStatus(row.dict_id, row.enable_flag)
  695. if (res.code === '000000') {
  696. ElMessage.success(res.message || '状态更新成功')
  697. } else {
  698. ElMessage.error(res.message || '状态更新失败')
  699. // 恢复原状态
  700. row.enable_flag = row.enable_flag === '1' ? '0' : '1'
  701. }
  702. } catch (error: any) {
  703. console.error('切换字典项状态失败:', error)
  704. ElMessage.error('状态更新失败')
  705. // 恢复原状态
  706. row.enable_flag = row.enable_flag === '1' ? '0' : '1'
  707. }
  708. }
  709. // 格式化日期
  710. const formatDate = (dateStr: string | undefined) => {
  711. if (!dateStr) return '-'
  712. const date = new Date(dateStr)
  713. return date.toLocaleString('zh-CN', {
  714. year: 'numeric',
  715. month: '2-digit',
  716. day: '2-digit',
  717. hour: '2-digit',
  718. minute: '2-digit',
  719. second: '2-digit'
  720. })
  721. }
  722. // 初始化
  723. onMounted(() => {
  724. loadCategoryTree()
  725. })
  726. </script>
  727. <style scoped>
  728. .dictionary-management {
  729. padding: 20px;
  730. height: 100%;
  731. display: flex;
  732. flex-direction: column;
  733. }
  734. .page-header {
  735. margin-bottom: 20px;
  736. }
  737. .page-header h2 {
  738. margin: 0 0 8px 0;
  739. font-size: 24px;
  740. font-weight: 600;
  741. color: #303133;
  742. }
  743. .page-header p {
  744. margin: 0;
  745. font-size: 14px;
  746. color: #909399;
  747. }
  748. .content-container {
  749. flex: 1;
  750. display: flex;
  751. gap: 20px;
  752. overflow: hidden;
  753. }
  754. .tree-panel {
  755. width: 300px;
  756. background: #fff;
  757. border-radius: 4px;
  758. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  759. display: flex;
  760. flex-direction: column;
  761. overflow: hidden;
  762. }
  763. .tree-header {
  764. padding: 16px;
  765. border-bottom: 1px solid #ebeef5;
  766. }
  767. .tree-header h3 {
  768. margin: 0 0 12px 0;
  769. font-size: 16px;
  770. font-weight: 600;
  771. color: #303133;
  772. }
  773. .tree-actions {
  774. display: flex;
  775. gap: 8px;
  776. }
  777. .tree-actions .el-button {
  778. flex: 1;
  779. }
  780. .el-tree {
  781. flex: 1;
  782. overflow-y: auto;
  783. padding: 16px;
  784. }
  785. .list-panel {
  786. flex: 1;
  787. background: #fff;
  788. border-radius: 4px;
  789. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  790. display: flex;
  791. flex-direction: column;
  792. overflow: hidden;
  793. }
  794. .list-header {
  795. padding: 16px;
  796. border-bottom: 1px solid #ebeef5;
  797. display: flex;
  798. justify-content: space-between;
  799. align-items: center;
  800. }
  801. .list-header h3 {
  802. margin: 0;
  803. font-size: 16px;
  804. font-weight: 600;
  805. color: #303133;
  806. }
  807. .list-actions {
  808. display: flex;
  809. align-items: center;
  810. gap: 8px;
  811. }
  812. .el-table {
  813. flex: 1;
  814. }
  815. .empty-state {
  816. flex: 1;
  817. display: flex;
  818. align-items: center;
  819. justify-content: center;
  820. }
  821. .pagination {
  822. padding: 16px;
  823. border-top: 1px solid #ebeef5;
  824. display: flex;
  825. justify-content: flex-end;
  826. }
  827. </style>