dict.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <template>
  2. <view class="dict-container">
  3. <NavBar title="数据字典" @back="goBack" />
  4. <!-- 字典编码列表视图 -->
  5. <view class="dict-list-view" v-if="!showDetail">
  6. <!-- 搜索栏 -->
  7. <view class="search-bar">
  8. <input
  9. v-model="searchCode"
  10. class="search-input"
  11. placeholder="搜索字典编码"
  12. @confirm="handleSearch"
  13. />
  14. <view class="search-actions">
  15. <view class="btn-search" @click="handleSearch">搜索</view>
  16. <view class="btn-reset" @click="handleReset">重置</view>
  17. <view class="btn-create" @click="handleCreate">创建</view>
  18. </view>
  19. </view>
  20. <!-- 字典编码列表 -->
  21. <view class="code-list" v-if="codeList.length > 0">
  22. <view
  23. class="code-item"
  24. v-for="(item, index) in codeList"
  25. :key="index"
  26. @click="handleSelectDict(item)"
  27. >
  28. <view class="item-main">
  29. <text class="item-code">{{ item.code }}</text>
  30. <text class="item-remark" v-if="item.remark">{{ item.remark }}</text>
  31. </view>
  32. <view class="item-meta">
  33. <text class="item-count">{{ item.count }} 项</text>
  34. <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 加载中 -->
  39. <view class="loading-state" v-if="loading">
  40. <view class="loading-spinner"></view>
  41. <text class="loading-text">加载中...</text>
  42. </view>
  43. <!-- 空状态 -->
  44. <view class="empty-state" v-if="codeList.length === 0 && !loading">
  45. <view class="empty-icon-wrapper">
  46. <AppIcon name="book-open" size="48" color="#CCCCCC" />
  47. </view>
  48. <text class="empty-text">暂无字典数据</text>
  49. <view class="btn-create empty-btn" @click="handleCreate">创建字典</view>
  50. </view>
  51. </view>
  52. <!-- 字典编辑视图 -->
  53. <view class="dict-edit-view" v-if="showDetail">
  54. <view class="edit-header">
  55. <view class="back-row" @click="handleBackToList">
  56. <AppIcon name="chevron-left" size="18" color="#C6171E" />
  57. <text class="edit-title">{{ isNew ? '创建字典' : editForm.code }}</text>
  58. </view>
  59. </view>
  60. <view class="edit-form">
  61. <!-- 字典编码(新建时可编辑,已有不可改) -->
  62. <view class="form-row">
  63. <text class="form-label">字典编码</text>
  64. <input
  65. v-model="editForm.code"
  66. class="form-input"
  67. placeholder="请输入字典编码"
  68. :disabled="!isNew"
  69. />
  70. </view>
  71. <!-- 备注 -->
  72. <view class="form-row">
  73. <text class="form-label">备注</text>
  74. <input
  75. v-model="editForm.remark"
  76. class="form-input"
  77. placeholder="请输入备注"
  78. />
  79. </view>
  80. <!-- 字典项列表 -->
  81. <view class="items-section">
  82. <view class="items-header">
  83. <text class="items-title">字典项</text>
  84. </view>
  85. <view
  86. class="dict-item-row"
  87. v-for="(item, idx) in editForm.list"
  88. :key="idx"
  89. >
  90. <view class="item-fields">
  91. <view class="field-group">
  92. <text class="field-label">名称</text>
  93. <input
  94. v-model="item.name"
  95. class="field-input"
  96. placeholder="显示名称"
  97. @input="markDirty"
  98. />
  99. </view>
  100. <view class="field-group">
  101. <text class="field-label">码值</text>
  102. <input
  103. v-model="item.value"
  104. class="field-input"
  105. placeholder="数据码值"
  106. :disabled="!!item.id"
  107. @input="markDirty"
  108. />
  109. </view>
  110. <view class="field-group field-sm">
  111. <text class="field-label">权重</text>
  112. <input
  113. v-model.number="item.weight"
  114. class="field-input"
  115. type="number"
  116. placeholder="排序"
  117. @input="markDirty"
  118. />
  119. </view>
  120. </view>
  121. <view class="item-delete" @click="handleDeleteItem(idx)">
  122. <AppIcon name="trash" size="18" color="#FF4D4F" />
  123. </view>
  124. </view>
  125. <view class="add-item-btn" @click="handleAddItem">
  126. <AppIcon name="plus" size="16" color="#C6171E" />
  127. <text>新增字典项</text>
  128. </view>
  129. </view>
  130. <!-- 保存按钮 -->
  131. <view class="save-section" v-if="isDirty">
  132. <view class="btn-save" @click="handleSave">保存</view>
  133. </view>
  134. </view>
  135. </view>
  136. </view>
  137. </template>
  138. <script setup>
  139. import { ref, onMounted } from 'vue'
  140. import { getDataDictList, saveOrUpdateDict } from '../../api/dict.js'
  141. import { showToast } from '../../utils/index.js'
  142. const searchCode = ref('')
  143. const codeList = ref([])
  144. const allDictData = ref([])
  145. const loading = ref(true)
  146. const showDetail = ref(false)
  147. const isNew = ref(false)
  148. const isDirty = ref(false)
  149. const editForm = ref({
  150. code: '',
  151. remark: '',
  152. list: []
  153. })
  154. const goBack = () => {
  155. if (showDetail.value) {
  156. handleBackToList()
  157. } else {
  158. uni.navigateBack()
  159. }
  160. }
  161. const loadData = async (code = '') => {
  162. loading.value = true
  163. try {
  164. const params = {}
  165. if (code) params.code = code
  166. const res = await getDataDictList(params)
  167. if (res && res.code === 200) {
  168. const data = res.data
  169. allDictData.value = data || []
  170. buildCodeList(allDictData.value)
  171. }
  172. } catch (e) {
  173. console.error('加载字典列表失败:', e)
  174. showToast('加载字典数据失败')
  175. } finally {
  176. loading.value = false
  177. }
  178. }
  179. const buildCodeList = (data) => {
  180. const seen = new Set()
  181. const result = []
  182. data.forEach(item => {
  183. if (!seen.has(item.code)) {
  184. seen.add(item.code)
  185. const items = data.filter(k => k.code === item.code)
  186. result.push({
  187. code: item.code,
  188. remark: item.remark || '',
  189. count: items.length
  190. })
  191. }
  192. })
  193. codeList.value = result
  194. }
  195. const handleSearch = () => {
  196. loadData(searchCode.value.trim())
  197. }
  198. const handleReset = () => {
  199. searchCode.value = ''
  200. loadData()
  201. }
  202. const handleCreate = () => {
  203. isNew.value = true
  204. isDirty.value = true
  205. editForm.value = {
  206. code: '',
  207. remark: '',
  208. list: [{ name: '', value: '', weight: 0 }]
  209. }
  210. showDetail.value = true
  211. }
  212. const handleSelectDict = (item) => {
  213. isNew.value = false
  214. isDirty.value = false
  215. const items = allDictData.value
  216. .filter(k => k.code === item.code)
  217. .sort((a, b) => (a.weight || 0) - (b.weight || 0))
  218. editForm.value = {
  219. code: item.code,
  220. remark: item.remark || '',
  221. list: items.map(k => ({
  222. id: k.id,
  223. name: k.name || '',
  224. value: k.value || '',
  225. weight: k.weight || 0
  226. }))
  227. }
  228. showDetail.value = true
  229. }
  230. const handleBackToList = () => {
  231. showDetail.value = false
  232. loadData(searchCode.value.trim())
  233. }
  234. const handleAddItem = () => {
  235. isDirty.value = true
  236. editForm.value.list.push({
  237. name: '',
  238. value: '',
  239. weight: 0
  240. })
  241. }
  242. const handleDeleteItem = (idx) => {
  243. isDirty.value = true
  244. editForm.value.list.splice(idx, 1)
  245. }
  246. const markDirty = () => {
  247. isDirty.value = true
  248. }
  249. const handleSave = async () => {
  250. // 校验
  251. if (!editForm.value.code) {
  252. showToast('字典编码不能为空')
  253. return
  254. }
  255. const hasEmptyName = editForm.value.list.some(k => !k.name)
  256. if (hasEmptyName) {
  257. showToast('字典项名称不能为空')
  258. return
  259. }
  260. const hasEmptyValue = editForm.value.list.some(k => !k.value && k.value !== 0)
  261. if (hasEmptyValue) {
  262. showToast('字典项码值不能为空')
  263. return
  264. }
  265. const params = editForm.value.list.map(k => ({
  266. id: k.id,
  267. name: k.name,
  268. value: k.value,
  269. weight: k.weight,
  270. code: editForm.value.code,
  271. remark: editForm.value.remark
  272. }))
  273. try {
  274. const res = await saveOrUpdateDict(params)
  275. if (res && res.code === 200) {
  276. showToast('保存成功', 'success')
  277. isDirty.value = false
  278. handleBackToList()
  279. }
  280. } catch (e) {
  281. console.error('保存字典失败:', e)
  282. showToast(e.msg || '保存失败')
  283. }
  284. }
  285. onMounted(() => loadData())
  286. </script>
  287. <style scoped>
  288. .dict-container {
  289. min-height: 100vh;
  290. background-color: #F5F7FA;
  291. padding-bottom: 100rpx;
  292. }
  293. /* ===== 搜索栏 ===== */
  294. .search-bar {
  295. background-color: #FFFFFF;
  296. padding: 20rpx;
  297. margin: 20rpx;
  298. border-radius: 20rpx;
  299. box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  300. }
  301. .search-input {
  302. width: 100%;
  303. height: 76rpx;
  304. padding: 0 24rpx;
  305. border: 2rpx solid #E8E8E8;
  306. border-radius: 12rpx;
  307. font-size: 28rpx;
  308. color: #1A1A1A;
  309. background-color: #F5F7FA;
  310. box-sizing: border-box;
  311. margin-bottom: 16rpx;
  312. }
  313. .search-input:focus {
  314. border-color: #C6171E;
  315. background-color: #FFFFFF;
  316. }
  317. .search-actions {
  318. display: flex;
  319. gap: 16rpx;
  320. }
  321. .btn-search,
  322. .btn-reset,
  323. .btn-create {
  324. flex: 1;
  325. height: 64rpx;
  326. display: flex;
  327. align-items: center;
  328. justify-content: center;
  329. border-radius: 12rpx;
  330. font-size: 26rpx;
  331. font-weight: 500;
  332. transition: all 0.2s;
  333. }
  334. .btn-search {
  335. background-color: #C6171E;
  336. color: #FFFFFF;
  337. }
  338. .btn-reset {
  339. background-color: #F5F5F5;
  340. color: #666666;
  341. border: 2rpx solid #E8E8E8;
  342. }
  343. .btn-create {
  344. background-color: #C6171E;
  345. color: #FFFFFF;
  346. }
  347. .btn-search:active,
  348. .btn-create:active {
  349. transform: translateY(1px);
  350. opacity: 0.9;
  351. }
  352. .btn-reset:active {
  353. background-color: #E8E8E8;
  354. }
  355. /* ===== 编码列表 ===== */
  356. .code-list {
  357. padding: 0 20rpx;
  358. }
  359. .code-item {
  360. display: flex;
  361. justify-content: space-between;
  362. align-items: center;
  363. background-color: #FFFFFF;
  364. border-radius: 20rpx;
  365. padding: 28rpx;
  366. margin-bottom: 16rpx;
  367. box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  368. }
  369. .code-item:active {
  370. transform: translateY(-2rpx);
  371. }
  372. .item-main {
  373. flex: 1;
  374. }
  375. .item-code {
  376. font-size: 30rpx;
  377. font-weight: 600;
  378. color: #1A1A1A;
  379. display: block;
  380. margin-bottom: 4rpx;
  381. }
  382. .item-remark {
  383. font-size: 24rpx;
  384. color: #999999;
  385. }
  386. .item-meta {
  387. display: flex;
  388. align-items: center;
  389. gap: 12rpx;
  390. }
  391. .item-count {
  392. font-size: 24rpx;
  393. color: #C6171E;
  394. background-color: rgba(198, 23, 30, 0.08);
  395. padding: 6rpx 16rpx;
  396. border-radius: 20rpx;
  397. }
  398. /* ===== 编辑视图 ===== */
  399. .edit-header {
  400. background-color: #FFFFFF;
  401. padding: 20rpx 30rpx;
  402. margin-bottom: 20rpx;
  403. }
  404. .back-row {
  405. display: flex;
  406. align-items: center;
  407. gap: 16rpx;
  408. }
  409. .edit-title {
  410. font-size: 30rpx;
  411. font-weight: 600;
  412. color: #1A1A1A;
  413. }
  414. .edit-form {
  415. padding: 0 20rpx;
  416. }
  417. .form-row {
  418. background-color: #FFFFFF;
  419. border-radius: 16rpx;
  420. padding: 20rpx 24rpx;
  421. margin-bottom: 16rpx;
  422. }
  423. .form-label {
  424. font-size: 26rpx;
  425. color: #999999;
  426. margin-bottom: 12rpx;
  427. display: block;
  428. }
  429. .form-input {
  430. width: 100%;
  431. height: 72rpx;
  432. padding: 0 20rpx;
  433. border: 2rpx solid #E8E8E8;
  434. border-radius: 12rpx;
  435. font-size: 28rpx;
  436. color: #1A1A1A;
  437. background-color: #F5F7FA;
  438. box-sizing: border-box;
  439. }
  440. .form-input:focus {
  441. border-color: #C6171E;
  442. background-color: #FFFFFF;
  443. }
  444. .form-input:disabled {
  445. background-color: #F0F0F0;
  446. color: #999999;
  447. }
  448. /* ===== 字典项 ===== */
  449. .items-section {
  450. background-color: #FFFFFF;
  451. border-radius: 16rpx;
  452. padding: 24rpx;
  453. margin-bottom: 24rpx;
  454. }
  455. .items-header {
  456. margin-bottom: 20rpx;
  457. }
  458. .items-title {
  459. font-size: 28rpx;
  460. font-weight: 600;
  461. color: #1A1A1A;
  462. }
  463. .dict-item-row {
  464. display: flex;
  465. align-items: flex-start;
  466. padding: 16rpx 0;
  467. border-bottom: 2rpx solid #F5F5F5;
  468. gap: 12rpx;
  469. }
  470. .dict-item-row:last-child {
  471. border-bottom: none;
  472. }
  473. .item-fields {
  474. flex: 1;
  475. display: flex;
  476. flex-direction: column;
  477. gap: 12rpx;
  478. }
  479. .field-group {
  480. display: flex;
  481. align-items: center;
  482. gap: 12rpx;
  483. }
  484. .field-label {
  485. width: 64rpx;
  486. font-size: 24rpx;
  487. color: #999999;
  488. flex-shrink: 0;
  489. }
  490. .field-input {
  491. flex: 1;
  492. height: 60rpx;
  493. padding: 0 16rpx;
  494. border: 2rpx solid #E8E8E8;
  495. border-radius: 10rpx;
  496. font-size: 26rpx;
  497. color: #1A1A1A;
  498. background-color: #F5F7FA;
  499. box-sizing: border-box;
  500. }
  501. .field-input:focus {
  502. border-color: #C6171E;
  503. background-color: #FFFFFF;
  504. }
  505. .field-input:disabled {
  506. background-color: #F0F0F0;
  507. color: #999999;
  508. }
  509. .field-sm .field-label {
  510. width: 64rpx;
  511. }
  512. .item-delete {
  513. padding: 30rpx 8rpx 0;
  514. flex-shrink: 0;
  515. }
  516. /* ===== 新增按钮 & 保存 ===== */
  517. .add-item-btn {
  518. display: flex;
  519. align-items: center;
  520. justify-content: center;
  521. gap: 8rpx;
  522. height: 72rpx;
  523. margin-top: 16rpx;
  524. border: 2rpx dashed #C6171E;
  525. border-radius: 12rpx;
  526. color: #C6171E;
  527. font-size: 26rpx;
  528. }
  529. .add-item-btn:active {
  530. background-color: rgba(198, 23, 30, 0.05);
  531. }
  532. .save-section {
  533. margin: 24rpx 0 40rpx;
  534. }
  535. .btn-save {
  536. width: 100%;
  537. height: 88rpx;
  538. display: flex;
  539. align-items: center;
  540. justify-content: center;
  541. background-color: #C6171E;
  542. color: #FFFFFF;
  543. border-radius: 16rpx;
  544. font-size: 32rpx;
  545. font-weight: 600;
  546. box-shadow: 0 6rpx 20rpx rgba(198, 23, 30, 0.35);
  547. }
  548. .btn-save:active {
  549. transform: translateY(2rpx);
  550. box-shadow: 0 2rpx 10rpx rgba(198, 23, 30, 0.25);
  551. }
  552. /* ===== 空状态 ===== */
  553. .empty-btn {
  554. width: auto;
  555. padding: 16rpx 48rpx;
  556. margin-top: 24rpx;
  557. background-color: #C6171E;
  558. color: #FFFFFF;
  559. border-radius: 12rpx;
  560. font-size: 28rpx;
  561. font-weight: 500;
  562. }
  563. </style>