first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# 字典使用示例
|
||||
@@ -0,0 +1,393 @@
|
||||
# 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` - 导入字典库
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-button @click="handleCopy">复制数据</el-button>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
//一键复制代码
|
||||
function handleCopy() {
|
||||
if (!props.model) return
|
||||
|
||||
const data = [
|
||||
`客户名称: ${props.model.name || '-'}`,
|
||||
`联系人: ${props.model.contact || '-'}`,
|
||||
`电话: ${props.model.phone || '-'}`,
|
||||
`邮箱: ${props.model.email || '-'}`,
|
||||
`客户类型: ${getLabel(customerTypeOptions.value, props.model.customer_type)}`,
|
||||
`客户等级: ${getLabel(customerLevelOptions.value, props.model.customer_level)}`,
|
||||
`所属行业: ${getLabel(industryOptions.value, props.model.industry)}`,
|
||||
`客户状态: ${getLabel(customerStatusOptions.value, String(props.model.status ?? '')) || ((props.model.status===1||props.model.status==='1') ? '正常' : '停用')}`,
|
||||
`地址: ${props.model.address || '-'}`,
|
||||
`经营范围: ${props.model.business_scope || '-'}`
|
||||
].join('\n')
|
||||
|
||||
navigator.clipboard.writeText(data).then(() => {
|
||||
ElMessage.success('数据已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
//拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
用例:
|
||||
<img :src="getEnvUrl(module.thumb)" :alt="module.title" />
|
||||
|
||||
|
||||
const url = getEnvUrl('/admin/moduleCenter/modules');
|
||||
@@ -0,0 +1,12 @@
|
||||
import { onMounted } from "vue";
|
||||
import { getMenus } from "@/api/menu";
|
||||
|
||||
onMounted(async () => {
|
||||
try{
|
||||
const response = await getMenus();
|
||||
} catch (error) {
|
||||
console.error('获取菜单数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// 使用 auth store 获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 获取租户ID
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = authStore.user;
|
||||
if (userInfo && userInfo.id) {
|
||||
console.log('用户名:', userInfo.username || userInfo.nickname);
|
||||
console.log('用户ID:', userInfo.id);
|
||||
console.log('角色:', userInfo.role);
|
||||
} else {
|
||||
console.log('未找到用户信息或用户未登录');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo();
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card"
|
||||
:headers="uploadHeaders" :limit="1" :auto-upload="false" :before-upload="beforeImgUpload"
|
||||
:on-success="handleImgSuccess">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
import { uploadFile } from '@/api/file.js';
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleImgUpload(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('cate', 'article')
|
||||
|
||||
uploadFile(formData).then((res: any) => {
|
||||
if (res?.url) {
|
||||
formData.image = res.url
|
||||
fileList.value = [{
|
||||
name: file.name,
|
||||
url: res.url
|
||||
}]
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
ElMessage.error('上传失败:' + (error.msg || '未知错误'))
|
||||
})
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
````
|
||||
<template>
|
||||
{{ (xxxxxxxxDict.find(item => item.dict_value == String(model?.status)) || {}).dict_label || '-' }}
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
|
||||
// 字典store
|
||||
const dictStore = useDictStore();
|
||||
const xxxxxxxxDict = ref<any[]>([]);
|
||||
|
||||
// 获取审核状态字典
|
||||
const fetchxxxxxxxxDict = async () => {
|
||||
try {
|
||||
xxxxxxxxDict.value = await dictStore.getDictItems('article_status');
|
||||
console.log(xxxxxxxxDict.value);
|
||||
} catch (err) {
|
||||
console.error('获取文章状态字典失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchxxxxxxxxDict();
|
||||
});
|
||||
</script>
|
||||
````
|
||||
Reference in New Issue
Block a user