更新完善字典相关功能

This commit is contained in:
2026-02-18 17:15:33 +08:00
parent 5450777bd7
commit 378b9bd71f
23 changed files with 1657 additions and 572 deletions

View File

@@ -28,7 +28,7 @@ export default {
post: async function (file) {
const formData = new FormData()
formData.append('file', file)
return await request.post('upload', formData, {
return await request.post('system/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
@@ -38,64 +38,64 @@ export default {
users: {
list: {
get: async function (params) {
return await request.get('users', { params })
return await request.get('auth/users', { params })
},
},
detail: {
get: async function (id) {
return await request.get(`users/${id}`)
return await request.get(`auth/users/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('users', params)
return await request.post('auth/users', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`users/${id}`, params)
return await request.put(`auth/users/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`users/${id}`)
return await request.delete(`auth/users/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('users/batch-delete', params)
return await request.post('auth/users/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('users/batch-status', params)
return await request.post('auth/users/batch-status', params)
},
},
batchDepartment: {
post: async function (params) {
return await request.post('users/batch-department', params)
return await request.post('auth/users/batch-department', params)
},
},
batchRoles: {
post: async function (params) {
return await request.post('users/batch-roles', params)
return await request.post('auth/users/batch-roles', params)
},
},
export: {
post: async function (params) {
return await request.post('users/export', params, { responseType: 'blob' })
return await request.post('auth/users/export', params, { responseType: 'blob' })
},
},
import: {
post: async function (formData) {
return await request.post('users/import', formData, {
return await request.post('auth/users/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
downloadTemplate: {
get: async function () {
return await request.get('users/download-template', { responseType: 'blob' })
return await request.get('auth/users/download-template', { responseType: 'blob' })
},
},
},
@@ -104,27 +104,27 @@ export default {
onlineUsers: {
count: {
get: async function () {
return await request.get('online-users/count')
return await request.get('auth/online-users/count')
},
},
list: {
get: async function (params) {
return await request.get('online-users', { params })
return await request.get('auth/online-users', { params })
},
},
sessions: {
get: async function (userId) {
return await request.get(`online-users/${userId}/sessions`)
return await request.get(`auth/online-users/${userId}/sessions`)
},
},
offline: {
post: async function (userId, params) {
return await request.post(`online-users/${userId}/offline`, params)
return await request.post(`auth/online-users/${userId}/offline`, params)
},
},
offlineAll: {
post: async function (userId) {
return await request.post(`online-users/${userId}/offline-all`)
return await request.post(`auth/online-users/${userId}/offline-all`)
},
},
},
@@ -133,77 +133,77 @@ export default {
roles: {
list: {
get: async function (params) {
return await request.get('roles', { params })
return await request.get('auth/roles', { params })
},
},
all: {
get: async function () {
return await request.get('roles/all')
return await request.get('auth/roles/all')
},
},
detail: {
get: async function (id) {
return await request.get(`roles/${id}`)
return await request.get(`auth/roles/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('roles', params)
return await request.post('auth/roles', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`roles/${id}`, params)
return await request.put(`auth/roles/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`roles/${id}`)
return await request.delete(`auth/roles/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('roles/batch-delete', params)
return await request.post('auth/roles/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('roles/batch-status', params)
return await request.post('auth/roles/batch-status', params)
},
},
permissions: {
get: async function (id) {
return await request.get(`roles/${id}/permissions`)
return await request.get(`auth/roles/${id}/permissions`)
},
post: async function (id, params) {
return await request.post(`roles/${id}/permissions`, params)
return await request.post(`auth/roles/${id}/permissions`, params)
},
},
copy: {
post: async function (id, params) {
return await request.post(`roles/${id}/copy`, params)
return await request.post(`auth/roles/${id}/copy`, params)
},
},
batchCopy: {
post: async function (params) {
return await request.post('roles/batch-copy', params)
return await request.post('auth/roles/batch-copy', params)
},
},
export: {
post: async function (params) {
return await request.post('roles/export', params, { responseType: 'blob' })
return await request.post('auth/roles/export', params, { responseType: 'blob' })
},
},
import: {
post: async function (formData) {
return await request.post('roles/import', formData, {
return await request.post('auth/roles/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
downloadTemplate: {
get: async function () {
return await request.get('roles/download-template', { responseType: 'blob' })
return await request.get('auth/roles/download-template', { responseType: 'blob' })
},
},
},
@@ -212,64 +212,64 @@ export default {
permissions: {
list: {
get: async function (params) {
return await request.get('permissions', { params })
return await request.get('auth/permissions', { params })
},
},
tree: {
get: async function () {
return await request.get('permissions/tree')
return await request.get('auth/permissions/tree')
},
},
menu: {
get: async function () {
return await request.get('permissions/menu')
return await request.get('auth/permissions/menu')
},
},
detail: {
get: async function (id) {
return await request.get(`permissions/${id}`)
return await request.get(`auth/permissions/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('permissions', params)
return await request.post('auth/permissions', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`permissions/${id}`, params)
return await request.put(`auth/permissions/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`permissions/${id}`)
return await request.delete(`auth/permissions/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('permissions/batch-delete', params)
return await request.post('auth/permissions/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('permissions/batch-status', params)
return await request.post('auth/permissions/batch-status', params)
},
},
export: {
post: async function (params) {
return await request.post('permissions/export', params, { responseType: 'blob' })
return await request.post('auth/permissions/export', params, { responseType: 'blob' })
},
},
import: {
post: async function (formData) {
return await request.post('permissions/import', formData, {
return await request.post('auth/permissions/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
downloadTemplate: {
get: async function () {
return await request.get('permissions/download-template', { responseType: 'blob' })
return await request.get('auth/permissions/download-template', { responseType: 'blob' })
},
},
},
@@ -278,64 +278,64 @@ export default {
departments: {
list: {
get: async function (params) {
return await request.get('departments', { params })
return await request.get('auth/departments', { params })
},
},
tree: {
get: async function (params) {
return await request.get('departments/tree', { params })
return await request.get('auth/departments/tree', { params })
},
},
all: {
get: async function () {
return await request.get('departments/all')
return await request.get('auth/departments/all')
},
},
detail: {
get: async function (id) {
return await request.get(`departments/${id}`)
return await request.get(`auth/departments/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('departments', params)
return await request.post('auth/departments', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`departments/${id}`, params)
return await request.put(`auth/departments/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`departments/${id}`)
return await request.delete(`auth/departments/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('departments/batch-delete', params)
return await request.post('auth/departments/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('departments/batch-status', params)
return await request.post('auth/departments/batch-status', params)
},
},
export: {
post: async function (params) {
return await request.post('departments/export', params, { responseType: 'blob' })
return await request.post('auth/departments/export', params, { responseType: 'blob' })
},
},
import: {
post: async function (formData) {
return await request.post('departments/import', formData, {
return await request.post('auth/departments/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
downloadTemplate: {
get: async function () {
return await request.get('departments/download-template', { responseType: 'blob' })
return await request.get('auth/departments/download-template', { responseType: 'blob' })
},
},
},

View File

@@ -5,47 +5,47 @@ export default {
configs: {
list: {
get: async function (params) {
return await request.get('configs', { params })
return await request.get('system/configs', { params })
},
},
groups: {
get: async function () {
return await request.get('configs/groups')
return await request.get('system/configs/groups')
},
},
all: {
get: async function (params) {
return await request.get('configs/all', { params })
return await request.get('system/configs/all', { params })
},
},
detail: {
get: async function (id) {
return await request.get(`configs/${id}`)
return await request.get(`system/configs/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('configs', params)
return await request.post('system/configs', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`configs/${id}`, params)
return await request.put(`system/configs/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`configs/${id}`)
return await request.delete(`system/configs/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('configs/batch-delete', params)
return await request.post('system/configs/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('configs/batch-status', params)
return await request.post('system/configs/batch-status', params)
},
},
},
@@ -54,32 +54,32 @@ export default {
logs: {
list: {
get: async function (params) {
return await request.get('logs', { params })
return await request.get('system/logs', { params })
},
},
detail: {
get: async function (id) {
return await request.get(`logs/${id}`)
return await request.get(`system/logs/${id}`)
},
},
delete: {
delete: async function (id) {
return await request.delete(`logs/${id}`)
return await request.delete(`system/logs/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('logs/batch-delete', params)
return await request.post('system/logs/batch-delete', params)
},
},
clear: {
post: async function (params) {
return await request.post('logs/clear', params)
return await request.post('system/logs/clear', params)
},
},
export: {
get: async function (params) {
return await request.get('logs/export', {
return await request.get('system/logs/export', {
params,
responseType: 'blob'
})
@@ -87,90 +87,102 @@ export default {
},
statistics: {
get: async function (params) {
return await request.get('logs/statistics', { params })
return await request.get('system/logs/statistics', { params })
},
},
},
// 数据字典管理
dictionaries: {
list: {
get: async function (params) {
return await request.get('dictionaries', { params })
// 数据字典管理
dictionaries: {
list: {
get: async function (params) {
return await request.get('system/dictionaries', { params })
},
},
all: {
get: async function () {
return await request.get('system/dictionaries/all')
},
},
detail: {
get: async function (id) {
return await request.get(`system/dictionaries/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('system/dictionaries', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`system/dictionaries/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`system/dictionaries/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('system/dictionaries/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('system/dictionaries/batch-status', params)
},
},
items: {
all: {
get: async function (code) {
return await request.get(`system/dictionaries/code`, { params: { code } })
},
},
},
},
all: {
get: async function () {
return await request.get('dictionaries/all')
},
},
detail: {
get: async function (id) {
return await request.get(`dictionaries/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('dictionaries', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`dictionaries/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`dictionaries/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('dictionaries/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('dictionaries/batch-status', params)
},
},
},
// 数据字典项管理
dictionaryItems: {
list: {
get: async function (params) {
return await request.get('dictionary-items', { params })
return await request.get('system/dictionary-items', { params })
},
},
all: {
get: async function () {
return await request.get('system/dictionary-items/all')
},
},
detail: {
get: async function (id) {
return await request.get(`dictionary-items/${id}`)
return await request.get(`system/dictionary-items/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('dictionary-items', params)
return await request.post('system/dictionary-items', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`dictionary-items/${id}`, params)
return await request.put(`system/dictionary-items/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`dictionary-items/${id}`)
return await request.delete(`system/dictionary-items/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('dictionary-items/batch-delete', params)
return await request.post('system/dictionary-items/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('dictionary-items/batch-status', params)
return await request.post('system/dictionary-items/batch-status', params)
},
},
},
@@ -179,52 +191,52 @@ export default {
tasks: {
list: {
get: async function (params) {
return await request.get('tasks', { params })
return await request.get('system/tasks', { params })
},
},
all: {
get: async function () {
return await request.get('tasks/all')
return await request.get('system/tasks/all')
},
},
detail: {
get: async function (id) {
return await request.get(`tasks/${id}`)
return await request.get(`system/tasks/${id}`)
},
},
add: {
post: async function (params) {
return await request.post('tasks', params)
return await request.post('system/tasks', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`tasks/${id}`, params)
return await request.put(`system/tasks/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`tasks/${id}`)
return await request.delete(`system/tasks/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('tasks/batch-delete', params)
return await request.post('system/tasks/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('tasks/batch-status', params)
return await request.post('system/tasks/batch-status', params)
},
},
run: {
post: async function (id) {
return await request.post(`tasks/${id}/run`)
return await request.post(`system/tasks/${id}/run`)
},
},
statistics: {
get: async function () {
return await request.get('tasks/statistics')
return await request.get('system/tasks/statistics')
},
},
},
@@ -233,62 +245,62 @@ export default {
cities: {
list: {
get: async function (params) {
return await request.get('cities', { params })
return await request.get('system/cities', { params })
},
},
tree: {
get: async function () {
return await request.get('cities/tree')
return await request.get('system/cities/tree')
},
},
detail: {
get: async function (id) {
return await request.get(`cities/${id}`)
return await request.get(`system/cities/${id}`)
},
},
children: {
get: async function (id) {
return await request.get(`cities/${id}/children`)
return await request.get(`system/cities/${id}/children`)
},
},
provinces: {
get: async function () {
return await request.get('cities/provinces')
return await request.get('system/cities/provinces')
},
},
cities: {
get: async function (provinceId) {
return await request.get(`cities/${provinceId}/cities`)
return await request.get(`system/cities/${provinceId}/cities`)
},
},
districts: {
get: async function (cityId) {
return await request.get(`cities/${cityId}/districts`)
return await request.get(`system/cities/${cityId}/districts`)
},
},
add: {
post: async function (params) {
return await request.post('cities', params)
return await request.post('system/cities', params)
},
},
edit: {
put: async function (id, params) {
return await request.put(`cities/${id}`, params)
return await request.put(`system/cities/${id}`, params)
},
},
delete: {
delete: async function (id) {
return await request.delete(`cities/${id}`)
return await request.delete(`system/cities/${id}`)
},
},
batchDelete: {
post: async function (params) {
return await request.post('cities/batch-delete', params)
return await request.post('system/cities/batch-delete', params)
},
},
batchStatus: {
post: async function (params) {
return await request.post('cities/batch-status', params)
return await request.post('system/cities/batch-status', params)
},
},
},
@@ -297,31 +309,31 @@ export default {
upload: {
single: {
post: async function (formData) {
return await request.post('upload', formData, {
return await request.post('system/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
multiple: {
post: async function (formData) {
return await request.post('upload/multiple', formData, {
return await request.post('system/upload/multiple', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
},
base64: {
post: async function (params) {
return await request.post('upload/base64', params)
return await request.post('system/upload/base64', params)
},
},
delete: {
post: async function (params) {
return await request.post('upload/delete', params)
return await request.post('system/upload/delete', params)
},
},
batchDelete: {
post: async function (params) {
return await request.post('upload/batch-delete', params)
return await request.post('system/upload/batch-delete', params)
},
},
},

View File

@@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,606 @@
# scSelect 组件使用文档
## 组件简介
`scSelect` 是一个基于 Ant Design Vue `a-select` 组件封装的增强型选择器组件支持多种数据源data、api、dictionary和智能缓存机制。
## 特性
- ✅ 支持三种数据源直接数据data、API 接口api、字典数据dictionary
- ✅ API 数据源自动缓存,减少重复请求
- ✅ 字典数据源自动从缓存读取,登录后自动预加载
- ✅ 支持按需加载数据focus 时触发)
- ✅ 支持立即加载数据(组件挂载时)
- ✅ 支持自定义字段映射
- ✅ 支持自定义数据处理函数
- ✅ 完全继承 `a-select` 的所有属性和方法
- ✅ 支持插槽透传
## 安装
组件已自动注册,直接使用即可:
```vue
<template>
<sc-select v-model:value="value" source-type="data" :data="options" />
</template>
<script setup>
import { ref } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const value = ref('')
const options = ref([
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
])
</script>
```
## Props
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|------|------|------|--------|--------|
| sourceType | 数据源类型 | String | 'data' \| 'api' \| 'dictionary' | 'data' |
| data | 直接数据(当 sourceType 为 data 时使用) | Array | - | [] |
| api | API 接口地址(当 sourceType 为 api 时使用) | String | - | '' |
| apiParams | API 请求参数(当 sourceType 为 api 时使用) | Object | - | {} |
| dictionaryCode | 字典编码(当 sourceType 为 dictionary 时使用) | String | - | '' |
| enableApiCache | 是否启用 API 数据缓存 | Boolean | - | true |
| apiCacheTime | API 缓存时间(毫秒) | Number | - | 3000005分钟 |
| fieldNames | 字段映射配置 | Object | - | { label: 'label', value: 'value' } |
| immediate | 是否在组件挂载时立即加载数据 | Boolean | - | false |
| dataProcessor | 数据处理函数 | Function | - | null |
## Events
组件完全继承 `a-select` 的所有事件,包括但不限于:
- `@change` - 选中值变化时触发
- `@focus` - 获得焦点时触发
- `@blur` - 失去焦点时触发
## Methods
通过 ref 可以调用以下方法:
| 方法名 | 说明 | 参数 |
|--------|------|------|
| refresh | 强制刷新数据 | - |
| loadApiData | 手动加载 API 数据 | - |
| loadDictionaryData | 手动加载字典数据 | - |
## 使用示例
### 1. 使用 data 数据源
最简单的使用方式,直接提供数据数组:
```vue
<template>
<a-form>
<a-form-item label="状态">
<sc-select
v-model:value="form.status"
source-type="data"
:data="statusOptions"
placeholder="请选择状态"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { reactive } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const form = reactive({
status: ''
})
const statusOptions = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
]
</script>
```
### 2. 使用 api 数据源
从 API 接口获取数据,自动缓存:
```vue
<template>
<a-form>
<a-form-item label="用户">
<sc-select
v-model:value="form.userId"
source-type="api"
api="users"
:api-params="{ page: 1, page_size: 100 }"
:enable-api-cache="true"
:api-cache-time="600000"
placeholder="请选择用户"
:field-names="{ label: 'username', value: 'id' }"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { reactive } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const form = reactive({
userId: ''
})
</script>
```
### 3. 使用 dictionary 数据源(推荐)
从字典缓存获取数据,性能最佳:
```vue
<template>
<a-form>
<a-form-item label="性别">
<sc-select
v-model:value="form.gender"
source-type="dictionary"
dictionary-code="gender"
placeholder="请选择性别"
/>
</a-form-item>
<a-form-item label="用户状态">
<sc-select
v-model:value="form.userStatus"
source-type="dictionary"
dictionary-code="user_status"
placeholder="请选择用户状态"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { reactive } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const form = reactive({
gender: '',
userStatus: ''
})
</script>
```
### 4. 立即加载数据
在组件挂载时立即加载数据,而不是等到 focus 时:
```vue
<template>
<sc-select
v-model:value="value"
source-type="api"
api="roles/all"
:immediate="true"
placeholder="请选择角色"
/>
</template>
<script setup>
import { ref } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const value = ref('')
</script>
```
### 5. 自定义数据处理
使用 `dataProcessor` 函数自定义数据格式:
```vue
<template>
<sc-select
v-model:value="value"
source-type="api"
api="roles"
:data-processor="processData"
placeholder="请选择角色"
/>
</template>
<script setup>
import { ref } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const value = ref('')
// 自定义数据处理函数
function processData(data) {
return data.map(item => ({
label: `${item.name} (${item.description})`,
value: item.id,
extra: item.description
}))
}
</script>
```
### 6. 使用 ref 调用方法
```vue
<template>
<div>
<sc-select
ref="selectRef"
v-model:value="value"
source-type="api"
api="users"
placeholder="请选择用户"
/>
<a-button @click="handleRefresh">刷新数据</a-button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
const selectRef = ref(null)
const value = ref('')
// 刷新数据
function handleRefresh() {
if (selectRef.value) {
selectRef.value.refresh()
message.success('数据已刷新')
}
}
</script>
```
### 7. 禁用 API 缓存
```vue
<template>
<sc-select
v-model:value="value"
source-type="api"
api="users"
:enable-api-cache="false"
placeholder="请选择用户"
/>
</template>
```
### 8. 多选模式
```vue
<template>
<sc-select
v-model:value="value"
mode="multiple"
source-type="dictionary"
dictionary-code="tags"
placeholder="请选择标签"
/>
</template>
<script setup>
import { ref } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const value = ref([])
</script>
```
### 9. 自定义插槽
```vue
<template>
<sc-select
v-model:value="value"
source-type="data"
:data="options"
placeholder="请选择"
>
<template #suffixIcon>
<SearchOutlined />
</template>
<template #notFoundContent>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="暂无数据" />
</template>
</sc-select>
</template>
<script setup>
import { ref } from 'vue'
import { SearchOutlined } from '@ant-design/icons-vue'
import { Empty } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
const value = ref('')
const options = ref([
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
])
</script>
```
## 数据格式要求
### data 数据源
支持以下格式:
1. 字符串/数字数组
```javascript
['选项1', '选项2', '选项3']
[1, 2, 3]
```
2. 对象数组(标准格式)
```javascript
[
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
]
```
3. 对象数组(自定义字段)
```javascript
[
{ name: '选项1', id: 1 },
{ name: '选项2', id: 2 },
]
// 使用 field-names 映射
field-names="{ label: 'name', value: 'id' }"
```
### api 数据源
API 接口需要返回以下格式:
```javascript
{
"code": 200,
"message": "success",
"data": [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
]
}
// 或者
{
"code": 200,
"message": "success",
"list": [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
]
}
```
### dictionary 数据源
字典数据通过后台管理系统的"数据字典管理"模块配置,登录后会自动缓存到前端。
字典数据格式:
```javascript
{
code: 'user_status',
name: '用户状态',
items: [
{ name: '启用', value: '1', sort: 1, status: 1 },
{ name: '禁用', value: '0', sort: 2, status: 1 },
]
}
```
组件会自动转换为:
```javascript
[
{ label: '启用', value: '1', name: '启用', sort: 1, status: 1 },
{ label: '禁用', value: '0', name: '禁用', sort: 2, status: 1 },
]
```
## 字典数据缓存机制
### 缓存流程
1. **登录时**:用户登录成功后,自动调用 `dictionaryStore.loadAllDictionaries()` 加载所有字典数据
2. **存储**:字典数据存储在 Pinia Store 中,并持久化到 localStorage
3. **使用**:组件通过 `dictionaryCode` 从 Store 获取数据,无需重复请求
4. **刷新**:可以调用 `refresh()` 方法强制刷新字典数据
### 清空缓存
```javascript
import { useDictionaryStore } from '@/stores/modules/dictionary'
const dictionaryStore = useDictionaryStore()
// 清空字典缓存
dictionaryStore.clearCache()
```
### 获取缓存信息
```javascript
import { useDictionaryStore } from '@/stores/modules/dictionary'
const dictionaryStore = useDictionaryStore()
// 获取缓存信息
const info = dictionaryStore.getCacheInfo()
console.log('字典数量:', info.count)
console.log('最后加载时间:', new Date(info.lastLoadTime))
```
## API 数据缓存机制
### 缓存策略
- 默认启用缓存,缓存时间为 5 分钟
- 缓存 key 由 `api` 地址和 `apiParams` 参数组成
- 缓存过期后自动重新请求
- 可以通过 `enableApiCache` 关闭缓存
- 可以通过 `apiCacheTime` 自定义缓存时间
### 缓存 key 示例
```javascript
// 同一个 API不同参数会有不同的缓存
api: 'users'
apiParams: { page: 1, page_size: 20 }
// 缓存 key: 'users{"page":1,"page_size":20}'
apiParams: { page: 2, page_size: 20 }
// 缓存 key: 'users{"page":2,"page_size":20}'
```
## 注意事项
1. **字典数据源**:确保在后台管理系统中配置了对应的字典数据
2. **API 数据源**:确保 API 接口返回的数据格式正确
3. **缓存失效**:修改字典数据后,需要调用 `refresh()` 方法刷新缓存
4. **字段映射**:如果后端返回的字段名不是 `label``value`,需要使用 `fieldNames` 映射
5. **按需加载**:默认在 focus 时加载数据,如果需要立即加载,设置 `immediate=true`
## 最佳实践
1. **优先使用字典数据源**:对于固定选项(如状态、类型等),优先使用字典数据源
2. **合理使用缓存**API 数据源建议启用缓存,减少服务器压力
3. **字段映射**:明确指定 `fieldNames`,避免数据格式不一致
4. **错误处理**API 请求失败时,组件会自动处理错误并记录日志
5. **性能优化**:对于大量数据,建议使用分页加载或虚拟滚动
## 常见问题
### Q: 字典数据不显示?
A: 检查以下几点:
1. 后台管理系统中是否配置了对应的字典
2. 字典编码是否正确
3. 字典状态是否为启用
4. 字典项状态是否为启用
### Q: API 数据加载失败?
A: 检查以下几点:
1. API 地址是否正确
2. API 是否需要认证
3. 返回数据格式是否符合要求
4. 控制台是否有错误信息
### Q: 如何刷新字典数据?
A: 使用 `refresh()` 方法:
```javascript
const selectRef = ref(null)
selectRef.value?.refresh()
```
### Q: 如何禁用缓存?
A: 设置 `enableApiCache``false`
```vue
<sc-select
source-type="api"
api="users"
:enable-api-cache="false"
/>
```
## 完整示例
```vue
<template>
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<!-- 使用数据源 -->
<a-form-item label="数据源示例">
<sc-select
v-model:value="form.dataSource"
source-type="data"
:data="[
{ label: '字典数据', value: 'dictionary' },
{ label: 'API数据', value: 'api' },
{ label: '直接数据', value: 'data' },
]"
placeholder="请选择数据源"
/>
</a-form-item>
<!-- 使用字典数据 -->
<a-form-item label="用户状态">
<sc-select
v-model:value="form.status"
source-type="dictionary"
dictionary-code="user_status"
placeholder="请选择用户状态"
/>
</a-form-item>
<!-- 使用 API 数据 -->
<a-form-item label="所属角色">
<sc-select
v-model:value="form.roleId"
source-type="api"
api="roles/all"
:enable-api-cache="true"
:immediate="true"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择角色"
/>
</a-form-item>
<!-- 多选模式 -->
<a-form-item label="标签">
<sc-select
v-model:value="form.tags"
mode="multiple"
source-type="dictionary"
dictionary-code="tags"
placeholder="请选择标签"
/>
</a-form-item>
<!-- 带搜索 -->
<a-form-item label="省份">
<sc-select
v-model:value="form.province"
source-type="api"
api="cities/provinces"
show-search
:filter-option="filterOption"
placeholder="请选择省份"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { reactive } from 'vue'
import scSelect from '@/components/scSelect/index.vue'
const form = reactive({
dataSource: '',
status: '',
roleId: '',
tags: [],
province: '',
})
// 搜索过滤
function filterOption(input, option) {
return option.label.toLowerCase().includes(input.toLowerCase())
}
</script>

View File

@@ -0,0 +1,308 @@
<template>
<a-select
v-bind="$attrs"
:loading="loading"
:options="options"
:field-names="fieldNames"
@focus="handleFocus"
>
<template v-for="(_, slot) of $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
</a-select>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useDictionaryStore } from '@/stores/modules/dictionary'
import request from '@/utils/request'
defineOptions({
name: 'ScSelect',
inheritAttrs: false,
})
const props = defineProps({
// 数据源类型data直接数据、api接口数据、dictionary字典数据
sourceType: {
type: String,
default: 'data',
validator: (value) => ['data', 'api', 'dictionary'].includes(value),
},
// 直接数据(当 sourceType 为 data 时使用)
data: {
type: Array,
default: () => [],
},
// API 接口地址(当 sourceType 为 api 时使用)
api: {
type: String,
default: '',
},
// API 请求参数(当 sourceType 为 api 时使用)
apiParams: {
type: Object,
default: () => ({}),
},
// 字典编码(当 sourceType 为 dictionary 时使用)
dictionaryCode: {
type: String,
default: '',
},
// 是否启用 API 数据缓存(当 sourceType 为 api 时使用)
enableApiCache: {
type: Boolean,
default: true,
},
// API 缓存时间(毫秒,当 sourceType 为 api 时使用)
apiCacheTime: {
type: Number,
default: 5 * 60 * 1000, // 默认 5 分钟
},
// 字段映射配置
fieldNames: {
type: Object,
default: () => ({
label: 'label',
value: 'value',
}),
},
// 是否在组件挂载时立即加载数据
immediate: {
type: Boolean,
default: false,
},
// 数据处理函数,用于自定义数据格式转换
dataProcessor: {
type: Function,
default: null,
},
})
const dictionaryStore = useDictionaryStore()
const loading = ref(false)
const apiData = ref(null)
const apiCacheTime = ref(null)
const options = computed(() => {
switch (props.sourceType) {
case 'data':
return processData(props.data)
case 'api':
return processData(apiData.value || [])
case 'dictionary':
return processData(getDictionaryData())
default:
return []
}
})
// API 数据缓存
const apiCache = new Map()
/**
* 处理数据格式
*/
function processData(data) {
if (!data || !Array.isArray(data)) {
return []
}
// 如果有自定义处理函数,使用自定义处理
if (props.dataProcessor && typeof props.dataProcessor === 'function') {
return props.dataProcessor(data)
}
// 默认处理:确保数据有 label 和 value 字段
return data.map((item) => {
if (typeof item === 'string' || typeof item === 'number') {
return {
label: item,
value: item,
}
}
// 如果已经有 label 和 value 字段,直接返回
if (item.label !== undefined && item.value !== undefined) {
return item
}
// 尝试使用 fieldNames 映射
const labelKey = props.fieldNames.label || 'label'
const valueKey = props.fieldNames.value || 'value'
return {
label: item[labelKey] || item.name || item.title || item.id,
value: item[valueKey] !== undefined ? item[valueKey] : item.id,
...item,
}
})
}
/**
* 获取字典数据
*/
function getDictionaryData() {
if (!props.dictionaryCode) {
return []
}
return dictionaryStore.dictionaries[props.dictionaryCode] || []
}
/**
* 加载 API 数据
*/
async function loadApiData() {
if (!props.api) {
return
}
// 检查缓存
if (props.enableApiCache) {
const cacheKey = props.api + JSON.stringify(props.apiParams)
const cached = apiCache.get(cacheKey)
if (cached && Date.now() - cached.time < props.apiCacheTime) {
apiData.value = cached.data
return
}
}
loading.value = true
try {
const res = await request.get(props.api, { params: props.apiParams })
if (res.code === 200) {
const data = res.data || res.list || []
apiData.value = data
// 缓存数据
if (props.enableApiCache) {
const cacheKey = props.api + JSON.stringify(props.apiParams)
apiCache.set(cacheKey, {
data,
time: Date.now(),
})
}
}
} catch (error) {
console.error('加载 API 数据失败:', error)
apiData.value = []
} finally {
loading.value = false
}
}
/**
* 加载字典数据
*/
async function loadDictionaryData() {
if (!props.dictionaryCode) {
return
}
// 检查缓存
if (dictionaryStore.dictionaries[props.dictionaryCode]) {
return
}
loading.value = true
try {
await dictionaryStore.getDictionary(props.dictionaryCode)
} catch (error) {
console.error('加载字典数据失败:', error)
} finally {
loading.value = false
}
}
/**
* 处理 focus 事件,按需加载数据
*/
async function handleFocus() {
switch (props.sourceType) {
case 'api':
if (!apiData.value || apiData.value.length === 0) {
await loadApiData()
}
break
case 'dictionary':
if (!dictionaryStore.dictionaries[props.dictionaryCode]) {
await loadDictionaryData()
}
break
}
}
/**
* 强制刷新数据
*/
async function refresh() {
switch (props.sourceType) {
case 'api':
await loadApiData()
break
case 'dictionary':
await dictionaryStore.getDictionary(props.dictionaryCode, true)
break
}
}
// 监听数据源变化
watch(
() => props.data,
(newVal) => {
if (props.sourceType === 'data') {
// data 类型不需要特殊处理,计算属性会自动更新
}
},
{ immediate: true }
)
watch(
() => props.api,
() => {
if (props.sourceType === 'api') {
apiData.value = null
}
}
)
watch(
() => props.apiParams,
() => {
if (props.sourceType === 'api') {
apiData.value = null
}
},
{ deep: true }
)
watch(
() => props.dictionaryCode,
() => {
if (props.sourceType === 'dictionary') {
// dictionary 变化时,数据会自动从 store 获取
}
}
)
// 组件挂载时立即加载数据
if (props.immediate) {
switch (props.sourceType) {
case 'api':
loadApiData()
break
case 'dictionary':
loadDictionaryData()
break
}
}
// 暴露方法供外部调用
defineExpose({
refresh,
loadApiData,
loadDictionaryData,
})
</script>
<style scoped lang="scss">
// 组件样式继承自 a-select
</style>

View File

@@ -2,10 +2,10 @@
<div class="sc-table" ref="tableWrapper">
<!-- 表格内容 -->
<div class="sc-table-content" ref="tableContent">
<a-table :columns="tableColumns" :data-source="dataSource" :loading="loading" :pagination="false"
<a-table v-if="dataSource.length > 0" :columns="tableColumns" :data-source="dataSource" :loading="loading" :pagination="false"
:row-key="rowKey" :row-selection="rowSelection" :scroll="scroll" :bordered="tableSettings.bordered"
:size="tableSettings.size" :show-header="showHeader" :locale="locale" @change="handleTableChange"
@resizeColumn="handleResizeColumn">
:size="tableSettings.size" :show-header="showHeader" :locale="locale" :expanded-row-keys="expandedRowKeys"
:expand-row-by-click="expandRowByClick" @change="handleTableChange" @resizeColumn="handleResizeColumn">
<!-- 自定义单元格内容 -->
<template #bodyCell="{ text, record, index, column }">
<!-- 序号列 -->
@@ -216,8 +216,20 @@ const props = defineProps({
type: String,
default: '暂无数据',
},
// 树形表格配置
defaultExpandAll: {
type: Boolean,
default: false,
},
expandRowByClick: {
type: Boolean,
default: false,
},
})
// 展开的行keys
const expandedRowKeys = ref([])
const tableContent = useTemplateRef('tableContent')
const tableWrapper = useTemplateRef('tableWrapper')
let scroll = ref({
@@ -226,6 +238,36 @@ let scroll = ref({
y: true,
})
// 递归获取所有节点的key
const getAllNodeKeys = (nodes) => {
const keys = []
const traverse = (list) => {
list.forEach(node => {
// 如果节点有children且不为空则该节点需要展开
if (node.children && node.children.length > 0) {
const key = typeof props.rowKey === 'function' ? props.rowKey(node) : node[props.rowKey]
keys.push(key)
traverse(node.children)
}
})
}
traverse(nodes)
return keys
}
// 监听数据变化,自动展开所有节点
watch(
() => props.dataSource,
(newData) => {
if (props.defaultExpandAll && newData && newData.length > 0) {
expandedRowKeys.value = getAllNodeKeys(newData)
} else {
expandedRowKeys.value = []
}
},
{ immediate: true, deep: true }
)
onMounted(() => {
updateTableHeight()
})

View File

@@ -70,6 +70,7 @@
:pagination="false"
:row-key="rowKey"
:row-selection="rowSelection"
:default-expand-all="true"
@refresh="refreshTable"
@select="handleSelectChange"
@selectAll="handleSelectAll"

View File

@@ -15,7 +15,7 @@
<a-input-number v-model:value="form.sort" :min="0" :step="1" style="width: 100%" placeholder="请输入排序" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="role_status" placeholder="请选择状态" allow-clear />
</a-form-item>
</a-form>
<template #footer>
@@ -30,6 +30,7 @@
<script setup>
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
import authApi from '@/api/auth'
const emit = defineEmits(['success', 'closed'])
@@ -50,15 +51,7 @@ const form = reactive({
code: '',
description: '',
sort: 1,
status: 1
})
// 状态开关计算属性
const statusChecked = computed({
get: () => form.status === 1,
set: (val) => {
form.status = val ? 1 : 0
}
status: null
})
// 表单引用
@@ -132,7 +125,7 @@ const setData = (data) => {
form.code = data.code
form.description = data.description || ''
form.sort = data.sort
form.status = data.status !== undefined ? data.status : 1
form.status = data.status !== undefined ? data.status : null
}
// 暴露方法给父组件

View File

@@ -38,8 +38,11 @@
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="性别" name="gender">
<sc-select v-model:value="form.gender" source-type="dictionary" dictionary-code="gender" placeholder="请选择性别" allow-clear />
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="user_status" placeholder="请选择状态" allow-clear />
</a-form-item>
</a-form>
<template #footer>
@@ -53,6 +56,7 @@
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import scUpload from '@/components/scUpload/index.vue'
import scSelect from '@/components/scSelect/index.vue'
import authApi from '@/api/auth'
const emit = defineEmits(['success', 'closed'])
@@ -76,15 +80,8 @@ const form = reactive({
phone: '',
department_id: null,
role_ids: [],
status: 1
})
// 状态开关计算属性
const statusChecked = computed({
get: () => form.status === 1,
set: (val) => {
form.status = val ? 1 : 0
}
gender: null,
status: null
})
// 表单引用
@@ -204,6 +201,7 @@ const submit = async () => {
phone: form.phone,
department_id: form.department_id,
role_ids: form.role_ids,
gender: form.gender,
status: form.status
}
@@ -242,7 +240,8 @@ const setData = (data) => {
form.phone = data.phone
form.department_id = data.department_id
form.role_ids = data.roles ? data.roles.map(item => item.id) : []
form.status = data.status !== undefined ? data.status : 1
form.gender = data.gender !== undefined ? data.gender : null
form.status = data.status !== undefined ? data.status : null
}
// 组件挂载时加载数据

View File

@@ -9,13 +9,14 @@
</a-input>
</div>
<div class="body">
<a-tree v-model:selectedKeys="selectedDeptKeys" :tree-data="filteredDepartmentTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }" show-line default-expand-all @select="onDeptSelect">
<a-tree v-if="filteredDepartmentTree.length > 0" v-model:selectedKeys="selectedDeptKeys" v-model:expandedKeys="expandedDeptKeys" :tree-data="filteredDepartmentTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }" show-line @select="onDeptSelect">
<template #icon="{ dataRef }">
<ApartmentOutlined v-if="dataRef.children && dataRef.children.length > 0" />
<UserOutlined v-else />
</template>
</a-tree>
<a-empty v-else description="暂无部门数据" />
</div>
</div>
<div class="right-box">
@@ -144,7 +145,7 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
SearchOutlined,
@@ -225,10 +226,40 @@ const departmentTree = ref([])
const filteredDepartmentTree = ref([])
const selectedDeptKeys = ref([])
const departmentKeyword = ref('')
const expandedDeptKeys = ref([])
// 行key
const rowKey = 'id'
// 递归获取所有部门节点的key
const getAllDepartmentKeys = (nodes) => {
const keys = []
const traverse = (list) => {
list.forEach(node => {
// 如果节点有children且不为空则该节点需要展开
if (node.children && node.children.length > 0) {
keys.push(node.id)
traverse(node.children)
}
})
}
traverse(nodes)
return keys
}
// 监听部门树数据变化,自动展开所有节点
watch(
() => filteredDepartmentTree.value,
(newData) => {
if (newData && newData.length > 0) {
expandedDeptKeys.value = getAllDepartmentKeys(newData)
} else {
expandedDeptKeys.value = []
}
},
{ immediate: true, deep: true }
)
// 表格列配置
const columns = [
{ title: '头像', dataIndex: 'avatar', key: 'avatar', width: 80, align: 'center', slot: 'avatar' },

View File

@@ -57,6 +57,7 @@ import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { useDictionaryStore } from '@/stores/modules/dictionary'
import auth from '@/api/auth'
import config from '@/config'
import '@/assets/style/auth.scss'
@@ -71,6 +72,7 @@ const loginFormRef = ref(null)
const loading = ref(false)
const userStore = useUserStore()
const dictionaryStore = useDictionaryStore()
// Login form data
const loginForm = reactive({
@@ -133,6 +135,11 @@ const handleLogin = async () => {
userStore.setPermissions(loginData.permissions)
}
// 4. Load dictionary data (缓存字典数据)
dictionaryStore.loadAllDictionaries().catch(error => {
console.error('加载字典数据失败:', error)
})
// Success message
message.success('登录成功!')

View File

@@ -86,7 +86,7 @@
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import systemApi from '@/api/system'
import dictionaryCache from '@/utils/dictionaryCache'
import { useDictionaryStore } from '@/stores/modules/dictionary'
const props = defineProps({
visible: {
@@ -146,9 +146,12 @@ const valueChecked = computed({
}
})
// 初始化字典 store
const dictionaryStore = useDictionaryStore()
// 加载配置分组
const loadGroups = async () => {
const groups = await dictionaryCache.getItemsByCode('config_group')
const groups = await dictionaryStore.getDictionary('config_group')
groupOptions.value = groups.map(item => ({
label: item.label,
value: item.value

View File

@@ -20,7 +20,7 @@
<!-- 状态 -->
<a-form-item label="状态" name="status">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="dictionary_status" placeholder="请选择状态" allow-clear />
</a-form-item>
<!-- 描述 -->
@@ -43,6 +43,7 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
import systemApi from '@/api/system'
// ===== Props =====
@@ -79,18 +80,10 @@ const form = ref({
name: '',
code: '',
description: '',
status: true,
status: null,
sort: 0
})
// ===== 计算属性:状态开关 =====
const statusChecked = computed({
get: () => form.value.status === true,
set: (val) => {
form.value.status = val ? true : false
}
})
// ===== 验证规则 =====
// 编码唯一性验证
const validateCodeUnique = async (rule, value) => {
@@ -131,7 +124,7 @@ const resetForm = () => {
name: '',
code: '',
description: '',
status: true,
status: null,
sort: 0
}
formRef.value?.clearValidate()
@@ -145,7 +138,7 @@ const setData = (data) => {
name: data.name || '',
code: data.code || '',
description: data.description || '',
status: data.status !== undefined ? data.status : true,
status: data.status !== undefined ? data.status : null,
sort: data.sort !== undefined ? data.sort : 0
}
}

View File

@@ -178,7 +178,7 @@ import systemApi from '@/api/system'
import scTable from '@/components/scTable/index.vue'
import DictionaryDialog from './components/DictionaryDialog.vue'
import ItemDialog from './components/ItemDialog.vue'
import dictionaryCache from '@/utils/dictionaryCache'
import { useDictionaryStore } from '@/stores/modules/dictionary'
// ===== 字典列表相关 =====
const dictionaryList = ref([])
@@ -250,6 +250,8 @@ const loadDictionaryList = async () => {
if (res.code === 200) {
dictionaryList.value = res.data || []
filteredDictionaries.value = res.data || []
// 建立字典ID到Code的映射
dictionaryStore.buildIdToCodeMap(res.data || [])
} else {
message.error(res.message || '加载字典列表失败')
}
@@ -388,6 +390,8 @@ const handleDeleteItem = async (record) => {
const res = await systemApi.dictionaryItems.delete.delete(record.id)
if (res.code === 200) {
message.success('删除成功')
// 清除字典缓存
dictionaryStore.clearDictionary(selectedDictionaryId.value)
refreshTable()
// 刷新字典列表以更新项数量
loadDictionaryList()
@@ -420,6 +424,8 @@ const handleBatchDelete = () => {
const res = await systemApi.dictionaryItems.batchDelete.post({ ids })
if (res.code === 200) {
message.success('删除成功')
// 清除字典缓存
dictionaryStore.clearDictionary(selectedDictionaryId.value)
selectedRows.value = []
refreshTable()
loadDictionaryList()
@@ -458,6 +464,8 @@ const handleBatchStatus = () => {
})
if (res.code === 200) {
message.success(`${statusText}成功`)
// 清除字典缓存
dictionaryStore.clearDictionary(selectedDictionaryId.value)
selectedRows.value = []
refreshTable()
} else {
@@ -471,11 +479,14 @@ const handleBatchStatus = () => {
})
}
// 初始化字典 store
const dictionaryStore = useDictionaryStore()
// ===== 方法:字典操作成功回调 =====
const handleDictionarySuccess = () => {
dialog.dictionary = false
// 清理字典缓存
dictionaryCache.clearDictionary()
dictionaryStore.clearCache()
loadDictionaryList()
}
@@ -483,7 +494,7 @@ const handleDictionarySuccess = () => {
const handleItemSuccess = () => {
dialog.item = false
// 清理字典缓存
dictionaryCache.clearDictionary(selectedDictionaryId.value)
dictionaryStore.clearDictionary(selectedDictionaryId.value)
refreshTable()
loadDictionaryList()
}

View File

@@ -71,7 +71,7 @@
<!-- 启用状态 -->
<a-form-item label="启用状态" name="is_active">
<a-switch v-model:checked="isActiveChecked" checked-children="启用" un-checked-children="禁用" />
<sc-select v-model:value="form.is_active" source-type="dictionary" dictionary-code="yes_no" placeholder="请选择状态" allow-clear />
</a-form-item>
<!-- 排序 -->
@@ -93,6 +93,7 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
import systemApi from '@/api/system'
const props = defineProps({
@@ -125,42 +126,13 @@ const form = ref({
expression: '* * * * *',
timezone: 'Asia/Shanghai',
description: '',
is_active: true,
is_active: null,
run_in_background: false,
without_overlapping: false,
only_one: false,
sort: 0
})
// 计算属性:开关
const isActiveChecked = computed({
get: () => form.value.is_active === true,
set: (val) => {
form.value.is_active = val ? true : false
}
})
const runInBackgroundChecked = computed({
get: () => form.value.run_in_background === true,
set: (val) => {
form.value.run_in_background = val ? true : false
}
})
const withoutOverlappingChecked = computed({
get: () => form.value.without_overlapping === true,
set: (val) => {
form.value.without_overlapping = val ? true : false
}
})
const onlyOneChecked = computed({
get: () => form.value.only_one === true,
set: (val) => {
form.value.only_one = val ? true : false
}
})
// Cron 表达式验证函数
const validateCronExpression = (rule, value) => {
if (!value || !value.trim()) {
@@ -304,7 +276,7 @@ const resetForm = () => {
expression: '* * * * *',
timezone: 'Asia/Shanghai',
description: '',
is_active: true,
is_active: null,
run_in_background: false,
without_overlapping: false,
only_one: false,
@@ -324,7 +296,7 @@ const setData = (data) => {
expression: data.expression || '* * * * *',
timezone: data.timezone || 'Asia/Shanghai',
description: data.description || '',
is_active: data.is_active !== undefined ? data.is_active : true,
is_active: data.is_active !== undefined ? data.is_active : null,
run_in_background: data.run_in_background || false,
without_overlapping: data.without_overlapping || false,
only_one: data.only_one || false,

View File

@@ -0,0 +1,199 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { customStorage } from '../persist'
import systemApi from '@/api/system'
export const useDictionaryStore = defineStore(
'dictionary',
() => {
// 字典数据缓存(按 code 缓存字典项列表)
const dictionaries = ref({})
// 字典元数据缓存(按 code 缓存字典信息)
const dictionaryMeta = ref({})
// 字典ID到Code的映射用于通过ID清除缓存
const dictionaryIdToCodeMap = ref({})
// 字典数据加载状态
const loading = ref(false)
// 最后加载时间
const lastLoadTime = ref(null)
/**
* 加载所有字典数据
*/
async function loadAllDictionaries(forceRefresh = false) {
// 如果已加载且不是强制刷新,直接返回
if (!forceRefresh && Object.keys(dictionaries.value).length > 0) {
return
}
if (loading.value) return
loading.value = true
try {
const res = await systemApi.dictionaryItems.all.get()
if (res.code === 200 && res.data) {
// 将字典数据按 code 缓存
const dictMap = {}
const metaMap = {}
res.data.forEach(dict => {
if (dict.code) {
// 缓存字典项列表
dictMap[dict.code] = dict.items || []
// 缓存字典元数据包含id以便通过id清除缓存
metaMap[dict.code] = {
id: dict.code, // 使用code作为唯一标识
name: dict.name,
code: dict.code,
description: dict.description
}
}
})
dictionaries.value = dictMap
dictionaryMeta.value = metaMap
lastLoadTime.value = new Date().getTime()
}
} catch (error) {
console.error('加载字典数据失败:', error)
} finally {
loading.value = false
}
}
/**
* 根据字典 code 获取字典数据
* @param {string} code 字典编码
* @param {boolean} forceRefresh 是否强制刷新
* @returns {Array} 字典数据数组
*/
async function getDictionary(code, forceRefresh = false) {
// 如果缓存为空或强制刷新,则重新加载所有字典
if (!dictionaries.value || Object.keys(dictionaries.value).length === 0 || forceRefresh) {
await loadAllDictionaries()
}
return dictionaries.value[code] || []
}
/**
* 批量获取字典数据
* @param {Array<string>} codes 字典编码数组
* @param {boolean} forceRefresh 是否强制刷新
* @returns {Object} 字典数据对象
*/
async function getDictionaries(codes, forceRefresh = false) {
// 如果缓存为空或强制刷新,则重新加载所有字典
if (!dictionaries.value || Object.keys(dictionaries.value).length === 0 || forceRefresh) {
await loadAllDictionaries()
}
const result = {}
codes.forEach(code => {
result[code] = dictionaries.value[code] || []
})
return result
}
/**
* 根据字典 code 和 value 获取 label
* @param {string} code 字典编码
* @param {any} value 字典值
* @returns {string} 字典标签
*/
function getLabelByValue(code, value) {
const dict = dictionaries.value[code]
if (!dict) return value
const item = dict.find(item => item.value === value)
return item ? item.label : value
}
/**
* 清空字典缓存
*/
function clearCache() {
dictionaries.value = {}
dictionaryMeta.value = {}
dictionaryIdToCodeMap.value = {}
lastLoadTime.value = null
}
/**
* 清除特定字典的缓存
* @param {string|number} dictionaryIdOrCode 字典ID或编码
*/
function clearDictionary(dictionaryIdOrCode) {
// 如果传入的是字典ID需要先找到对应的code
let code = dictionaryIdOrCode
if (typeof dictionaryIdOrCode === 'number') {
// 直接从ID映射表中查找
code = dictionaryIdToCodeMap.value[dictionaryIdOrCode]
}
// 删除对应字典的缓存
if (code && dictionaries.value[code]) {
delete dictionaries.value[code]
delete dictionaryMeta.value[code]
if (dictionaryIdOrCode && typeof dictionaryIdOrCode === 'number') {
delete dictionaryIdToCodeMap.value[dictionaryIdOrCode]
}
}
}
/**
* 建立字典ID到Code的映射
* @param {Array} dictionaryList 字典列表包含id和code
*/
function buildIdToCodeMap(dictionaryList) {
dictionaryList.forEach(dict => {
if (dict.id && dict.code) {
dictionaryIdToCodeMap.value[dict.id] = dict.code
}
})
}
/**
* 刷新字典缓存
* @param {boolean} force 是否强制刷新
*/
async function refresh(force = true) {
await loadAllDictionaries(force)
}
/**
* 获取缓存信息
*/
function getCacheInfo() {
return {
count: Object.keys(dictionaries.value).length,
lastLoadTime: lastLoadTime.value
}
}
return {
dictionaries,
dictionaryMeta,
dictionaryIdToCodeMap,
loading,
lastLoadTime,
loadAllDictionaries,
getDictionary,
getDictionaries,
getLabelByValue,
clearCache,
clearDictionary,
buildIdToCodeMap,
refresh,
getCacheInfo
}
},
{
persist: {
key: 'dictionary-store',
storage: customStorage,
pick: ['dictionaries', 'lastLoadTime']
}
}
)

View File

@@ -1,147 +0,0 @@
/**
* 数据字典缓存工具
* 用于缓存和管理数据字典数据
*/
import systemApi from '@/api/system'
// 缓存存储
const cacheStorage = new Map()
// 缓存过期时间毫秒默认1小时
const CACHE_EXPIRE_TIME = 3600 * 1000
/**
* 字典缓存管理类
*/
class DictionaryCacheManager {
/**
* 获取所有字典(带缓存)
*/
async getAll() {
const cacheKey = 'all'
const cached = this.get(cacheKey)
if (cached) {
return cached
}
const res = await systemApi.dictionaries.all.get()
if (res.code === 200) {
const data = res.data || []
this.set(cacheKey, data)
return data
}
return []
}
/**
* 根据编码获取字典项(带缓存)
*/
async getItemsByCode(code) {
const cacheKey = `items:${code}`
const cached = this.get(cacheKey)
if (cached) {
return cached
}
const res = await systemApi.public.dictionaries.code.get({ code })
if (res.code === 200) {
const data = res.data || []
this.set(cacheKey, data)
return data
}
return []
}
/**
* 根据编码获取字典(带缓存)
*/
async getByCode(code) {
const cacheKey = `code:${code}`
const cached = this.get(cacheKey)
if (cached) {
return cached
}
const all = await this.getAll()
const dictionary = all.find((item) => item.code === code)
if (dictionary) {
this.set(cacheKey, dictionary)
}
return dictionary
}
/**
* 根据编码获取字典项的标签
*/
async getLabelByCode(code, value) {
const items = await this.getItemsByCode(code)
const item = items.find((item) => item.value === value)
return item ? item.label : value
}
/**
* 清除所有缓存
*/
clear() {
cacheStorage.clear()
}
/**
* 清除指定缓存
*/
delete(key) {
cacheStorage.delete(key)
}
/**
* 清除字典相关缓存
*/
clearDictionary(dictionaryId) {
// 清除特定字典的缓存暂时清除所有因为前端不知道字典ID对应的code
this.clear()
}
/**
* 获取缓存
*/
get(key) {
const item = cacheStorage.get(key)
if (!item) {
return null
}
// 检查是否过期
if (Date.now() > item.expires) {
cacheStorage.delete(key)
return null
}
return item.data
}
/**
* 设置缓存
*/
set(key, data) {
const item = {
data,
expires: Date.now() + CACHE_EXPIRE_TIME
}
cacheStorage.set(key, item)
}
}
// 创建单例实例
const dictionaryCache = new DictionaryCacheManager()
export default dictionaryCache