做字典管理
This commit is contained in:
Vendored
+2
@@ -18,6 +18,8 @@ declare module 'vue' {
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
# Pinia 字典管理系统使用指南
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ API 接口 (getDictItemsByCode) │
|
||||
│ /api/dict/items/code/{code} │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Pinia Store (useDictStore) │
|
||||
│ ✅ 自动缓存字典数据 │
|
||||
│ ✅ 避免重复请求 │
|
||||
│ ✅ 支持同步/异步访问 │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
↓ ↓
|
||||
┌────────┐ ┌──────────────┐
|
||||
│组件 │ │Composable │
|
||||
│直接用 │ │useDict Hook │
|
||||
└────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心文件说明
|
||||
|
||||
### 1. **Store**: `src/stores/dict.js`
|
||||
|
||||
字典数据的全局管理器
|
||||
|
||||
**主要方法**:
|
||||
```javascript
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
// ✅ 异步获取字典(推荐)
|
||||
const items = await dictStore.getDictItems('user_status')
|
||||
|
||||
// ✅ 同步获取字典(已缓存时)
|
||||
const items = dictStore.getDictItemsSync('user_status')
|
||||
|
||||
// ✅ 获取字典值对应的标签
|
||||
const label = dictStore.getDictLabel('user_status', 1)
|
||||
|
||||
// ✅ 预加载多个字典
|
||||
await dictStore.preloadDicts(['user_status', 'user_role'])
|
||||
|
||||
// ✅ 清空缓存
|
||||
dictStore.clearCache('user_status')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **常量**: `src/constants/dictCodes.js`
|
||||
|
||||
集中管理所有字典编码
|
||||
|
||||
**使用示例**:
|
||||
```javascript
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
// 好处:避免硬编码,IDE 有自动完成
|
||||
const items = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// 所有可用的编码:
|
||||
DICT_CODES.USER_STATUS // 用户状态
|
||||
DICT_CODES.USER_GENDER // 用户性别
|
||||
DICT_CODES.USER_ROLE // 用户角色
|
||||
DICT_CODES.DEPT_STATUS // 部门状态
|
||||
DICT_CODES.POSITION_STATUS // 职位状态
|
||||
// ... 更多编码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Composable**: `src/composables/useDict.js`
|
||||
|
||||
简化在组件中使用字典的 Hook
|
||||
|
||||
**基础用法**:
|
||||
```javascript
|
||||
import { useDictionary, useUserStatusDict } from '@/composables/useDict'
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
// 方式1:使用常量
|
||||
const { statusDict, loading } = useDictionary(DICT_CODES.USER_STATUS)
|
||||
|
||||
// 方式2:使用字符串
|
||||
const { dicts, loading } = useDictionary('user_status')
|
||||
|
||||
// 方式3:使用特化 Hook(推荐)
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1:在列表页加载字典
|
||||
|
||||
**文件**: `src/views/system/users/index.vue`
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const statusDict = ref([])
|
||||
|
||||
const fetchStatusDict = async () => {
|
||||
statusDict.value = await dictStore.getDictItems('user_status')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStatusDict()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 传给子组件 -->
|
||||
<UserEditDialog :status-dict="statusDict" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:在编辑对话框使用字典
|
||||
|
||||
**文件**: `src/views/system/users/components/UserEdit.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 状态选择 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
v-for="item in statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:快速使用 Composable Hook
|
||||
|
||||
最简单的方式,自动处理加载:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="loading">加载中...</p>
|
||||
<el-select v-else v-model="status">
|
||||
<el-option
|
||||
v-for="item in user_statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStatusDict } from '@/composables/useDict'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const status = ref('active')
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景4:预加载应用启动时需要的字典
|
||||
|
||||
**文件**: `src/main.js`
|
||||
|
||||
```javascript
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// 在应用启动后预加载常用字典
|
||||
const dictStore = useDictStore()
|
||||
await dictStore.preloadDicts([
|
||||
DICT_CODES.USER_STATUS,
|
||||
DICT_CODES.COMMON_STATUS,
|
||||
DICT_CODES.YES_NO,
|
||||
])
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字典数据结构
|
||||
|
||||
后端返回的字典数据结构:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dict_id": 1,
|
||||
"dict_value": "1",
|
||||
"dict_label": "启用",
|
||||
"dict_type": "user_status",
|
||||
"remarks": "用户启用状态",
|
||||
"remark": "用户启用状态"
|
||||
},
|
||||
{
|
||||
"dict_id": 2,
|
||||
"dict_value": "0",
|
||||
"dict_label": "禁用",
|
||||
"dict_type": "user_status",
|
||||
"remarks": "用户禁用状态",
|
||||
"remark": "用户禁用状态"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**关键字段**:
|
||||
- `dict_value`: 字典值(存储在数据库中)
|
||||
- `dict_label`: 字典标签(显示给用户)
|
||||
- `dict_type`: 字典类型编码(如 'user_status')
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **使用常量而不是硬编码字符串**
|
||||
```javascript
|
||||
// ✅ 好
|
||||
dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// ❌ 差
|
||||
dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
2. **在父组件加载,通过 props 传给子组件**
|
||||
```javascript
|
||||
// ✅ 父组件负责数据,子组件负责展示
|
||||
// index.vue
|
||||
const statusDict = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// UserEdit.vue
|
||||
const props = defineProps({ statusDict: Array })
|
||||
```
|
||||
|
||||
3. **用 Composable 简化组件逻辑**
|
||||
```javascript
|
||||
// ✅ 一行代码搞定
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
```
|
||||
|
||||
4. **预加载常用字典**
|
||||
```javascript
|
||||
// ✅ 应用启动时预加载,避免页面初始化时加载
|
||||
await dictStore.preloadDicts([...])
|
||||
```
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **不要在多个地方重复加载同一个字典**
|
||||
```javascript
|
||||
// ❌ 糟糕:重复加载
|
||||
// 页面A
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
|
||||
// 页面B(Store 会自动缓存,但代码看起来重复)
|
||||
const dict2 = await dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
2. **不要忘记处理加载状态**
|
||||
```javascript
|
||||
// ❌ 可能展示空白
|
||||
const { statusDict } = useUserStatusDict()
|
||||
|
||||
// ✅ 处理加载状态
|
||||
const { statusDict, loading } = useUserStatusDict()
|
||||
if (loading) { /* 显示加载中 */ }
|
||||
```
|
||||
|
||||
3. **不要混用不同的字典访问方式**
|
||||
```javascript
|
||||
// ❌ 混乱
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
const dict2 = dictStore.getDictItemsSync('user_role')
|
||||
|
||||
// ✅ 统一使用
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
const dict2 = await dictStore.getDictItems('user_role')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
| 优化项 | 说明 |
|
||||
|------|------|
|
||||
| **缓存** | Store 自动缓存,同一个字典只请求一次 |
|
||||
| **预加载** | 在路由切换前预加载需要的字典 |
|
||||
| **同步访问** | 已加载的字典可用 `getDictItemsSync` 同步获取 |
|
||||
| **避免重复** | 不要在多个组件重复请求同一个字典 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:状态选项为空
|
||||
|
||||
**原因**:字典未加载
|
||||
**解决**:
|
||||
```javascript
|
||||
// ❌ 错误:字典还未加载
|
||||
const statusDict = dictStore.getDictItemsSync('user_status') // 返回 []
|
||||
|
||||
// ✅ 正确:等待异步加载完成
|
||||
const statusDict = await dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
### 问题2:重复加载字典
|
||||
|
||||
**原因**:没有使用 Store 的缓存
|
||||
**解决**:
|
||||
```javascript
|
||||
// 所有调用都会自动使用缓存,只请求一次
|
||||
await dictStore.getDictItems('user_status') // 首次:发送请求
|
||||
await dictStore.getDictItems('user_status') // 第二次:返回缓存
|
||||
```
|
||||
|
||||
### 问题3:字典显示不对
|
||||
|
||||
**原因**:value 类型不匹配(如 1 vs "1")
|
||||
**解决**:
|
||||
```javascript
|
||||
// Store 会自动处理类型匹配
|
||||
const item = items.find(i =>
|
||||
String(i.dict_value) === String(value) || i.dict_value === value
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 集成检清表
|
||||
|
||||
- [ ] 创建 `src/stores/dict.js` - Store
|
||||
- [ ] 创建 `src/constants/dictCodes.js` - 常量
|
||||
- [ ] 创建 `src/composables/useDict.js` - Composable
|
||||
- [ ] 在 `index.vue` 中导入 `useDictStore`
|
||||
- [ ] 在 `UserEdit.vue` 中接收 `statusDict` props
|
||||
- [ ] 测试字典加载和显示
|
||||
- [ ] 验证缓存功能(打开浏览器 DevTools 检查 Network)
|
||||
- [ ] 预加载常用字典(可选)
|
||||
|
||||
---
|
||||
|
||||
## 相关文件修改
|
||||
|
||||
已修改的文件:
|
||||
- ✅ `src/stores/dict.js` - 新建
|
||||
- ✅ `src/constants/dictCodes.js` - 新建
|
||||
- ✅ `src/composables/useDict.js` - 新建
|
||||
- ✅ `src/views/system/users/index.vue` - 使用 `useDictStore`
|
||||
- ✅ `src/views/system/users/components/UserEdit.vue` - 导入字典库
|
||||
|
||||
+63
-54
@@ -1,114 +1,123 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取字典类型列表
|
||||
/**
|
||||
* 数据字典接口(平台端)
|
||||
*
|
||||
* 表:yz_platform_dict / yz_platform_dict_item
|
||||
* 平台端字典不做租户隔离,与租户端(/backend/dict)各自独立。
|
||||
*/
|
||||
const PREFIX = '/platform/dict'
|
||||
|
||||
/* ================= 字典列表(字典类型) ================= */
|
||||
|
||||
/** 字典列表(分页)params: { page, pageSize, keyword, status } */
|
||||
export function getDictTypes(params) {
|
||||
return request({
|
||||
url: '/platform/dict/types',
|
||||
url: `${PREFIX}/types`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典类型
|
||||
export function getDictTypeById(id) {
|
||||
/** 新增字典 */
|
||||
export function createDictType(data) {
|
||||
return request({
|
||||
url: `/platform/dict/types/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
export function addDictType(data) {
|
||||
return request({
|
||||
url: '/platform/dict/types',
|
||||
url: `${PREFIX}/types`,
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典类型
|
||||
/** 编辑字典(dict_code 不可改,由后端忽略) */
|
||||
export function updateDictType(id, data) {
|
||||
return request({
|
||||
url: `/platform/dict/types/${id}`,
|
||||
url: `${PREFIX}/types/${id}`,
|
||||
method: 'put',
|
||||
data: {
|
||||
...data,
|
||||
is_global: data.is_global !== undefined ? data.is_global : 0
|
||||
}
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
/** 删除字典(其下仍有字典项时后端会拒绝) */
|
||||
export function deleteDictType(id) {
|
||||
return request({
|
||||
url: `/platform/dict/types/${id}`,
|
||||
url: `${PREFIX}/types/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
/* ================= 字典项 ================= */
|
||||
|
||||
/** 字典项列表(分页)params: { dict_id, page, pageSize, keyword, status } */
|
||||
export function getDictItems(params) {
|
||||
return request({
|
||||
url: '/platform/dict/items',
|
||||
url: `${PREFIX}/items`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典项
|
||||
export function getDictItemById(id) {
|
||||
/** 新增字典项 */
|
||||
export function createDictItem(data) {
|
||||
return request({
|
||||
url: `/platform/dict/items/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
export function addDictItem(data) {
|
||||
return request({
|
||||
url: '/platform/dict/items',
|
||||
url: `${PREFIX}/items`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典项
|
||||
/** 编辑字典项 */
|
||||
export function updateDictItem(id, data) {
|
||||
return request({
|
||||
url: `/platform/dict/items/${id}`,
|
||||
url: `${PREFIX}/items/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
/** 启用 / 停用字典项 */
|
||||
export function toggleDictItemStatus(id, status) {
|
||||
return request({
|
||||
url: `${PREFIX}/items/${id}/status`,
|
||||
method: 'post',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除字典项 */
|
||||
export function deleteDictItem(id) {
|
||||
return request({
|
||||
url: `/platform/dict/items/${id}`,
|
||||
url: `${PREFIX}/items/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据字典编码获取字典项(用于业务查询)
|
||||
export function getDictItemsByCode(code, includeDisabled = false) {
|
||||
/* ================= 业务读取 ================= */
|
||||
|
||||
/** 按字典编码取「字典类型与字典项都启用」的选项,供业务页面下拉使用 */
|
||||
export function getDictOptions(code) {
|
||||
return request({
|
||||
url: `/platform/dict/items/code/${code}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
include_disabled: includeDisabled ? '1' : '0'
|
||||
}
|
||||
url: `${PREFIX}/options/${code}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 批量更新字典项排序
|
||||
export function batchUpdateDictItemSort(data) {
|
||||
/**
|
||||
* 批量取多个字典的选项(一次请求),供页面/应用启动时预加载。
|
||||
* 返回 data.dicts = { [dict_code]: 字典项[] },不存在的编码返回空数组。
|
||||
* @param {string[]} codes 字典编码数组
|
||||
*/
|
||||
export function getDictOptionsBatch(codes) {
|
||||
return request({
|
||||
url: '/platform/dict/items/sort',
|
||||
method: 'put',
|
||||
data
|
||||
url: `${PREFIX}/options`,
|
||||
method: 'post',
|
||||
data: { codes }
|
||||
})
|
||||
}
|
||||
|
||||
/** 一次取回全部「启用」字典的字典项,供登录后预热缓存 */
|
||||
export function getAllDictOptions() {
|
||||
return request({
|
||||
url: `${PREFIX}/options`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { computed } from 'vue'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
/**
|
||||
* 字典 Composable:把「加载 + 取值 + 文案/标签色查询」收敛成一行调用。
|
||||
*
|
||||
* 用法:
|
||||
* const { items, options, label, tagType } = useDictionary('payment_channel')
|
||||
* // 模板:<el-option v-for="o in options" :label="o.label" :value="o.value" />
|
||||
* // <el-tag :type="tagType(row.status)">{{ label(row.status) }}</el-tag>
|
||||
*
|
||||
* @param {string} code 字典编码(库里真实的 dict_code)
|
||||
*/
|
||||
export function useDictionary(code) {
|
||||
const dictStore = useDictStore()
|
||||
// setup 阶段同步触发加载:缓存命中时不发请求,首次会自动拉取并响应式刷新
|
||||
dictStore.ensure(code)
|
||||
|
||||
/** 字典项原始数组(含 item_value / item_label / tag_type 等) */
|
||||
const items = computed(() => dictStore.getDictItemsSync(code))
|
||||
/** el-select / el-option 直接可用的 { label, value } 列表 */
|
||||
const options = computed(() => items.value.map((it) => ({
|
||||
label: it.item_label,
|
||||
value: it.item_value,
|
||||
tag_type: it.tag_type
|
||||
})))
|
||||
|
||||
return {
|
||||
items,
|
||||
options,
|
||||
label: (value, fallback) => dictStore.getDictLabel(code, value, fallback),
|
||||
tagType: (value, fallback) => dictStore.getDictTagType(code, value, fallback),
|
||||
find: (value) => dictStore.getDictItem(code, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只要「值 → 文案」或「值 → 标签色」时的轻量用法。
|
||||
* const channelLabel = useDictLabel('payment_channel')
|
||||
*/
|
||||
export function useDictLabel(code) {
|
||||
const dictStore = useDictStore()
|
||||
dictStore.ensure(code)
|
||||
return (value, fallback) => dictStore.getDictLabel(code, value, fallback)
|
||||
}
|
||||
|
||||
export function useDictTagType(code) {
|
||||
const dictStore = useDictStore()
|
||||
dictStore.ensure(code)
|
||||
return (value, fallback) => dictStore.getDictTagType(code, value, fallback)
|
||||
}
|
||||
|
||||
/** 预加载多个字典(页面 onMounted 里调用,一次批量请求) */
|
||||
export function useDictPreload(codes) {
|
||||
const dictStore = useDictStore()
|
||||
return () => dictStore.preloadDicts(codes)
|
||||
}
|
||||
|
||||
export default useDictionary
|
||||
@@ -0,0 +1,187 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getDictOptions, getDictOptionsBatch, getAllDictOptions } from '@/api/dict'
|
||||
|
||||
/**
|
||||
* 字典 Store(平台端)
|
||||
*
|
||||
* 全部字典数据都来自数据库,前端不维护编码清单:
|
||||
* - 登录后调用一次 loadAll(),把库里所有「启用」字典读进缓存;
|
||||
* - 页面按库里真实的 dict_code 取值即可(字符串直接写,无需常量文件);
|
||||
* - 之后在字典管理页新增的字典,第一次被引用时会按需自动加载。
|
||||
*
|
||||
* 用法:
|
||||
* const dictStore = useDictStore()
|
||||
* dictStore.getDictLabel('payment_order_status', row.status) // 文案
|
||||
* dictStore.getDictTagType('payment_order_status', row.status) // 标签色
|
||||
* dictStore.getSelectOptions('payment_channel') // 下拉选项
|
||||
*/
|
||||
export const useDictStore = defineStore('dict', () => {
|
||||
/** dict_code -> 字典项数组(渲染依赖它,必须是响应式的) */
|
||||
const dictMap = ref({})
|
||||
|
||||
/** 已加载过的编码(仅用于去重,不参与渲染) */
|
||||
let loaded = {}
|
||||
/** 正在请求中的编码 -> Promise(并发调用复用同一个请求) */
|
||||
const pending = new Map()
|
||||
/** 是否已完成全量预热 */
|
||||
let allLoaded = false
|
||||
let allPending = null
|
||||
|
||||
function setItems(code, list) {
|
||||
dictMap.value = { ...dictMap.value, [code]: Array.isArray(list) ? list : [] }
|
||||
}
|
||||
|
||||
function itemsOf(code) {
|
||||
return dictMap.value[code] || []
|
||||
}
|
||||
|
||||
/** 把入参规范成去重后的编码数组 */
|
||||
function normalizeCodes(codes) {
|
||||
return [...new Set((codes || []).map((c) => String(c || '').trim()).filter(Boolean))]
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载字典(带缓存与并发去重)。
|
||||
* 请求失败也会标记为已加载,避免页面反复重试;需要重试请先 clearCache。
|
||||
*/
|
||||
async function load(codes) {
|
||||
const list = normalizeCodes(codes)
|
||||
const missing = list.filter((c) => !loaded[c] && !pending.has(c))
|
||||
|
||||
if (missing.length) {
|
||||
const promise = (async () => {
|
||||
if (missing.length === 1) {
|
||||
// 单个字典走专用接口
|
||||
const code = missing[0]
|
||||
try {
|
||||
const res = await getDictOptions(code)
|
||||
setItems(code, res?.code === 200 ? res.data?.list || [] : [])
|
||||
} catch {
|
||||
setItems(code, [])
|
||||
}
|
||||
} else {
|
||||
// 多个字典合并成一次请求
|
||||
try {
|
||||
const res = await getDictOptionsBatch(missing)
|
||||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||||
missing.forEach((code) => setItems(code, dicts[code] || []))
|
||||
} catch {
|
||||
missing.forEach((code) => setItems(code, []))
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
missing.forEach((code) => {
|
||||
loaded[code] = true
|
||||
pending.delete(code)
|
||||
})
|
||||
})
|
||||
|
||||
missing.forEach((code) => pending.set(code, promise))
|
||||
}
|
||||
|
||||
await Promise.all(list.map((c) => pending.get(c) || Promise.resolve()))
|
||||
return list.flatMap((c) => itemsOf(c))
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量预热:一次请求把库里所有启用字典读进缓存(登录后调用一次即可)。
|
||||
* 之后再新增的字典仍会走 load() 按需加载。
|
||||
*/
|
||||
function loadAll() {
|
||||
if (allLoaded) return Promise.resolve()
|
||||
if (allPending) return allPending
|
||||
|
||||
allPending = (async () => {
|
||||
try {
|
||||
const res = await getAllDictOptions()
|
||||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||||
const next = { ...dictMap.value }
|
||||
Object.entries(dicts).forEach(([code, list]) => {
|
||||
loaded[code] = true
|
||||
next[code] = Array.isArray(list) ? list : []
|
||||
})
|
||||
dictMap.value = next
|
||||
} catch {
|
||||
// 预热失败不影响使用:后续按需加载
|
||||
} finally {
|
||||
allLoaded = true
|
||||
allPending = null
|
||||
}
|
||||
})()
|
||||
return allPending
|
||||
}
|
||||
|
||||
/** 同步触发加载(不等待),用于组件 setup/onMounted 里“先渲染后填充” */
|
||||
function ensure(code) {
|
||||
const list = normalizeCodes([code])
|
||||
if (list.some((c) => !loaded[c] && !pending.has(c))) {
|
||||
load(list)
|
||||
}
|
||||
}
|
||||
|
||||
/** 同步取字典项;未加载时顺带触发加载,拿到后由响应式自动刷新 */
|
||||
function getDictItemsSync(code) {
|
||||
ensure(code)
|
||||
return itemsOf(code)
|
||||
}
|
||||
|
||||
/** 值 → 字典项(值按字符串比较,兼容 1 / '1' 混用) */
|
||||
function getDictItem(code, value) {
|
||||
if (value === undefined || value === null || value === '') return null
|
||||
const key = String(value)
|
||||
return itemsOf(code).find((it) => String(it.item_value) === key) || null
|
||||
}
|
||||
|
||||
/** 值 → 文案;取不到时返回 fallback,未传则原样返回取值 */
|
||||
function getDictLabel(code, value, fallback) {
|
||||
const hit = getDictItem(code, value)
|
||||
if (hit) return hit.item_label
|
||||
if (fallback !== undefined) return fallback
|
||||
return value === undefined || value === null ? '' : String(value)
|
||||
}
|
||||
|
||||
/** 值 → el-tag 的 type;取不到时返回 fallback(默认 info) */
|
||||
function getDictTagType(code, value, fallback = 'info') {
|
||||
const hit = getDictItem(code, value)
|
||||
return hit && hit.tag_type ? hit.tag_type : fallback
|
||||
}
|
||||
|
||||
/** 值 → 下拉选项 { label, value },供 el-select / el-option 直接使用 */
|
||||
function getSelectOptions(code) {
|
||||
return getDictItemsSync(code).map((it) => ({ label: it.item_label, value: it.item_value, tag_type: it.tag_type }))
|
||||
}
|
||||
|
||||
/** 预加载多个字典(一次批量请求) */
|
||||
function preloadDicts(codes) {
|
||||
return load(codes)
|
||||
}
|
||||
|
||||
/** 清空缓存:传编码清单个,不传清全部(同时允许重新全量预热) */
|
||||
function clearCache(code) {
|
||||
if (code) {
|
||||
delete loaded[code]
|
||||
const next = { ...dictMap.value }
|
||||
delete next[code]
|
||||
dictMap.value = next
|
||||
return
|
||||
}
|
||||
loaded = {}
|
||||
dictMap.value = {}
|
||||
allLoaded = false
|
||||
}
|
||||
|
||||
return {
|
||||
dictMap,
|
||||
ensure,
|
||||
loadAll,
|
||||
getDictItems: load,
|
||||
getDictItemsSync,
|
||||
getDictItem,
|
||||
getDictLabel,
|
||||
getDictTagType,
|
||||
getSelectOptions,
|
||||
preloadDicts,
|
||||
clearCache
|
||||
}
|
||||
})
|
||||
@@ -53,3 +53,4 @@ export function formatDate(value: any): string {
|
||||
export function formatDateOnly(value: any): string {
|
||||
return formatDate(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import CommonAside from '@/components/CommonAside.vue';
|
||||
import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
|
||||
import { More, Close, CircleClose, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const dictStore = useDictStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const asideRef = ref(null);
|
||||
@@ -39,6 +41,9 @@ function restoreTabFromRoute() {
|
||||
|
||||
// 组件挂载后,根据当前路由恢复 tab
|
||||
onMounted(() => {
|
||||
// 进入后台后把库里所有字典预热到缓存(失败不影响按需加载)
|
||||
dictStore.loadAll();
|
||||
|
||||
// 等待路由完全加载后再恢复
|
||||
nextTick(() => {
|
||||
// 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复
|
||||
|
||||
@@ -1,335 +1,173 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-input
|
||||
:value="dictTypeDisplayName"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">当前字典类型:{{ dictTypeDisplayName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级字典项" prop="parent_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.parent_id"
|
||||
placeholder="选择父级字典项(可选)"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in parentDictItems"
|
||||
:key="item.id"
|
||||
:label="item.dict_label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
>
|
||||
<template #append>
|
||||
<el-color-picker
|
||||
v-model="dictItemForm.color"
|
||||
:predefine="predefineColors"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip">支持十六进制颜色值(如:#FF0000)或颜色名称(如:red)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标(如:✓、✗、⭐)"
|
||||
/>
|
||||
<div class="form-tip">支持emoji或图标字符</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictItem, updateDictItem, getDictItems } from '@/api/dict';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictItem?: any;
|
||||
dictTypeId?: number;
|
||||
dictTypeName?: string;
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? '编辑字典项' : '新增字典项'"
|
||||
width="520px"
|
||||
@update:model-value="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="字典项名称" prop="item_label">
|
||||
<el-input v-model="form.item_label" placeholder="如:原材料供应商" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典项值" prop="item_value">
|
||||
<el-input
|
||||
v-model="form.item_value"
|
||||
:disabled="isEdit"
|
||||
placeholder="业务入库值,如:1(同一字典下不可重复,保存后不可修改)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签色" prop="tag_type">
|
||||
<el-select v-model="form.tag_type" clearable placeholder="留空则不渲染标签" style="width: 100%">
|
||||
<el-option v-for="t in TAG_TYPES" :key="t" :label="t" :value="t">
|
||||
<el-tag :type="t" size="small">{{ t }}</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认选中" prop="is_default">
|
||||
<el-radio-group v-model="form.is_default">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createDictItem, updateDictItem } from '@/api/dict'
|
||||
|
||||
/** 与 el-tag 的 type 对齐,存入 yz_*_dict_item.tag_type */
|
||||
const TAG_TYPES = ['primary', 'success', 'warning', 'danger', 'info']
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
/** 所属字典ID */
|
||||
dictId: { type: [Number, String], default: null },
|
||||
/** 传入表示编辑,null 表示新增 */
|
||||
dictItem: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
item_label: '',
|
||||
item_value: '',
|
||||
tag_type: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
is_default: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!form.id)
|
||||
|
||||
const rules = {
|
||||
item_label: [
|
||||
{ required: true, message: '请输入字典项名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
item_value: [
|
||||
{ required: true, message: '请输入字典项值', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) initForm()
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: undefined,
|
||||
dictTypeName: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictItemFormRef = ref<FormInstance>();
|
||||
const parentDictItems = ref<any[]>([]);
|
||||
|
||||
// 预定义颜色
|
||||
const predefineColors = [
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#ff0000',
|
||||
'#00ff00',
|
||||
'#0000ff',
|
||||
];
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id);
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: 0,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 获取父级字典项列表(排除自己)
|
||||
async function fetchParentDictItems() {
|
||||
if (!props.dictTypeId) return;
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: props.dictTypeId });
|
||||
if (res.success && res.data) {
|
||||
// 如果是编辑模式,排除当前字典项
|
||||
if (isEditing.value && props.dictItem) {
|
||||
parentDictItems.value = res.data.filter(
|
||||
(item: any) => item.id !== props.dictItem.id
|
||||
);
|
||||
} else {
|
||||
parentDictItems.value = res.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取父级字典项失败:', error);
|
||||
}
|
||||
)
|
||||
|
||||
function initForm() {
|
||||
const d = props.dictItem
|
||||
form.id = d?.id || null
|
||||
form.item_label = d?.item_label || ''
|
||||
form.item_value = d?.item_value || ''
|
||||
form.tag_type = d?.tag_type || ''
|
||||
form.sort = d?.sort ?? 0
|
||||
form.status = d?.status ?? 1
|
||||
form.is_default = d?.is_default ?? 0
|
||||
form.remark = d?.remark || ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算字典类型显示名称
|
||||
const dictTypeDisplayName = computed(() => {
|
||||
if (props.dictTypeName) {
|
||||
return props.dictTypeName;
|
||||
}
|
||||
if (props.dictTypeId) {
|
||||
return `字典类型 ID: ${props.dictTypeId}`;
|
||||
}
|
||||
return '未知字典类型';
|
||||
});
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null;
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = newDictItem.dict_label || '';
|
||||
dictItemForm.dict_value = newDictItem.dict_value || '';
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0;
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1;
|
||||
dictItemForm.sort = newDictItem.sort || 0;
|
||||
dictItemForm.color = newDictItem.color || '';
|
||||
dictItemForm.icon = newDictItem.icon || '';
|
||||
dictItemForm.remark = newDictItem.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听弹窗打开,获取父级字典项
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
if (props.dictTypeId) {
|
||||
dictItemForm.dict_type_id = props.dictTypeId;
|
||||
}
|
||||
fetchParentDictItems();
|
||||
}
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null;
|
||||
dictItemForm.dict_type_id = props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = '';
|
||||
dictItemForm.dict_value = '';
|
||||
dictItemForm.parent_id = 0;
|
||||
dictItemForm.status = 1;
|
||||
dictItemForm.sort = 0;
|
||||
dictItemForm.color = '';
|
||||
dictItemForm.icon = '';
|
||||
dictItemForm.remark = '';
|
||||
dictItemFormRef.value?.clearValidate();
|
||||
if (!isEdit.value && !props.dictId) {
|
||||
ElMessage.error('未选择字典')
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
|
||||
const payload = {
|
||||
item_label: form.item_label,
|
||||
tag_type: form.tag_type || '',
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
is_default: form.is_default,
|
||||
remark: form.remark
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dictItemForm.dict_type_id) {
|
||||
ElMessage.error('字典类型ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id || 0,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictItem(dictItemForm.id!, submitData);
|
||||
ElMessage.success('字典项更新成功');
|
||||
} else {
|
||||
await addDictItem(submitData);
|
||||
ElMessage.success('字典项添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
if (isEdit.value) {
|
||||
// 字典项值不可改,避免历史数据里的存量值对不上
|
||||
payload.id = form.id
|
||||
} else {
|
||||
payload.dict_id = props.dictId
|
||||
payload.item_value = form.item_value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = isEdit.value
|
||||
? await updateDictItem(form.id, payload)
|
||||
: await createDictItem(payload)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success', res.data || null)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.msg || e?.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.dict_type_id"
|
||||
placeholder="请选择字典类型"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="isEditing || !!dictTypeId"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="父级ID" prop="parent_id">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.parent_id"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="0表示顶级"
|
||||
/>
|
||||
<div class="form-tip">0表示顶级字典项</div>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色标记" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
maxlength="20"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的颜色标记</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标名称或类名"
|
||||
maxlength="50"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的图标</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getDictTypes, addDictItem, updateDictItem } from '@/api/dict'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dictItem?: any
|
||||
dictTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const dictItemFormRef = ref<FormInstance>()
|
||||
const dictTypes = ref<any[]>([])
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id)
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: null as number | null,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_type_id: [
|
||||
{ required: true, message: '请选择字典类型', trigger: 'change' },
|
||||
],
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
parent_id: [
|
||||
{ type: 'number', min: 0, message: '父级ID必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || null
|
||||
dictItemForm.dict_label = newDictItem.dict_label || ''
|
||||
dictItemForm.dict_value = newDictItem.dict_value || ''
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1
|
||||
dictItemForm.sort = newDictItem.sort || 0
|
||||
dictItemForm.color = newDictItem.color || ''
|
||||
dictItemForm.icon = newDictItem.icon || ''
|
||||
dictItemForm.remark = newDictItem.remark || ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听 dictTypeId 变化
|
||||
watch(
|
||||
() => props.dictTypeId,
|
||||
(newTypeId) => {
|
||||
if (newTypeId && !isEditing.value) {
|
||||
dictItemForm.dict_type_id = newTypeId
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null
|
||||
dictItemForm.dict_type_id = props.dictTypeId || null
|
||||
dictItemForm.dict_label = ''
|
||||
dictItemForm.dict_value = ''
|
||||
dictItemForm.parent_id = 0
|
||||
dictItemForm.status = 1
|
||||
dictItemForm.sort = 0
|
||||
dictItemForm.color = ''
|
||||
dictItemForm.icon = ''
|
||||
dictItemForm.remark = ''
|
||||
dictItemFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
}
|
||||
|
||||
if (isEditing.value) {
|
||||
const res = await updateDictItem(dictItemForm.id!, submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项更新成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '更新失败,请重试')
|
||||
}
|
||||
} else {
|
||||
const res = await addDictItem(submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '添加失败,请重试')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
<template>
|
||||
<div class="dict-item-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select
|
||||
v-model="selectedDictTypeId"
|
||||
placeholder="选择字典类型"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 250px"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典项标签或值"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!selectedDictTypeId"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<el-alert
|
||||
v-if="!selectedDictTypeId"
|
||||
title="请先选择字典类型"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictItems">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典项列表 -->
|
||||
<div v-else-if="selectedDictTypeId">
|
||||
<el-table
|
||||
:data="filteredDictItems"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_label" label="字典标签" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_value" label="字典值" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_value }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="color" label="颜色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.color" class="color-display">
|
||||
<span
|
||||
class="color-dot"
|
||||
:style="{ backgroundColor: row.color }"
|
||||
></span>
|
||||
<span>{{ row.color }}</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.icon">{{ row.icon }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictItemEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:dict-item="currentDictItem"
|
||||
:dict-type-id="selectedDictTypeId"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
getDictItems,
|
||||
deleteDictItem,
|
||||
} from '@/api/dict'
|
||||
import DictItemEditDialog from './DictItemEditDialog.vue'
|
||||
|
||||
interface Props {
|
||||
selectedTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const dictItems = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictItem = ref<any>(null)
|
||||
|
||||
// 过滤后的字典项
|
||||
const filteredDictItems = computed(() => {
|
||||
let result = dictItems.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(item) =>
|
||||
(item.dict_label && item.dict_label.toLowerCase().includes(keyword)) ||
|
||||
(item.dict_value && item.dict_value.toLowerCase().includes(keyword)) ||
|
||||
(item.remark && item.remark.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 监听外部传入的selectedTypeId
|
||||
watch(
|
||||
() => props.selectedTypeId,
|
||||
(newId) => {
|
||||
if (newId) {
|
||||
selectedDictTypeId.value = newId
|
||||
fetchDictItems()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典类型列表(用于下拉选择)
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
async function fetchDictItems() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
dictItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: selectedDictTypeId.value })
|
||||
if (res.success === true && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictItems.value = res
|
||||
} else if (res && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型改变
|
||||
const handleTypeChange = () => {
|
||||
fetchDictItems()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 设置字典类型ID(供外部调用)
|
||||
function setTypeId(typeId: number) {
|
||||
selectedDictTypeId.value = typeId
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
function handleAdd() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
ElMessage.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
currentDictItem.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典项
|
||||
function handleEdit(row: any) {
|
||||
currentDictItem.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典项「${row.dict_label}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
setTypeId,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-item-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,241 +1,145 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典类型' : '添加字典类型'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictTypeForm"
|
||||
:rules="formRules"
|
||||
ref="dictTypeFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_code"
|
||||
placeholder="请输入字典编码(如:user_status)"
|
||||
:disabled="isEditing"
|
||||
/>
|
||||
<div class="form-tip">只能包含字母、数字或下划线,且必须唯一</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_name"
|
||||
placeholder="请输入字典名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictTypeForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="shouldShowGlobalOption" label="全局展示" prop="is_global">
|
||||
<el-radio-group v-model="dictTypeForm.is_global">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">是:所有租户可见;否:仅租户内部可见</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictTypeForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictTypeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictType, updateDictType, getDictTypes } from '@/api/dict';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictType?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictType: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictTypeFormRef = ref<FormInstance>();
|
||||
const authStore = useAuthStore();
|
||||
// 已移除父级选择
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictType && props.dictType.id);
|
||||
});
|
||||
|
||||
// 判断是否应该显示全局展示选项(只有 system_admin 和 admin 角色才能看到)
|
||||
const shouldShowGlobalOption = computed(() => {
|
||||
const roleCode = authStore.user.role_code;
|
||||
return roleCode === 'system_admin' || roleCode === 'admin';
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictTypeForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
status: 1,
|
||||
is_global: 0,
|
||||
sort: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '只能包含字母、数字或下划线',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 已移除父级选择相关数据获取
|
||||
|
||||
// 监听 dictType 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictType,
|
||||
(newDictType) => {
|
||||
if (newDictType) {
|
||||
dictTypeForm.id = newDictType.id || null;
|
||||
dictTypeForm.dict_code = newDictType.dict_code || '';
|
||||
dictTypeForm.dict_name = newDictType.dict_name || '';
|
||||
dictTypeForm.status = newDictType.status !== undefined ? newDictType.status : 1;
|
||||
dictTypeForm.is_global = newDictType.is_global !== undefined ? newDictType.is_global : 0;
|
||||
dictTypeForm.sort = newDictType.sort || 0;
|
||||
dictTypeForm.remark = newDictType.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? '编辑字典' : '新增字典'"
|
||||
width="520px"
|
||||
@update:model-value="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input v-model="form.dict_name" placeholder="如:供应商类型" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="form.dict_code"
|
||||
:disabled="isEdit"
|
||||
placeholder="如:supplier_type(业务按编码取选项,保存后不可修改)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createDictType, updateDictType } from '@/api/dict'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
/** 传入表示编辑,null 表示新增 */
|
||||
dictType: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
dict_name: '',
|
||||
dict_code: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!form.id)
|
||||
|
||||
const rules = {
|
||||
dict_name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: '编码以字母开头,只能包含字母、数字与下划线',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 已移除父级选择监听
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictTypeForm.id = null;
|
||||
dictTypeForm.dict_code = '';
|
||||
dictTypeForm.dict_name = '';
|
||||
dictTypeForm.status = 1;
|
||||
dictTypeForm.is_global = 0;
|
||||
dictTypeForm.sort = 0;
|
||||
dictTypeForm.remark = '';
|
||||
dictTypeFormRef.value?.clearValidate();
|
||||
{ max: 100, message: '长度不能超过 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) initForm()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
)
|
||||
|
||||
function initForm() {
|
||||
const d = props.dictType
|
||||
form.id = d?.id || null
|
||||
form.dict_name = d?.dict_name || ''
|
||||
form.dict_code = d?.dict_code || ''
|
||||
form.sort = d?.sort ?? 0
|
||||
form.status = d?.status ?? 1
|
||||
form.remark = d?.remark || ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictTypeFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictTypeFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_code: dictTypeForm.dict_code,
|
||||
dict_name: dictTypeForm.dict_name,
|
||||
status: dictTypeForm.status,
|
||||
is_global: dictTypeForm.is_global,
|
||||
sort: dictTypeForm.sort,
|
||||
remark: dictTypeForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictType(dictTypeForm.id!, submitData);
|
||||
ElMessage.success('字典类型更新成功');
|
||||
} else {
|
||||
await addDictType(submitData);
|
||||
ElMessage.success('字典类型添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
|
||||
const payload = {
|
||||
dict_name: form.dict_name,
|
||||
sort: form.sort,
|
||||
status: form.status,
|
||||
remark: form.remark
|
||||
}
|
||||
// 新增时才提交编码;编辑时编码不可改,由后端忽略
|
||||
if (!isEdit.value) payload.dict_code = form.dict_code
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = isEdit.value
|
||||
? await updateDictType(form.id, payload)
|
||||
: await createDictType(payload)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '新增成功')
|
||||
emit('update:visible', false)
|
||||
emit('success', isEdit.value ? { id: form.id } : res.data || null)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.response?.data?.msg || e?.message || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 无需加载父级数据
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div class="dict-type-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictTypes">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型列表 -->
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tid" label="租户ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.tid || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click.stop="handleViewItems(row)">
|
||||
<el-icon><Menu /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictTypeEdit
|
||||
v-model="editDialogVisible"
|
||||
:dict-type="currentDictType"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
deleteDictType,
|
||||
} from '@/api/dict'
|
||||
import DictTypeEdit from './DictTypeEdit.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-type': [typeId: number]
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictType = ref<any>(null)
|
||||
|
||||
// 过滤后的字典类型
|
||||
const filteredDictTypes = computed(() => {
|
||||
let result = dictTypes.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(type) =>
|
||||
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
|
||||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((type) => type.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
function handleAdd() {
|
||||
currentDictType.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典类型
|
||||
function handleEdit(row: any) {
|
||||
currentDictType.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
emit('select-type', row.id)
|
||||
}
|
||||
|
||||
// 行点击
|
||||
function handleRowClick(row: any) {
|
||||
// 可以在这里处理行点击事件
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-type-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,368 +1,517 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>字典管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 内容区域:根据当前视图切换 -->
|
||||
<div v-if="currentView === 'types'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAddType">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="typeSearchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="dictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="typeLoading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.parent_id > 0">{{ row.parent_id }}</span>
|
||||
<span v-else style="color: #999">-</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_global" label="全局展示" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_global === 1 ? 'success' : 'warning'" size="small">
|
||||
{{ row.is_global === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="sort" label="排序" width="80" align="center" /> -->
|
||||
<!-- <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip /> -->
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditType(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleViewItems(row)">
|
||||
<el-icon><List /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="totalCount"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentView === 'items'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<el-button @click="goBackToTypes">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<span v-if="selectedDictType">
|
||||
当前字典类型:{{ selectedDictType.dict_name }}({{ selectedDictType.dict_code }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="dict-page">
|
||||
<!-- ================= 一级:字典列表 ================= -->
|
||||
<template v-if="view === 'list'">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>数据字典</h2>
|
||||
<p>维护系统下拉选项:先建字典,再进「字典内容」维护字典项。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="openTypeCreate">新增字典</el-button>
|
||||
<el-button :icon="Refresh" @click="loadTypes">刷新</el-button>
|
||||
</div>
|
||||
<DictItemList :selected-type-id="selectedDictTypeId || undefined" @refresh="refreshTypesSilently" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型编辑对话框 -->
|
||||
<DictTypeEdit
|
||||
v-model="typeDialogVisible"
|
||||
:dict-type="currentDictType"
|
||||
@success="handleTypeSuccess"
|
||||
<div class="dict-panel">
|
||||
<div class="panel-header">
|
||||
<el-input
|
||||
v-model="typeKeyword"
|
||||
class="panel-search"
|
||||
placeholder="搜索字典名称 / 编码"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
@keyup.enter="handleTypeSearch"
|
||||
@clear="handleTypeSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="types"
|
||||
class="dict-table"
|
||||
stripe
|
||||
v-loading="typeLoading"
|
||||
>
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.dict_name }}</span>
|
||||
<!-- 全局内置字典:所有租户共享、只读(平台端无此字段,自动不显示) -->
|
||||
<el-tag v-if="row.is_global" class="inline-tag" size="small" type="warning" effect="plain">
|
||||
内置
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="说明" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="175" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openItems(row)">字典内容</el-button>
|
||||
<el-button link type="primary" size="small" :disabled="row.is_global" @click="openTypeEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" :disabled="row.is_global" @click="handleTypeDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="panel-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="typePage.page"
|
||||
v-model:page-size="typePage.pageSize"
|
||||
:total="typePage.total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadTypes"
|
||||
@current-change="loadTypes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================= 二级:字典项列表 ================= -->
|
||||
<template v-else>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>
|
||||
<a class="crumb-link" @click="goBackToList">数据字典</a>
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>字典项</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<h2 class="sub-title">
|
||||
{{ currentType?.dict_name }}
|
||||
<el-tag size="small" effect="plain">{{ currentType?.dict_code }}</el-tag>
|
||||
<el-tag v-if="isGlobalDict" size="small" type="warning" effect="plain">内置只读</el-tag>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="ArrowLeft" @click="goBackToList">返回</el-button>
|
||||
<el-button type="primary" :icon="Plus" :disabled="isGlobalDict" @click="openItemCreate">
|
||||
新增字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dict-panel">
|
||||
<div class="panel-header">
|
||||
<el-input
|
||||
v-model="itemKeyword"
|
||||
class="panel-search"
|
||||
placeholder="搜索字典项名称 / 值"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
@keyup.enter="handleItemSearch"
|
||||
@clear="handleItemSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="items"
|
||||
class="dict-table"
|
||||
stripe
|
||||
border
|
||||
v-loading="itemLoading"
|
||||
>
|
||||
<el-table-column prop="item_label" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="item_value" label="值" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ row.item_value }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tag_type" label="标签色" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.tag_type" :type="row.tag_type" size="small">{{ row.tag_type }}</el-tag>
|
||||
<span v-else class="muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="is_default" label="默认" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default === 1" type="success" size="small">默认</el-tag>
|
||||
<span v-else class="muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" :disabled="isGlobalDict" @click="openItemEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" :disabled="isGlobalDict" @click="handleItemToggle(row)">
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" :disabled="isGlobalDict" @click="handleItemDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="panel-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="itemPage.page"
|
||||
v-model:page-size="itemPage.pageSize"
|
||||
:total="itemPage.total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadItems"
|
||||
@current-change="loadItems"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DictTypeEdit v-model="typeDialogVisible" :dict-type="currentDictType" @success="handleTypeSaved" />
|
||||
|
||||
<DictItemEdit
|
||||
v-model="itemDialogVisible"
|
||||
:dict-id="currentType?.id"
|
||||
:dict-item="currentDictItem"
|
||||
@success="loadItems"
|
||||
/>
|
||||
|
||||
<!-- 字典项独立页面内操作通过 DictItemList 完成 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
<script setup>
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Refresh,
|
||||
Search,
|
||||
List,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { Plus, Search, Refresh, ArrowLeft } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
deleteDictType,
|
||||
getDictItems,
|
||||
deleteDictItem,
|
||||
toggleDictItemStatus
|
||||
} from '@/api/dict'
|
||||
import DictTypeEdit from './components/DictTypeEdit.vue'
|
||||
import DictItemList from './components/DictItemList.vue'
|
||||
import DictItemEdit from './components/DictItemEdit.vue'
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
|
||||
/** 当前视图:list 字典列表 / items 字典项列表 */
|
||||
const view = ref('list')
|
||||
|
||||
/* ---------------- 字典列表 ---------------- */
|
||||
|
||||
const currentView = ref<'types' | 'items'>('types')
|
||||
const typeLoading = ref(false)
|
||||
|
||||
// 字典类型相关
|
||||
const dictTypes = ref<any[]>([])
|
||||
const typeSearchKeyword = ref('')
|
||||
const types = ref([])
|
||||
const typeKeyword = ref('')
|
||||
const typeDialogVisible = ref(false)
|
||||
const currentDictType = ref<any>(null)
|
||||
const currentDictType = ref(null)
|
||||
const typePage = reactive({ page: 1, pageSize: 15, total: 0 })
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalCount = ref(0)
|
||||
/* ---------------- 字典项 ---------------- */
|
||||
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
const selectedDictType = computed(() => {
|
||||
if (!selectedDictTypeId.value) {
|
||||
return null
|
||||
}
|
||||
return dictTypes.value.find((type) => type.id === selectedDictTypeId.value) || null
|
||||
})
|
||||
const itemLoading = ref(false)
|
||||
const items = ref([])
|
||||
const itemKeyword = ref('')
|
||||
const itemDialogVisible = ref(false)
|
||||
const currentDictItem = ref(null)
|
||||
const itemPage = reactive({ page: 1, pageSize: 15, total: 0 })
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
/** 进入字典项页时选中的字典 */
|
||||
const currentType = ref(null)
|
||||
|
||||
/** 后端失败时优先透出业务提示(兼容 {code,msg} 与 HTTP 错误两种形态) */
|
||||
function errMsg(e, fallback) {
|
||||
return e?.response?.data?.msg || e?.message || fallback
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
/** 全局内置字典:只读(平台端字段为 undefined,自然放开) */
|
||||
const isGlobalDict = computed(() => !!currentType.value?.is_global)
|
||||
|
||||
onMounted(loadTypes)
|
||||
|
||||
async function loadTypes() {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await getDictTypes({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: typeSearchKeyword.value
|
||||
page: typePage.page,
|
||||
pageSize: typePage.pageSize,
|
||||
keyword: typeKeyword.value
|
||||
})
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data.list) ? res.data.list : (Array.isArray(res.data) ? res.data : [])
|
||||
totalCount.value = res.data.total || res.data.length || 0
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
totalCount.value = res.length
|
||||
if (res.code === 200) {
|
||||
types.value = res.data?.list || []
|
||||
typePage.total = res.data?.total || 0
|
||||
} else {
|
||||
dictTypes.value = []
|
||||
totalCount.value = 0
|
||||
ElMessage.error(res.message || '获取字典类型失败')
|
||||
ElMessage.error(res.msg || '获取字典列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取字典类型失败')
|
||||
dictTypes.value = []
|
||||
totalCount.value = 0
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '获取字典列表失败'))
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型搜索
|
||||
function handleTypeSearch() {
|
||||
currentPage.value = 1
|
||||
fetchDictTypes()
|
||||
typePage.page = 1
|
||||
loadTypes()
|
||||
}
|
||||
|
||||
// 分页切换
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
function handleAddType() {
|
||||
function openTypeCreate() {
|
||||
currentDictType.value = null
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典类型
|
||||
function handleEditType(row: any) {
|
||||
function openTypeEdit(row) {
|
||||
currentDictType.value = { ...row }
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
selectedDictTypeId.value = row.id
|
||||
currentView.value = 'items'
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
async function handleDeleteType(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
/** 字典保存后刷新列表;新增的字典直接进入它的字典项 */
|
||||
async function handleTypeSaved(created) {
|
||||
await loadTypes()
|
||||
if (created?.id) {
|
||||
const hit = types.value.find((t) => t.id === created.id) || created
|
||||
openItems(hit)
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型操作成功回调
|
||||
function handleTypeSuccess() {
|
||||
fetchDictTypes()
|
||||
async function handleTypeDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典「${row.dict_name}」吗?删除后不可恢复,其下的字典项也会一并失效。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
loadTypes()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 从字典项页返回
|
||||
function goBackToTypes() {
|
||||
currentView.value = 'types'
|
||||
/* ---------------- 字典项 ---------------- */
|
||||
|
||||
/** 进入某个字典的字典项列表 */
|
||||
function openItems(row) {
|
||||
currentType.value = { ...row }
|
||||
itemKeyword.value = ''
|
||||
itemPage.page = 1
|
||||
view.value = 'items'
|
||||
loadItems()
|
||||
}
|
||||
|
||||
// 子页面刷新后,静默刷新类型列表
|
||||
function refreshTypesSilently() {
|
||||
fetchDictTypes()
|
||||
function goBackToList() {
|
||||
view.value = 'list'
|
||||
if (currentType.value) {
|
||||
loadTypes() // 回列表时同步一下(字典项数量可能变了)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
async function refresh() {
|
||||
await fetchDictTypes()
|
||||
ElMessage.success('刷新成功')
|
||||
async function loadItems() {
|
||||
if (!currentType.value?.id) return
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const res = await getDictItems({
|
||||
dict_id: currentType.value.id,
|
||||
page: itemPage.page,
|
||||
pageSize: itemPage.pageSize,
|
||||
keyword: itemKeyword.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
items.value = res.data?.list || []
|
||||
itemPage.total = res.data?.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取字典项失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '获取字典项失败'))
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
function handleItemSearch() {
|
||||
itemPage.page = 1
|
||||
loadItems()
|
||||
}
|
||||
|
||||
function openItemCreate() {
|
||||
if (!currentType.value) return
|
||||
currentDictItem.value = null
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openItemEdit(row) {
|
||||
currentDictItem.value = { ...row }
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleItemToggle(row) {
|
||||
const next = row.status === 1 ? 0 : 1
|
||||
try {
|
||||
const res = await toggleDictItemStatus(row.id, next)
|
||||
if (res.code === 200) {
|
||||
row.status = next
|
||||
ElMessage.success(next === 1 ? '已启用' : '已停用')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleItemDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典项「${row.item_label}」吗?删除后不可恢复。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
loadItems()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(errMsg(e, '删除失败'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container-box {
|
||||
<style lang="less" scoped>
|
||||
.dict-page {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px 0;
|
||||
.crumb-link {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.dict-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.panel-search {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.filter-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 表格高度自适应:行数少时按内容收缩,行数多时封顶并在表格内部滚动(表头常驻) */
|
||||
.dict-table {
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: calc(100vh - 440px);
|
||||
min-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
.panel-pagination {
|
||||
flex-shrink: 0;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e5e5e5;
|
||||
display: inline-block;
|
||||
.inline-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* 屏幕很矮 / 窄屏时放宽封顶高度,交给页面滚动 */
|
||||
@media (max-height: 620px), (max-width: 900px) {
|
||||
.dict-table {
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>支付宝配置</h2>
|
||||
<p class="sub-title">平台使用费收款渠道,支持电脑网站支付与手机网站支付(RSA2 签名)。</p>
|
||||
</div>
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict="channel" />
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>云闪付配置</h2>
|
||||
<p class="sub-title">云闪付走银联通道收款,复用银联商户参数,本页只控制收银台展示与开关。</p>
|
||||
</div>
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict="channel" />
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>PayPal 配置</h2>
|
||||
<p class="sub-title">面向港澳台及海外租户的收款渠道,当前业务不跨境,默认保持停用。</p>
|
||||
</div>
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict="channel" />
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>银联配置</h2>
|
||||
<p class="sub-title">银联全渠道收单(PC 网关支付 / 手机控件支付),参数由收单机构下发。</p>
|
||||
</div>
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict="channel" />
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>微信支付配置</h2>
|
||||
<p class="sub-title">平台使用费收款主渠道,支持 JSAPI(公众号内)、Native(PC 扫码)支付。</p>
|
||||
</div>
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict="channel" />
|
||||
<StatusTag :status="form.enabled ? 'enabled' : 'disabled'" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.status" dict="commission" />
|
||||
<StatusTag :status="row.status" dict-code="payment_commission_status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发放方式" width="130" align="center">
|
||||
@@ -139,12 +139,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh, Setting } from '@element-plus/icons-vue'
|
||||
import StatusTag from '../components/StatusTag.vue'
|
||||
import { getCommissionList, getCommissionStats, markCommissionPaid } from '@/api/payment'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -165,20 +166,12 @@ const filters = reactive<{
|
||||
|
||||
const page = reactive({ current: 1, size: 10, total: 0 })
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '应付', value: 'payable' },
|
||||
{ label: '已付', value: 'settled' },
|
||||
{ label: '税务处理中', value: 'tax_processing' },
|
||||
{ label: '已作废', value: 'canceled' }
|
||||
]
|
||||
/** 佣金状态与发放方式取自数据字典 */
|
||||
const dictStore = useDictStore()
|
||||
const statusOptions = computed(() => dictStore.getSelectOptions('payment_commission_status'))
|
||||
|
||||
const payMethodMap: Record<string, string> = {
|
||||
balance: '余额抵扣',
|
||||
bank: '银行转账',
|
||||
manual: '人工发放',
|
||||
offset: '下期抵扣'
|
||||
}
|
||||
const payMethodLabel = (method: string) => payMethodMap[method] || method || '-'
|
||||
const payMethodLabel = (method: string) =>
|
||||
dictStore.getDictLabel('payment_commission_pay_method', method, method || '-')
|
||||
|
||||
/** 统计卡片数据。 */
|
||||
const stats = reactive({
|
||||
|
||||
@@ -107,9 +107,9 @@
|
||||
|
||||
<el-form-item label="计算方式" prop="calc_type">
|
||||
<el-radio-group v-model="form.calc_type">
|
||||
<el-radio value="percent">按比例</el-radio>
|
||||
<el-radio value="fixed">固定金额</el-radio>
|
||||
<el-radio value="tiered">阶梯</el-radio>
|
||||
<el-radio v-for="item in calcTypeOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
@@ -165,10 +165,12 @@
|
||||
|
||||
<el-form-item label="适用订单类型" prop="order_type">
|
||||
<el-select v-model="form.order_type" placeholder="请选择" style="width: 260px">
|
||||
<el-option label="全部订单" value="all" />
|
||||
<el-option label="平台使用费" value="platform_usage" />
|
||||
<el-option label="模块购买" value="module_shop" />
|
||||
<el-option label="服务费" value="service_fee" />
|
||||
<el-option
|
||||
v-for="item in orderTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -191,12 +193,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus, Edit, Delete, Tickets } from '@element-plus/icons-vue'
|
||||
import { getCommissionRules, saveCommissionRule, updateCommissionRule, deleteCommissionRule, toggleCommissionRule } from '@/api/payment'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -228,19 +231,24 @@ interface RuleRow {
|
||||
|
||||
const tableData = ref<RuleRow[]>([])
|
||||
|
||||
/** 计算方式与订单类型取自数据字典 */
|
||||
const dictStore = useDictStore()
|
||||
const calcTypeLabel = (type: string) =>
|
||||
({ percent: '按比例', fixed: '固定金额', tiered: '阶梯' } as Record<string, string>)[type] || type
|
||||
|
||||
dictStore.getDictLabel('payment_calc_type', type, type)
|
||||
const calcTypeTag = (type: string) =>
|
||||
({ percent: 'primary', fixed: 'success', tiered: 'warning' } as Record<string, any>)[type] || 'info'
|
||||
dictStore.getDictTagType('payment_calc_type', type)
|
||||
const calcTypeOptions = computed(() => dictStore.getSelectOptions('payment_calc_type'))
|
||||
|
||||
const orderTypeMap: Record<string, string> = {
|
||||
all: '全部订单',
|
||||
platform_usage: '平台使用费',
|
||||
module_shop: '模块购买',
|
||||
service_fee: '服务费'
|
||||
}
|
||||
const orderTypeLabel = (type: string) => orderTypeMap[type] || type || '-'
|
||||
/** 「全部订单」是筛选项而非业务取值,单独保留 */
|
||||
const ALL_ORDER_TYPE = 'all'
|
||||
const orderTypeOptions = computed(() => [
|
||||
{ label: '全部订单', value: ALL_ORDER_TYPE },
|
||||
...dictStore.getSelectOptions('payment_order_type')
|
||||
])
|
||||
const orderTypeLabel = (type: string) =>
|
||||
type === ALL_ORDER_TYPE
|
||||
? '全部订单'
|
||||
: dictStore.getDictLabel('payment_order_type', type, type || '-')
|
||||
|
||||
/** 规则值展示 */
|
||||
function valueText(row: RuleRow) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="merchant-no">{{ idLabel }}:{{ merchantNo || (status === 'unconfigured' ? '未配置' : '已配置') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatusTag :status="status" dict="channel" />
|
||||
<StatusTag :status="status" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
|
||||
<div v-if="desc" class="card-desc">{{ desc }}</div>
|
||||
|
||||
@@ -6,14 +6,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
type TagType = 'primary' | 'success' | 'info' | 'warning' | 'danger'
|
||||
type DictName = 'common' | 'channel' | 'order' | 'commission' | 'reconcile' | 'diff'
|
||||
|
||||
interface StatusItem {
|
||||
text: string
|
||||
type: TagType
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -21,93 +16,48 @@ const props = withDefaults(
|
||||
status?: string | number
|
||||
/** 自定义展示文案,不传则取字典里的文案 */
|
||||
text?: string
|
||||
/** 字典分组,不同业务模块状态含义不同 */
|
||||
dict?: DictName
|
||||
/** 字典编码:库里真实的 dict_code,如 payment_order_status */
|
||||
dictCode?: string
|
||||
/** 兜底字典编码:主字典查不到时按顺序再查(可选) */
|
||||
fallbackCodes?: string[]
|
||||
size?: 'large' | 'default' | 'small'
|
||||
effect?: 'dark' | 'light' | 'plain'
|
||||
}>(),
|
||||
{
|
||||
status: '',
|
||||
text: '',
|
||||
dict: 'common',
|
||||
dictCode: '',
|
||||
fallbackCodes: () => [],
|
||||
size: 'small',
|
||||
effect: 'light'
|
||||
}
|
||||
)
|
||||
|
||||
/** 通用状态(渠道 / 规则 / 单据都常见的枚举) */
|
||||
const COMMON: Record<string, StatusItem> = {
|
||||
'1': { text: '启用', type: 'success' },
|
||||
'0': { text: '停用', type: 'info' },
|
||||
enabled: { text: '已启用', type: 'success' },
|
||||
disabled: { text: '已停用', type: 'info' },
|
||||
unconfigured: { text: '未配置', type: 'warning' },
|
||||
yes: { text: '是', type: 'success' },
|
||||
no: { text: '否', type: 'info' }
|
||||
const dictStore = useDictStore()
|
||||
|
||||
/** 查找顺序:主字典 → 兜底字典(都来自数据库,前端不维护映射表) */
|
||||
const codes = computed(() => [props.dictCode, ...props.fallbackCodes].filter(Boolean) as string[])
|
||||
|
||||
if (codes.value.length) {
|
||||
// 全量预热已完成时这里不会发请求
|
||||
dictStore.preloadDicts(codes.value)
|
||||
}
|
||||
|
||||
/** 渠道状态 */
|
||||
const CHANNEL: Record<string, StatusItem> = {
|
||||
enabled: { text: '已启用', type: 'success' },
|
||||
disabled: { text: '已停用', type: 'info' },
|
||||
unconfigured: { text: '未配置', type: 'warning' }
|
||||
}
|
||||
const statusKey = computed(() =>
|
||||
props.status === undefined || props.status === null ? '' : String(props.status)
|
||||
)
|
||||
|
||||
/** 支付单状态 */
|
||||
const ORDER: Record<string, StatusItem> = {
|
||||
created: { text: '已创建', type: 'info' },
|
||||
pending: { text: '待支付', type: 'warning' },
|
||||
paying: { text: '支付中', type: 'warning' },
|
||||
paid: { text: '支付成功', type: 'success' },
|
||||
success: { text: '支付成功', type: 'success' },
|
||||
failed: { text: '支付失败', type: 'danger' },
|
||||
closed: { text: '已关闭', type: 'info' },
|
||||
timeout: { text: '已超时', type: 'info' },
|
||||
refunding: { text: '退款中', type: 'warning' },
|
||||
refunded: { text: '已退款', type: 'info' },
|
||||
partial_refunded: { text: '部分退款', type: 'warning' }
|
||||
}
|
||||
|
||||
/** 佣金状态 */
|
||||
const COMMISSION: Record<string, StatusItem> = {
|
||||
payable: { text: '应付', type: 'warning' },
|
||||
settled: { text: '已付', type: 'success' },
|
||||
tax_processing: { text: '税务处理中', type: 'primary' },
|
||||
canceled: { text: '已作废', type: 'info' }
|
||||
}
|
||||
|
||||
/** 对账处理状态 */
|
||||
const RECONCILE: Record<string, StatusItem> = {
|
||||
unhandled: { text: '待处理', type: 'warning' },
|
||||
handling: { text: '处理中', type: 'primary' },
|
||||
handled: { text: '已处理', type: 'success' },
|
||||
ignored: { text: '已忽略', type: 'info' }
|
||||
}
|
||||
|
||||
/** 对账差异类型 */
|
||||
const DIFF: Record<string, StatusItem> = {
|
||||
long: { text: '长款', type: 'warning' },
|
||||
short: { text: '短款', type: 'danger' },
|
||||
status_mismatch: { text: '状态不一致', type: 'warning' }
|
||||
}
|
||||
|
||||
const DICTS: Record<DictName, Record<string, StatusItem>> = {
|
||||
common: COMMON,
|
||||
channel: CHANNEL,
|
||||
order: ORDER,
|
||||
commission: COMMISSION,
|
||||
reconcile: RECONCILE,
|
||||
diff: DIFF
|
||||
}
|
||||
|
||||
const current = computed<StatusItem>(() => {
|
||||
const key = props.status === undefined || props.status === null ? '' : String(props.status)
|
||||
return (
|
||||
DICTS[props.dict]?.[key] ||
|
||||
COMMON[key] || {
|
||||
text: key || '-',
|
||||
type: 'info'
|
||||
const current = computed<{ text: string; type: TagType }>(() => {
|
||||
const key = statusKey.value
|
||||
if (key) {
|
||||
for (const code of codes.value) {
|
||||
const hit = dictStore.getDictItem(code, key)
|
||||
if (hit) {
|
||||
return { text: hit.item_label, type: (hit.tag_type || 'info') as TagType }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// 字典里没有的取值:原样展示,避免吞掉后端新增的状态
|
||||
return { text: key || '-', type: 'info' }
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<template #title>
|
||||
<div class="panel-title">
|
||||
<span class="name">{{ doc.name }}</span>
|
||||
<StatusTag :status="doc.status" dict="channel" />
|
||||
<StatusTag :status="doc.status" dict-code="payment_channel_state" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<el-table-column prop="scope" label="覆盖场景" min-width="220" />
|
||||
<el-table-column prop="status" label="当前状态" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.status" dict="channel" />
|
||||
<StatusTag :status="row.status" dict-code="payment_channel_state" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="说明" min-width="260" show-overflow-tooltip />
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.status" dict="order" />
|
||||
<StatusTag :status="row.status" dict-code="payment_order_status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="180" align="center">
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">支付单信息</span>
|
||||
<StatusTag :status="detail.status" dict="order" />
|
||||
<StatusTag :status="detail.status" dict-code="payment_order_status" />
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="支付单号">{{ detail.pay_no || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付状态">
|
||||
<StatusTag :status="detail.status" dict="order" />
|
||||
<StatusTag :status="detail.status" dict-code="payment_order_status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付金额">
|
||||
<span class="amount">¥{{ fenToYuan(detail.amount) }}</span>
|
||||
@@ -56,7 +56,7 @@
|
||||
<span class="amount">¥{{ fenToYuan(detail.order_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务订单状态">
|
||||
<StatusTag :status="detail.order_status" dict="order" />
|
||||
<StatusTag :status="detail.order_status" dict-code="payment_order_status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单摘要" :span="2">
|
||||
{{ detail.subject || '-' }}
|
||||
@@ -108,7 +108,7 @@
|
||||
placement="top"
|
||||
>
|
||||
<div class="record-row">
|
||||
<StatusTag :status="item.to_status" dict="order" />
|
||||
<StatusTag :status="item.to_status" dict-code="payment_order_status" />
|
||||
<span class="record-remark">{{ item.remark }}</span>
|
||||
</div>
|
||||
<div class="record-operator">操作来源:{{ item.operator }}</div>
|
||||
@@ -126,6 +126,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import StatusTag from '../components/StatusTag.vue'
|
||||
import { getOrderDetail, queryPayment, refundPayment } from '@/api/payment'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -174,21 +175,12 @@ const detail = reactive<Record<string, any>>({
|
||||
/** 仅支付成功且未退款时允许手动原路退回(兜底能力) */
|
||||
const canRefund = computed(() => detail.status === 'paid')
|
||||
|
||||
const channelMap: Record<string, string> = {
|
||||
wechat: '微信支付',
|
||||
alipay: '支付宝',
|
||||
unionpay: '银联',
|
||||
cloudpay: '云闪付',
|
||||
paypal: 'PayPal'
|
||||
}
|
||||
const channelLabel = (channel: string) => channelMap[channel] || channel || '-'
|
||||
|
||||
const orderTypeMap: Record<string, string> = {
|
||||
platform_usage: '平台使用费',
|
||||
module_shop: '模块购买',
|
||||
service_fee: '服务费'
|
||||
}
|
||||
const orderTypeLabel = (type: string) => orderTypeMap[type] || type || '-'
|
||||
/** 渠道与订单类型文案统一取自数据字典 */
|
||||
const dictStore = useDictStore()
|
||||
const channelLabel = (channel: string) =>
|
||||
dictStore.getDictLabel('payment_channel', channel, channel || '-')
|
||||
const orderTypeLabel = (type: string) =>
|
||||
dictStore.getDictLabel('payment_order_type', type, type || '-')
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.status" dict="order" />
|
||||
<StatusTag :status="row.status" dict-code="payment_order_status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
|
||||
@@ -121,13 +121,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import StatusTag from '../components/StatusTag.vue'
|
||||
import { getOrders } from '@/api/payment'
|
||||
import { getTenantList } from '@/api/tenant'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -150,27 +151,15 @@ const filters = reactive<{
|
||||
|
||||
const page = reactive({ current: 1, size: 10, total: 0 })
|
||||
|
||||
const channelOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '银联', value: 'unionpay' },
|
||||
{ label: '云闪付', value: 'cloudpay' },
|
||||
{ label: 'PayPal', value: 'paypal' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待支付', value: 'pending' },
|
||||
{ label: '支付中', value: 'paying' },
|
||||
{ label: '支付成功', value: 'paid' },
|
||||
{ label: '支付失败', value: 'failed' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已退款', value: 'refunded' }
|
||||
]
|
||||
/** 渠道与状态选项统一取自数据字典(见 /platform/dict/options) */
|
||||
const dictStore = useDictStore()
|
||||
const channelOptions = computed(() => dictStore.getSelectOptions('payment_channel'))
|
||||
const statusOptions = computed(() => dictStore.getSelectOptions('payment_order_status'))
|
||||
|
||||
const tenantOptions = ref<{ id: string | number; name: string }[]>([])
|
||||
|
||||
const channelName = (channel: string) =>
|
||||
channelOptions.find((i) => i.value === channel)?.label || channel
|
||||
dictStore.getDictLabel('payment_channel', channel, channel)
|
||||
|
||||
async function searchTenant(keyword: string) {
|
||||
tenantLoading.value = true
|
||||
|
||||
@@ -48,17 +48,22 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="差异类型">
|
||||
<el-select v-model="filters.diff_type" placeholder="全部类型" clearable style="width: 160px">
|
||||
<el-option label="长款" value="long" />
|
||||
<el-option label="短款" value="short" />
|
||||
<el-option label="状态不一致" value="status_mismatch" />
|
||||
<el-option
|
||||
v-for="item in diffTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理状态">
|
||||
<el-select v-model="filters.handle_status" placeholder="全部状态" clearable style="width: 160px">
|
||||
<el-option label="待处理" value="unhandled" />
|
||||
<el-option label="处理中" value="handling" />
|
||||
<el-option label="已处理" value="handled" />
|
||||
<el-option label="已忽略" value="ignored" />
|
||||
<el-option
|
||||
v-for="item in handleStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -101,13 +106,13 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="差异类型" width="180">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.diff_type" dict="diff" />
|
||||
<StatusTag :status="row.diff_type" dict-code="payment_reconcile_diff_type" />
|
||||
<span class="diff-amount">{{ diffAmountText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<StatusTag :status="row.handle_status" dict="reconcile" />
|
||||
<StatusTag :status="row.handle_status" dict-code="payment_reconcile_status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理备注" min-width="180" show-overflow-tooltip>
|
||||
@@ -147,11 +152,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import StatusTag from '../components/StatusTag.vue'
|
||||
import { getReconcileList, markReconcileHandled } from '@/api/payment'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -164,22 +170,14 @@ const filters = reactive({
|
||||
|
||||
const page = reactive({ current: 1, size: 10, total: 0 })
|
||||
|
||||
const channelOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '银联', value: 'unionpay' },
|
||||
{ label: '云闪付', value: 'cloudpay' },
|
||||
{ label: 'PayPal', value: 'paypal' }
|
||||
]
|
||||
/** 渠道与差异类型选项统一取自数据字典 */
|
||||
const dictStore = useDictStore()
|
||||
const channelOptions = computed(() => dictStore.getSelectOptions('payment_channel'))
|
||||
const diffTypeOptions = computed(() => dictStore.getSelectOptions('payment_reconcile_diff_type'))
|
||||
const handleStatusOptions = computed(() => dictStore.getSelectOptions('payment_reconcile_status'))
|
||||
|
||||
const channelMap: Record<string, string> = {
|
||||
wechat: '微信支付',
|
||||
alipay: '支付宝',
|
||||
unionpay: '银联',
|
||||
cloudpay: '云闪付',
|
||||
paypal: 'PayPal'
|
||||
}
|
||||
const channelLabel = (channel: string) => channelMap[channel] || channel || '-'
|
||||
const channelLabel = (channel: string) =>
|
||||
dictStore.getDictLabel('payment_channel', channel, channel || '-')
|
||||
|
||||
/* ---------------- 列表数据 ---------------- */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user