This commit is contained in:
zhouwentao 2026-01-31 19:31:41 +08:00
parent a67857a012
commit 1b7baf85df
13 changed files with 1762 additions and 40 deletions

226
IFLOW.md Normal file
View File

@ -0,0 +1,226 @@
# IFLOW.md - 核心工作规则
## Global Protocols
所有操作必须严格遵循以下系统约束:
- **交互语言**:技术术语、工具与模型交互强制使用 **English**;用户输出强制使用 **中文**
- **最小改动**:仅对需求做针对性改动,严禁影响用户现有的其他功能。
- **风格一致**:遵循项目现有的代码风格,使用项目已有的工具函数。
## Tool Priority
在执行任何操作前,必须按照以下顺序选择工具,严禁跳级使用:
**1. MCP 工具**:当 MCP 工具能够完成任务时,必须使用 MCP禁止降级到内置工具或 Shell 命令。
**2. 内置工具**:仅当 MCP 工具**无法覆盖**该功能时,使用内置工具。
**3. Shell 命令**Shell 命令是最后手段,同时遵循以下规则:
- 只读类安全操作允许直接执行
| 类别 | 安全操作示例 |
| ---------------- | ------------------------------------------------- |
| Git 只读操作 | `git status`、`git log`、`git diff`、`git branch` |
| 包管理器只读操作 | `npm list`、`pnpm why`、`pip show` |
| 容器只读操作 | `docker ps`、`docker logs` |
| 环境检查 | `node -v`、`python -version`、`which xxx` |
- 写入/删除/修改/安装等危险操作必须征得用户同意
| 类别 | 危险操作示例 |
| ------------ | ------------------------------------------------------------ |
| Git 写操作 | `commit`、`push`、`pull`、`merge`、`rebase`、`reset`、`checkout <branch>` |
| 文件删除 | `rm`、`rmdir`、清空目录 |
| 批量文件修改 | `sed -i`(多文件)、批量重命名 |
| 包管理写操作 | `pnpm install/uninstall`、`pnpm add/remove`、`uv add/remove` |
| 容器写操作 | `docker rm`、`docker rmi`、`docker-compose down` |
| 系统级操作 | 修改环境变量、修改系统配置文件 |
- 触发危险操作时告知用户
```
# 告知示例
!!!即将执行危险操作!!!
命令git push origin main
影响:将本地 main 分支的提交推送到远程仓库
是否继续?请回复"确认"或"取消"
```
## Technology Stack
如果是对已有项目二次开发/修改bug则遵循项目已有技术栈。
如果是从0到1开发新的项目尽可能使用下方给出的技术栈
### 后端 - Go主力
| 配置项 | 要求 |
| -------- | -------------------------------------- |
| 语言版本 | Go 1.21+ |
| 开发框架 | Gin |
| ORM框架 | GORM |
| 代码规范 | Google Go 编程规范 |
### 后端 - Java
| 配置项 | 要求 |
| -------- | -------------------------------------- |
| 语言版本 | Java 17 |
| 开发框架 | Spring Boot 3.x + Spring Cloud Alibaba |
| ORM框架 | MyBatis Plus |
| 包管理器 | Maven |
| 代码规范 | 阿里巴巴Java开发手册嵩山版 |
### 后端 - Python辅助/小工具)
| 配置项 | 要求 |
| ---------- | ------------------------------------------------------------ |
| 语言版本 | Python 3.10+ |
| 开发框架 | FastAPI轻量级API/ TyperCLI工具/ Streamlit数据可视化 |
| 包管理工具 | uv |
| 代码规范 | PEP 8 + Google Python Style Guide |
| 虚拟环境 | **强制启用**uv venv |
### 后端 - 其他组件
| 组件 | 选型 |
| -------- | --------- |
| 数据库 | MySQL 8.x |
| 缓存 | Redis |
### 前端 - TypeScript + Vue 3
| 配置项 | 要求 |
| -------- | ---------------------------- |
| 语言版本 | TypeScript 5.x |
| 开发框架 | Vue 3Composition API |
| UI组件库 | TailWind CSS |
| 包管理器 | pnpm |
| 构建工具 | Vite |
| 代码规范 | ESLint严格模式+ Prettier |
### 桌面端 - Electron
| 配置项 | 要求 |
| -------- | ------------------ |
| 基础框架 | Vue 3 + TypeScript |
| 打包工具 | electron-builder |
## Workflow
在开发过程中,严格按照以下阶段顺序执行任务。
**格式要求**: 每次回复必须在开头标注 `【当前阶段: [阶段名称]】`
---
### Phase 0上下文全量检索
**执行条件**:在生成任何建议或代码前。
**调用工具**`mcp__auggie-mcp__codebase-retrieval`
**检索策略**
- 禁止基于假设Assumption回答。
- 使用自然语言NL构建语义查询Where/What/How
- **完整性检查**:必须获取相关类、函数、变量的完整定义与签名。若上下文不足,触发递归检索。
**需求对齐**:若检索后需求仍有模糊空间,**必须**向用户输出引导性问题列表,直至需求边界清晰(无遗漏、无冗余)。
---
### Phase 1 产品需求分析
**角色**:产品经理
**方法**:通过`AskUserQuestion`工具进行多轮提问引导,直到需求完全量化。
**最小维度**
- 目标用户与使用场景。
- 核心功能清单(按优先级 P0/P1/P2 排列)。
- 业务规则与约束条件。
**输出**`requirement.md`(需求规格书)
---
### Phase 2 UI/UX 设计
**角色**UI/UX 设计师
**方法**:基于`requirement.md`,通过多轮提问引导,定义交互与视觉规范。
**最小维度**
- 核心用户流程。
- 页面结构与布局。
- 组件状态定义。
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
**输出**`ui_ux_specifications.md`UI/UX 规范)
---
### Phase 3 架构设计
**角色**:系统架构师
**方法**:基于`requirement.md`和`ui_ux_specifications.md`,通过多轮提问引导,设计技术方案。
**最小维度**
- 技术栈选型(遵循本文档`Technology Stack`章节)。
- 系统分层、模块划分、目录结构。
- API 契约定义。
**冲突检测**:与`requirement.md`中的约束进行一致性校验,如有冲突,必须提问澄清后再继续。
**输出**`architecture_design_document.md`(架构设计文档)
---
### Phase 4 代码实现
**角色**:全栈开发工程师
**方法**
1. 根据 `requirement.md``architecture_design_document.md`,拆分开发任务
2. 在 `task_list.md` 中记录任务清单,将**待开发/已开发/跳过**的任务通过不同的复选框进行标记
3. 逐个任务开发,每个任务完成后更新状态
**输出**`task_list.md`(任务清单,持续更新)、`deployment.md`(部署文档)
---
### Phase 5 代码审计
**执行条件**:每个任务模块开发完成后进行增量审计,全部完成后进行最终审计。
**角色**:代码审计工程师
**方法**:根据`task_list.md`,逐个对已完成代码进行 Code Review。
**审计范围**
- 功能完整性:是否覆盖`requirement.md`对应功能的全部需求
- 代码质量:命名规范、无重复代码、适当抽象、注释完整
- 安全检查输入验证、SQL注入防护、XSS防护、敏感数据处理、权限控制
- 性能检查:算法效率、数据库查询优化、资源释放
**问题分级与处理**
| 级别 | 定义 | 处理方式 |
| ---- | -------------------------------- | ------------------ |
| P0 | 安全漏洞、数据风险、核心功能缺失 | 阻断发布,立即修复 |
| P1 | 功能不完整、明显性能问题 | 当前迭代必须修复 |
| P2 | 代码规范、可维护性问题 | 可选 |
| P3 | 优化建议 | 可选 |
**输出**`audit_report.md`(审计报告)、`fix_changelog.md`(修复记录)

162
docs/dict-system.md Normal file
View File

@ -0,0 +1,162 @@
# 字典系统使用指南
## 概述
字典系统是为了解决项目中数据字典不统一、硬编码问题而设计的统一管理方案。系统支持静态字典本地定义和动态字典API获取并提供了便捷的访问接口。
## 系统架构
字典系统由以下几个部分组成:
1. **工具类** (`src/utils/dict.ts`) - 提供静态字典数据和工具函数
2. **Store** (`src/stores/dict.ts`) - 管理动态字典数据与现有Pinia架构集成
3. **API服务** (`src/service/api/dict.ts`) - 提供动态字典获取接口
4. **组件示例** - 展示如何在组件中使用字典系统
## 使用方法
### 1. 在组件中使用字典
```typescript
<script setup lang="ts">
import { useDictStore } from '~/stores/dict'
import { getDictLabel, getDictColor } from '~/utils/dict'
const dictStore = useDictStore()
// 获取字典项列表
const professionalCategoryItems = computed(() => dictStore.getDictItems('professionalCategory'))
// 获取字典项标签
const label = dictStore.getDictLabel('professionalCategory', 'science')
// 获取字典项颜色
const color = dictStore.getDictColor('educationalLevel', 'undergraduate')
</script>
```
### 2. 在模板中使用
```vue
<template>
<select v-model="selectedValue">
<option
v-for="item in dictStore.getDictItems('professionalCategory')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
<!-- 显示标签 -->
<span>选中值: {{ dictStore.getDictLabel('professionalCategory', selectedValue) }}</span>
</template>
```
### 3. 静态字典工具函数
```typescript
// 获取字典项列表
const items = getDictItems('professionalCategory')
// 获取字典标签
const label = getDictLabel('professionalCategory', 'science')
// 获取字典值
const value = getDictValue('professionalCategory', '理工类')
// 获取字典颜色
const color = getDictColor('professionalCategory', 'science')
// 获取字典项对象
const item = getDictItem('professionalCategory', 'science')
```
### 4. 动态字典管理
```typescript
// 加载动态字典
await dictStore.loadDynamicDicts()
// 加载特定类型的动态字典
await dictStore.loadDynamicDicts(['dynamic_type1', 'dynamic_type2'])
// 手动设置动态字典
dictStore.setDynamicDict('custom_type', [
{ label: '自定义项1', value: 'custom1' },
{ label: '自定义项2', value: 'custom2' }
])
// 清空动态字典
dictStore.clearDynamicDicts()
```
## 字典数据结构
字典项接口定义:
```typescript
interface DictItem {
label: string // 显示标签
value: string | number // 实际值
disabled?: boolean // 是否禁用
color?: string // 颜色值
order?: number // 排序
[key: string]: any // 扩展属性
}
```
## 静态字典类型
目前系统已内置以下静态字典:
- `professionalCategory` - 专业类别
- `educationalLevel` - 学历层次
- `provinces` - 省份
- `subjectList` - 科目列表
- `gender` - 性别
- `status` - 状态
- `type` - 类型
## 动态字典API
动态字典通过API获取支持以下接口
- `GET /dict/list` - 获取字典列表
- `GET /dict/type/{type}` - 获取指定类型的字典
## 在现有组件中集成
以ScoreForm.vue为例展示了如何将现有硬编码的选项替换为字典系统
1. 导入字典Store
2. 使用computed属性获取字典项
3. 在模板中使用字典数据
## 扩展字典类型
如需添加新的静态字典类型:
1. 在 `src/utils/dict.ts` 中的 `staticDicts` 对象中添加新类型
2. 确保遵循 `DictItem[]` 的数据结构
如需添加新的动态字典类型:
1. 在后端API中提供相应的字典数据接口
2. 在前端调用 `dictStore.loadDynamicDicts()` 时指定类型
## 最佳实践
1. **优先使用字典系统** - 避免在代码中硬编码选项数据
2. **统一管理** - 所有字典数据通过字典系统统一管理
3. **性能优化** - 字典数据通常在应用初始化时加载一次,后续直接使用
4. **扩展性** - 支持静态和动态字典,满足不同业务场景需求
5. **类型安全** - 提供完整的TypeScript类型定义
## 注意事项
1. 动态字典加载失败时,系统会继续使用静态字典数据
2. 字典数据在应用生命周期内会被缓存,避免重复请求
3. 在组件中使用字典前,确保字典数据已加载完成
4. 动态字典会覆盖同名的静态字典数据

View File

@ -169,6 +169,7 @@ declare global {
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDictStore: typeof import('./stores/dict')['useDictStore']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable'] const useDraggable: typeof import('@vueuse/core')['useDraggable']
@ -495,6 +496,7 @@ declare module 'vue' {
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']> readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']> readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']> readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDictStore: UnwrapRef<typeof import('./stores/dict')['useDictStore']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']> readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']> readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']> readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>

2
src/components.d.ts vendored
View File

@ -10,11 +10,13 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
BackToTop: typeof import('./components/BackToTop.vue')['default'] BackToTop: typeof import('./components/BackToTop.vue')['default']
copy: typeof import('./components/ScoreForm copy.vue')['default'] copy: typeof import('./components/ScoreForm copy.vue')['default']
DictDemo: typeof import('./components/DictDemo.vue')['default']
FilterBar: typeof import('./components/FilterBar.vue')['default'] FilterBar: typeof import('./components/FilterBar.vue')['default']
LoginForm: typeof import('./components/LoginForm.vue')['default'] LoginForm: typeof import('./components/LoginForm.vue')['default']
README: typeof import('./components/README.md')['default'] README: typeof import('./components/README.md')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
ScoreDictForm: typeof import('./components/ScoreDictForm.vue')['default']
ScoreForm: typeof import('./components/ScoreForm.vue')['default'] ScoreForm: typeof import('./components/ScoreForm.vue')['default']
TheCounter: typeof import('./components/TheCounter.vue')['default'] TheCounter: typeof import('./components/TheCounter.vue')['default']
TheFooter: typeof import('./components/TheFooter.vue')['default'] TheFooter: typeof import('./components/TheFooter.vue')['default']

192
src/components/DictDemo.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<div class="dict-demo-container p-6">
<h2 class="text-xl font-bold mb-4">字典系统使用示例</h2>
<!-- 使用静态字典 -->
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2">1. 使用静态字典</h3>
<!-- 专业类别选择 -->
<div class="mb-4">
<label class="block mb-1">专业类别:</label>
<select
v-model="selectedProfessionalCategory"
class="border rounded px-3 py-2 w-64"
@change="onProfessionalCategoryChange"
>
<option value="">请选择专业类别</option>
<option
v-for="item in dictItems.professionalCategory"
:key="item.value"
:value="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</option>
</select>
<div class="mt-1 text-sm text-gray-600">
选中的标签: {{ selectedProfessionalCategoryLabel }}
</div>
</div>
<!-- 学历层次选择 -->
<div class="mb-4">
<label class="block mb-1">学历层次:</label>
<select
v-model="selectedEducationalLevel"
class="border rounded px-3 py-2 w-64"
>
<option value="">请选择学历层次</option>
<option
v-for="item in dictItems.educationalLevel"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
<div class="mt-1 text-sm text-gray-600">
选中的颜色:
<span
:style="{ color: selectedEducationalLevelColor }"
class="font-semibold"
>
{{ selectedEducationalLevelColor }}
</span>
</div>
</div>
</div>
<!-- 使用字典Store -->
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2">2. 使用字典Store</h3>
<div class="mb-4">
<label class="block mb-1">省份选择:</label>
<select
v-model="selectedProvince"
class="border rounded px-3 py-2 w-64"
>
<option value="">请选择省份</option>
<option
v-for="item in dictStoreItems.provinces"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
<div class="mt-1 text-sm text-gray-600">
选中的标签: {{ selectedProvinceLabel }}
</div>
</div>
<button
@click="loadDynamicDicts"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 mr-2"
:disabled="loading"
>
{{ loading ? '加载中...' : '加载动态字典' }}
</button>
<span v-if="loadingMsg" class="text-sm text-gray-600">{{ loadingMsg }}</span>
</div>
<!-- 字典项展示 -->
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2">3. 字典项展示</h3>
<div class="grid grid-cols-3 gap-4">
<div v-for="dictType in dictTypes" :key="dictType" class="border rounded p-3">
<h4 class="font-medium mb-2">{{ dictType }}</h4>
<ul class="text-sm">
<li
v-for="item in getDictItems(dictType)"
:key="item.value"
class="py-1"
>
<span
v-if="item.color"
class="inline-block w-3 h-3 rounded-full mr-1"
:style="{ backgroundColor: item.color }"
></span>
{{ item.label }} ({{ item.value }})
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getDictItems, getDictLabel, getDictColor } from '~/utils/dict'
import { useDictStore } from '~/stores/dict'
// 使Store
const dictStore = useDictStore()
//
const dictItems = {
professionalCategory: getDictItems('professionalCategory'),
educationalLevel: getDictItems('educationalLevel'),
}
//
const selectedProfessionalCategory = ref('')
const selectedEducationalLevel = ref('')
const selectedProvince = ref('')
const loading = ref(false)
const loadingMsg = ref('')
//
const selectedProfessionalCategoryLabel = computed(() => {
return getDictLabel('professionalCategory', selectedProfessionalCategory.value)
})
const selectedEducationalLevelColor = computed(() => {
return getDictColor('educationalLevel', selectedEducationalLevel.value)
})
const selectedProvinceLabel = computed(() => {
return dictStore.getDictLabel('provinces', selectedProvince.value)
})
const dictStoreItems = computed(() => ({
provinces: dictStore.getDictItems('provinces')
}))
const dictTypes = ['professionalCategory', 'educationalLevel', 'subjectList', 'gender', 'status']
//
const onProfessionalCategoryChange = () => {
console.log('选中的专业类别:', selectedProfessionalCategory.value)
}
const loadDynamicDicts = async () => {
loading.value = true
loadingMsg.value = '正在加载动态字典...'
try {
await dictStore.loadDynamicDicts()
loadingMsg.value = '动态字典加载完成'
} catch (error) {
console.error('加载动态字典失败:', error)
loadingMsg.value = '加载失败,请检查控制台'
} finally {
loading.value = false
}
}
//
onMounted(() => {
//
// dictStore.loadDynamicDicts(['dynamic_type1', 'dynamic_type2'])
})
</script>
<style scoped>
.dict-demo-container {
max-width: 1200px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<div class="score-form-container p-6 max-w-4xl mx-auto">
<h2 class="text-2xl font-bold mb-6">成绩信息表单</h2>
<form @submit.prevent="submitForm" class="space-y-6">
<!-- 省份 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">省份 *</label>
<select
v-model="formData.province"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择省份</option>
<option
v-for="item in dictStore.getDictItems('provinces')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<!-- 学历层次 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">学历层次 *</label>
<select
v-model="formData.educationalLevel"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择学历层次</option>
<option
v-for="item in dictStore.getDictItems('educationalLevel')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<!-- 专业类别 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">专业类别 *</label>
<select
v-model="formData.professionalCategory"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择专业类别</option>
<option
v-for="item in dictStore.getDictItems('professionalCategory')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<!-- 科目选择多选 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">科目列表 *</label>
<div class="grid grid-cols-3 gap-2">
<label
v-for="item in dictStore.getDictItems('subjectList')"
:key="item.value"
class="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-gray-50"
>
<input
type="checkbox"
:value="item.value"
v-model="formData.subjectList"
class="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span>{{ item.label }}</span>
</label>
</div>
</div>
<!-- 分数输入 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">专业分数</label>
<input
v-model.number="formData.professionalScore"
type="number"
step="0.01"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">文化分数</label>
<input
v-model.number="formData.culturalScore"
type="number"
step="0.01"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">英语分数</label>
<input
v-model.number="formData.englishScore"
type="number"
step="0.01"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">语文分数</label>
<input
v-model.number="formData.chineseScore"
type="number"
step="0.01"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<!-- 提交按钮 -->
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="resetForm"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
重置
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="submitting"
>
{{ submitting ? '提交中...' : '提交' }}
</button>
</div>
</form>
<!-- 表单数据预览 -->
<div v-if="Object.keys(formData).length" class="mt-8 p-4 bg-gray-50 rounded-md">
<h3 class="text-lg font-medium mb-2">表单数据预览</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div><strong>省份:</strong> {{ dictStore.getDictLabel('provinces', formData.province) }}</div>
<div><strong>学历层次:</strong> {{ dictStore.getDictLabel('educationalLevel', formData.educationalLevel) }}</div>
<div><strong>专业类别:</strong> {{ dictStore.getDictLabel('professionalCategory', formData.professionalCategory) }}</div>
<div><strong>科目列表:</strong> {{ getSubjectLabels(formData.subjectList).join(', ') }}</div>
<div><strong>专业分数:</strong> {{ formData.professionalScore }}</div>
<div><strong>文化分数:</strong> {{ formData.culturalScore }}</div>
<div><strong>英语分数:</strong> {{ formData.englishScore }}</div>
<div><strong>语文分数:</strong> {{ formData.chineseScore }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useDictStore } from '~/stores/dict'
import { useScoreStore } from '~/stores/score'
import type { SaveScoreRequest } from '~/service/api/score'
// 使Store
const dictStore = useDictStore()
const scoreStore = useScoreStore()
//
const submitting = ref(false)
//
const formData = reactive<SaveScoreRequest>({
cognitioPolyclinic: '',
subjectList: [],
professionalCategory: '',
professionalCategoryChildren: [],
professionalCategoryChildrenScore: {},
professionalScore: 0,
culturalScore: 0,
englishScore: 0,
chineseScore: 0,
province: '',
})
//
const getSubjectLabels = (subjectValues: string[]): string[] => {
return subjectValues.map(value => dictStore.getDictLabel('subjectList', value))
}
//
const submitForm = async () => {
submitting.value = true
try {
await scoreStore.saveScore(formData)
alert('提交成功!')
} catch (error) {
console.error('提交失败:', error)
alert('提交失败,请检查控制台')
} finally {
submitting.value = false
}
}
//
const resetForm = () => {
formData.cognitioPolyclinic = ''
formData.subjectList = []
formData.professionalCategory = ''
formData.professionalCategoryChildren = []
formData.professionalCategoryChildrenScore = {}
formData.professionalScore = 0
formData.culturalScore = 0
formData.englishScore = 0
formData.chineseScore = 0
formData.province = ''
}
//
onMounted(async () => {
//
try {
await dictStore.loadDynamicDicts(['dynamic_score_types']) //
} catch (error) {
console.error('加载字典数据失败:', error)
}
//
try {
const scoreInfo = await scoreStore.fetchScore()
if (scoreInfo) {
Object.assign(formData, {
cognitioPolyclinic: scoreInfo.cognitioPolyclinic || '',
subjectList: scoreInfo.subjectList || [],
professionalCategory: scoreInfo.professionalCategory || '',
professionalCategoryChildren: scoreInfo.professionalCategoryChildren || [],
professionalCategoryChildrenScore: scoreInfo.professionalCategoryChildrenScore || {},
professionalScore: scoreInfo.professionalScore || 0,
culturalScore: scoreInfo.culturalScore || 0,
englishScore: scoreInfo.englishScore || 0,
chineseScore: scoreInfo.chineseScore || 0,
province: scoreInfo.province || '',
})
}
} catch (error) {
console.error('加载成绩数据失败:', error)
}
})
</script>

View File

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch, computed } from 'vue'
import { useScoreStore } from '~/stores/score' import { useScoreStore } from '~/stores/score'
import { useDictStore } from '~/stores/dict'
import { type SaveScoreRequest } from '~/service/api/score' import { type SaveScoreRequest } from '~/service/api/score'
import message from '~/utils/message' import message from '~/utils/message'
const scoreStore = useScoreStore() const scoreStore = useScoreStore()
const dictStore = useDictStore()
// //
export interface ScoreFormData { export interface ScoreFormData {
@ -54,15 +56,20 @@ const errors = ref({
}, },
}) })
// --- Options Data --- // --- Computed Properties using Dict System ---
const electiveOptions = [ const electiveOptions = computed(() => {
// 使使
return dictStore.getDictItems('subjectList') || [
{ label: '地理', value: '地理' }, { label: '地理', value: '地理' },
{ label: '政治', value: '政治' }, { label: '政治', value: '政治' },
{ label: '化学', value: '化学' }, { label: '化学', value: '化学' },
{ label: '生物', value: '生物' }, { label: '生物', value: '生物' },
] ]
})
const majorCategoryOptions = [ const majorCategoryOptions = computed(() => {
// 使
return dictStore.getDictItems('professionalCategory') || [
{ label: '美术与设计类', value: '美术与设计类' }, { label: '美术与设计类', value: '美术与设计类' },
{ label: '播音与主持类', value: '播音与主持类' }, { label: '播音与主持类', value: '播音与主持类' },
{ label: '表演类', value: '表演类' }, { label: '表演类', value: '表演类' },
@ -72,27 +79,7 @@ const majorCategoryOptions = [
{ label: '戏曲类', value: '戏曲类' }, { label: '戏曲类', value: '戏曲类' },
{ label: '体育类', value: '体育类' }, { label: '体育类', value: '体育类' },
] ]
})
// --- Logic Methods ---
function getSubMajorOptions() {
switch (majorCategory.value) {
case '表演类':
return [
{ label: '服装表演', value: '服装表演' },
{ label: '戏剧影视导演', value: '戏剧影视导演' },
{ label: '戏剧影视表演', value: '戏剧影视表演' },
]
case '音乐类':
return [
{ label: '音乐表演声乐', value: '音乐表演声乐', disabled: selectedSubMajors.value.includes('音乐表演器乐') },
{ label: '音乐表演器乐', value: '音乐表演器乐', disabled: selectedSubMajors.value.includes('音乐表演声乐') },
{ label: '音乐教育', value: '音乐教育' },
]
default:
return []
}
}
function handleElectiveChange(value: string) { function handleElectiveChange(value: string) {
console.warn('handleElectiveChange', value) console.warn('handleElectiveChange', value)
@ -259,7 +246,16 @@ function initForm() {
} }
} }
onMounted(() => { onMounted(async () => {
//
if (Object.keys(dictStore.allDicts).length === 0) {
try {
await dictStore.loadDynamicDicts()
} catch (error) {
console.warn('加载字典数据失败,使用默认值:', error)
}
}
if (!scoreStore.scoreInfo) { if (!scoreStore.scoreInfo) {
// scoreStore.fetchScore().catch(() => { // scoreStore.fetchScore().catch(() => {
// // // //

229
src/pages/dict-demo.vue Normal file
View File

@ -0,0 +1,229 @@
<template>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">字典系统演示页面</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 字典展示区域 -->
<div class="bg-white p-4 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">字典项展示</h2>
<div class="space-y-4">
<div>
<h3 class="font-medium mb-2">专业类别</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="item in professionalCategoryItems"
:key="item.value"
class="px-3 py-1 rounded-full text-sm"
:style="{ backgroundColor: item.color ? item.color + '20' : '#f0f0f0', color: item.color || '#333' }"
>
{{ item.label }} ({{ item.value }})
</span>
</div>
</div>
<div>
<h3 class="font-medium mb-2">学历层次</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="item in educationalLevelItems"
:key="item.value"
class="px-3 py-1 rounded-full text-sm"
:style="{ backgroundColor: item.color ? item.color + '20' : '#f0f0f0', color: item.color || '#333' }"
>
{{ item.label }} ({{ item.value }})
</span>
</div>
</div>
<div>
<h3 class="font-medium mb-2">科目列表</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="item in subjectListItems"
:key="item.value"
class="px-3 py-1 rounded-full text-sm"
:style="{ backgroundColor: item.color ? item.color + '20' : '#f0f0f0', color: item.color || '#333' }"
>
{{ item.label }} ({{ item.value }})
</span>
</div>
</div>
</div>
</div>
<!-- 表单使用示例 -->
<div class="bg-white p-4 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">表单使用示例</h2>
<form @submit.prevent="submitForm" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">省份</label>
<select
v-model="formData.province"
class="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="">请选择省份</option>
<option
v-for="item in provincesItems"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
<div class="text-sm text-gray-500 mt-1">
选中值: {{ formData.province }}, 标签: {{ dictStore.getDictLabel('provinces', formData.province) }}
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">学历层次</label>
<select
v-model="formData.educationalLevel"
class="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="">请选择学历层次</option>
<option
v-for="item in educationalLevelItems"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">专业类别</label>
<select
v-model="formData.professionalCategory"
class="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="">请选择专业类别</option>
<option
v-for="item in professionalCategoryItems"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">科目选择 (多选)</label>
<div class="flex flex-wrap gap-2">
<label
v-for="item in subjectListItems"
:key="item.value"
class="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-gray-50"
>
<input
type="checkbox"
:value="item.value"
v-model="formData.subjectList"
class="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span>{{ item.label }}</span>
</label>
</div>
</div>
<div class="pt-4">
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
提交
</button>
<button
type="button"
@click="loadDynamicDicts"
class="ml-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
加载动态字典
</button>
</div>
</form>
</div>
</div>
<!-- 动态加载状态 -->
<div v-if="loading" class="mt-4 p-4 bg-blue-50 rounded-md">
<p>正在加载动态字典...</p>
</div>
<!-- 表单数据预览 -->
<div v-if="Object.keys(formData).length" class="mt-6 p-4 bg-gray-50 rounded-md">
<h3 class="text-lg font-medium mb-2">表单数据预览</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div><strong>省份:</strong> {{ dictStore.getDictLabel('provinces', formData.province) }}</div>
<div><strong>学历层次:</strong> {{ dictStore.getDictLabel('educationalLevel', formData.educationalLevel) }}</div>
<div><strong>专业类别:</strong> {{ dictStore.getDictLabel('professionalCategory', formData.professionalCategory) }}</div>
<div><strong>科目列表:</strong> {{ getSubjectLabels(formData.subjectList).join(', ') }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useDictStore } from '~/stores/dict'
// 使Store
const dictStore = useDictStore()
//
const formData = ref({
province: '',
educationalLevel: '',
professionalCategory: '',
subjectList: [] as string[],
})
//
const loading = ref(false)
//
const professionalCategoryItems = computed(() => dictStore.getDictItems('professionalCategory'))
const educationalLevelItems = computed(() => dictStore.getDictItems('educationalLevel'))
const subjectListItems = computed(() => dictStore.getDictItems('subjectList'))
const provincesItems = computed(() => dictStore.getDictItems('provinces'))
//
const getSubjectLabels = (subjectValues: string[]): string[] => {
return subjectValues.map(value => dictStore.getDictLabel('subjectList', value))
}
//
const submitForm = () => {
alert(`表单数据已提交:\n${JSON.stringify(formData.value, null, 2)}`)
}
//
const loadDynamicDicts = async () => {
loading.value = true
try {
await dictStore.loadDynamicDicts()
alert('动态字典加载成功!')
} catch (error) {
console.error('加载动态字典失败:', error)
alert('加载动态字典失败,请查看控制台')
} finally {
loading.value = false
}
}
//
onMounted(async () => {
//
if (Object.keys(dictStore.allDicts).length === 0) {
try {
await dictStore.loadDynamicDicts()
} catch (error) {
console.warn('加载动态字典失败,使用静态字典:', error)
}
}
})
</script>

316
src/pages/volunteer.vue Normal file
View File

@ -0,0 +1,316 @@
<template>
<div class="p-6 max-w-4xl mx-auto">
<h1 class="text-2xl font-bold mb-6">高校专业志愿填报</h1>
<div class="space-y-6">
<!-- 基本信息 -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">基本信息</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">省份 *</label>
<select
v-model="formData.province"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择省份</option>
<option
v-for="item in dictStore.getDictItems('provinces')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">学历层次 *</label>
<select
v-model="formData.educationalLevel"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择学历层次</option>
<option
v-for="item in dictStore.getDictItems('educationalLevel')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
</div>
</div>
<!-- 专业选择 -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">专业选择</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">专业类别 *</label>
<select
v-model="formData.professionalCategory"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">请选择专业类别</option>
<option
v-for="item in dictStore.getDictItems('professionalCategory')"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">选考科目 (最多3门) *</label>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
<label
v-for="item in dictStore.getDictItems('subjectList')"
:key="item.value"
class="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-gray-50"
:class="{ 'bg-blue-50 border-blue-300': formData.subjectList.includes(item.value) }"
>
<input
type="checkbox"
:value="item.value"
v-model="formData.subjectList"
:disabled="formData.subjectList.length >= 3 && !formData.subjectList.includes(item.value)"
class="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span>{{ item.label }}</span>
</label>
</div>
<div class="text-sm text-gray-500 mt-1">
已选择 {{ formData.subjectList.length }}/3 门科目
<span v-if="formData.subjectList.length >= 3" class="text-red-500">已达到上限</span>
</div>
</div>
</div>
</div>
<!-- 成绩信息 -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">成绩信息</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">专业成绩</label>
<input
v-model.number="formData.professionalScore"
type="number"
step="0.01"
min="0"
max="300"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入专业成绩 (0-300)"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">文化成绩</label>
<input
v-model.number="formData.culturalScore"
type="number"
step="0.01"
min="0"
max="750"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入文化成绩 (0-750)"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">语文成绩</label>
<input
v-model.number="formData.chineseScore"
type="number"
step="0.01"
min="0"
max="150"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入语文成绩 (0-150)"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">英语成绩</label>
<input
v-model.number="formData.englishScore"
type="number"
step="0.01"
min="0"
max="150"
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入英语成绩 (0-150)"
/>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="resetForm"
class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
重置
</button>
<button
type="submit"
@click="submitForm"
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
:disabled="!isFormValid"
>
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</div>
</div>
<!-- 表单验证错误提示 -->
<div v-if="errors.length > 0" class="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<h3 class="text-red-800 font-medium mb-2">请修正以下错误</h3>
<ul class="list-disc list-inside text-red-700 space-y-1">
<li v-for="(error, index) in errors" :key="index">{{ error }}</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useDictStore } from '~/stores/dict'
import { useScoreStore } from '~/stores/score'
import { SaveScoreRequest } from '~/service/api/score'
import message from '~/utils/message'
// 使StoreStore
const dictStore = useDictStore()
const scoreStore = useScoreStore()
//
const formData = reactive<SaveScoreRequest>({
cognitioPolyclinic: '',
subjectList: [],
professionalCategory: '',
professionalCategoryChildren: [],
professionalCategoryChildrenScore: {},
professionalScore: 0,
culturalScore: 0,
englishScore: 0,
chineseScore: 0,
province: '',
})
//
const isSubmitting = ref(false)
//
const errors = ref<string[]>([])
//
const isFormValid = computed(() => {
return formData.province &&
formData.professionalCategory &&
formData.subjectList.length > 0
})
//
const submitForm = async () => {
errors.value = []
//
if (!formData.province) {
errors.value.push('请选择省份')
}
if (!formData.professionalCategory) {
errors.value.push('请选择专业类别')
}
if (formData.subjectList.length === 0) {
errors.value.push('请至少选择一门选考科目')
}
if (errors.value.length > 0) {
return
}
isSubmitting.value = true
try {
// -
formData.cognitioPolyclinic = formData.province.includes('beijing') || formData.province.includes('shanghai')
? '综合改革'
: formData.subjectList.includes('physics') || formData.subjectList.includes('chemistry')
? '理科'
: '文科'
//
await scoreStore.saveScore(formData)
message.success('志愿信息保存成功!', 2000)
//
} catch (error) {
console.error('提交失败:', error)
message.error('提交失败,请检查网络连接或稍后重试', 3000)
} finally {
isSubmitting.value = false
}
}
//
const resetForm = () => {
formData.cognitioPolyclinic = ''
formData.subjectList = []
formData.professionalCategory = ''
formData.professionalCategoryChildren = []
formData.professionalCategoryChildrenScore = {}
formData.professionalScore = 0
formData.culturalScore = 0
formData.englishScore = 0
formData.chineseScore = 0
formData.province = ''
errors.value = []
}
//
onMounted(async () => {
//
if (Object.keys(dictStore.allDicts).length === 0) {
try {
await dictStore.loadDynamicDicts()
} catch (error) {
console.warn('加载字典数据失败,使用默认值:', error)
}
}
//
try {
const scoreInfo = await scoreStore.fetchScore()
if (scoreInfo) {
// 使
Object.assign(formData, {
cognitioPolyclinic: scoreInfo.cognitioPolyclinic || '',
subjectList: scoreInfo.subjectList || [],
professionalCategory: scoreInfo.professionalCategory || '',
professionalCategoryChildren: scoreInfo.professionalCategoryChildren || [],
professionalCategoryChildrenScore: scoreInfo.professionalCategoryChildrenScore || {},
professionalScore: scoreInfo.professionalScore || 0,
culturalScore: scoreInfo.culturalScore || 0,
englishScore: scoreInfo.englishScore || 0,
chineseScore: scoreInfo.chineseScore || 0,
province: scoreInfo.province || '',
})
}
} catch (error) {
console.warn('加载成绩数据失败:', error)
//
}
})
</script>

52
src/service/api/dict.ts Normal file
View File

@ -0,0 +1,52 @@
/**
* API服务
*
*/
import request from '~/service/request'
// 字典项接口
export interface DictItem {
label: string
value: string | number
disabled?: boolean
color?: string
order?: number
[key: string]: any
}
// 字典响应数据接口
export interface DictData {
type: string
items: DictItem[]
}
/**
*
* @param types
* @returns
*/
export function getDictionaryList(types?: string[]): Promise<DictData[]> {
const params: any = {}
if (types && types.length > 0) {
params.types = types.join(',')
}
return request.get<DictData[]>('/dict/list', {
params,
showLoading: false
})
}
/**
*
* @param type
* @returns
*/
export function getDictionaryByType(type: string): Promise<DictItem[]> {
return request.get<DictItem[]>(`/dict/type/${type}`, {
showLoading: false
})
}
// 如果需要其他字典相关API可以在这里添加

118
src/stores/dict.ts Normal file
View File

@ -0,0 +1,118 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { DictItem, DictType, getDictItems, staticDicts } from '~/utils/dict'
import { getDictionaryList } from '~/service/api/dict'
export const useDictStore = defineStore('dict', () => {
// 存储动态字典数据
const dynamicDicts = ref<DictType>({})
// 合并静态和动态字典
const allDicts = computed(() => ({
...staticDicts,
...dynamicDicts.value
}))
/**
*
* @param dictType
* @returns
*/
function getDictItems(dictType: string): DictItem[] {
return allDicts.value[dictType] || []
}
/**
*
* @param dictType
* @param value
* @returns
*/
function getDictLabel(dictType: string, value: string | number): string {
const dictItems = getDictItems(dictType)
const item = dictItems.find(item => item.value === value)
return item ? item.label : ''
}
/**
*
* @param dictType
* @param value
* @returns
*/
function getDictColor(dictType: string, value: string | number): string {
const dictItems = getDictItems(dictType)
const item = dictItems.find(item => item.value === value)
return item ? item.color || '' : ''
}
/**
*
* @param dictType
* @param value
* @returns
*/
function getDictItem(dictType: string, value: string | number): DictItem | undefined {
const dictItems = getDictItems(dictType)
return dictItems.find(item => item.value === value)
}
/**
*
* @param dictTypes
*/
async function loadDynamicDicts(dictTypes?: string[]): Promise<void> {
try {
// 调用API获取动态字典数据
const response = await getDictionaryList(dictTypes)
// 更新动态字典数据
if (response && Array.isArray(response)) {
response.forEach((dictData: any) => {
if (dictData.type && Array.isArray(dictData.items)) {
dynamicDicts.value[dictData.type] = dictData.items
}
})
}
} catch (error) {
console.error('加载动态字典失败:', error)
throw error
}
}
/**
*
* @param dictType
* @param items
*/
function setDynamicDict(dictType: string, items: DictItem[]): void {
dynamicDicts.value[dictType] = items
}
/**
*
*/
function clearDynamicDicts(): void {
dynamicDicts.value = {}
}
return {
// 状态
dynamicDicts,
allDicts,
// 计算属性
getDictItems,
getDictLabel,
getDictColor,
getDictItem,
// 动作
loadDynamicDicts,
setDynamicDict,
clearDynamicDicts,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useDictStore as any, import.meta.hot))

View File

@ -24,6 +24,7 @@ declare module 'vue-router/auto-routes' {
'/agreement': RouteRecordInfo<'/agreement', '/agreement', Record<never, never>, Record<never, never>>, '/agreement': RouteRecordInfo<'/agreement', '/agreement', Record<never, never>, Record<never, never>>,
'/contact-us': RouteRecordInfo<'/contact-us', '/contact-us', Record<never, never>, Record<never, never>>, '/contact-us': RouteRecordInfo<'/contact-us', '/contact-us', Record<never, never>, Record<never, never>>,
'/demo/pop-confirm': RouteRecordInfo<'/demo/pop-confirm', '/demo/pop-confirm', Record<never, never>, Record<never, never>>, '/demo/pop-confirm': RouteRecordInfo<'/demo/pop-confirm', '/demo/pop-confirm', Record<never, never>, Record<never, never>>,
'/dict-demo': RouteRecordInfo<'/dict-demo', '/dict-demo', Record<never, never>, Record<never, never>>,
'/hi/[name]': RouteRecordInfo<'/hi/[name]', '/hi/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>, '/hi/[name]': RouteRecordInfo<'/hi/[name]', '/hi/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/majors': RouteRecordInfo<'/majors', '/majors', Record<never, never>, Record<never, never>>, '/majors': RouteRecordInfo<'/majors', '/majors', Record<never, never>, Record<never, never>>,
'/privacy-policy': RouteRecordInfo<'/privacy-policy', '/privacy-policy', Record<never, never>, Record<never, never>>, '/privacy-policy': RouteRecordInfo<'/privacy-policy', '/privacy-policy', Record<never, never>, Record<never, never>>,
@ -31,5 +32,6 @@ declare module 'vue-router/auto-routes' {
'/school/[schoolCode]': RouteRecordInfo<'/school/[schoolCode]', '/school/:schoolCode', { schoolCode: ParamValue<true> }, { schoolCode: ParamValue<false> }>, '/school/[schoolCode]': RouteRecordInfo<'/school/[schoolCode]', '/school/:schoolCode', { schoolCode: ParamValue<true> }, { schoolCode: ParamValue<false> }>,
'/simulate': RouteRecordInfo<'/simulate', '/simulate', Record<never, never>, Record<never, never>>, '/simulate': RouteRecordInfo<'/simulate', '/simulate', Record<never, never>, Record<never, never>>,
'/universities': RouteRecordInfo<'/universities', '/universities', Record<never, never>, Record<never, never>>, '/universities': RouteRecordInfo<'/universities', '/universities', Record<never, never>, Record<never, never>>,
'/volunteer': RouteRecordInfo<'/volunteer', '/volunteer', Record<never, never>, Record<never, never>>,
} }
} }

171
src/utils/dict.ts Normal file
View File

@ -0,0 +1,171 @@
/**
* -
* API获取
*/
// 字典项接口
export interface DictItem {
label: string
value: string | number
disabled?: boolean
color?: string
order?: number
[key: string]: any
}
// 字典类型
export interface DictType {
[key: string]: DictItem[]
}
// 静态字典数据
const staticDicts: DictType = {
// 专业类别
professionalCategory: [
{ label: '理工类', value: 'science', color: '#108ee9' },
{ label: '文史类', value: 'liberal_arts', color: '#2db7f5' },
{ label: '艺术类', value: 'art', color: '#87d068' },
{ label: '体育类', value: 'sports', color: '#ff5500' },
],
// 学历层次
educationalLevel: [
{ label: '本科', value: 'undergraduate', color: '#108ee9' },
{ label: '专科', value: 'college', color: '#2db7f5' },
{ label: '研究生', value: 'graduate', color: '#87d068' },
],
// 省份
provinces: [
{ label: '北京市', value: 'beijing', order: 1 },
{ label: '上海市', value: 'shanghai', order: 2 },
{ label: '广东省', value: 'guangdong', order: 3 },
{ label: '江苏省', value: 'jiangsu', order: 4 },
{ label: '浙江省', value: 'zhejiang', order: 5 },
{ label: '山东省', value: 'shandong', order: 6 },
{ label: '河南省', value: 'henan', order: 7 },
{ label: '河北省', value: 'hebei', order: 8 },
{ label: '山西省', value: 'shanxi', order: 9 },
{ label: '辽宁省', value: 'liaoning', order: 10 },
{ label: '吉林省', value: 'jilin', order: 11 },
{ label: '黑龙江省', value: 'heilongjiang', order: 12 },
{ label: '安徽省', value: 'anhui', order: 13 },
{ label: '福建省', value: 'fujian', order: 14 },
{ label: '江西省', value: 'jiangxi', order: 15 },
{ label: '湖北省', value: 'hubei', order: 16 },
{ label: '湖南省', value: 'hunan', order: 17 },
{ label: '四川省', value: 'sichuan', order: 18 },
{ label: '贵州省', value: 'guizhou', order: 19 },
{ label: '云南省', value: 'yunnan', order: 20 },
{ label: '陕西省', value: 'shaanxi', order: 21 },
{ label: '甘肃省', value: 'gansu', order: 22 },
{ label: '青海省', value: 'qinghai', order: 23 },
{ label: '海南省', value: 'hainan', order: 24 },
{ label: '台湾省', value: 'taiwan', order: 25 },
{ label: '内蒙古自治区', value: 'neimenggu', order: 26 },
{ label: '广西壮族自治区', value: 'guangxi', order: 27 },
{ label: '西藏自治区', value: 'xizang', order: 28 },
{ label: '宁夏回族自治区', value: 'ningxia', order: 29 },
{ label: '新疆维吾尔自治区', value: 'xinjiang', order: 30 },
{ label: '香港特别行政区', value: 'hongkong', order: 31 },
{ label: '澳门特别行政区', value: 'aomen', order: 32 },
],
// 科目列表
subjectList: [
{ label: '语文', value: 'chinese', color: '#108ee9' },
{ label: '数学', value: 'mathematics', color: '#2db7f5' },
{ label: '英语', value: 'english', color: '#87d068' },
{ label: '物理', value: 'physics', color: '#ff5500' },
{ label: '化学', value: 'chemistry', color: '#f5222d' },
{ label: '生物', value: 'biology', color: '#fa8c16' },
{ label: '政治', value: 'politics', color: '#faad14' },
{ label: '历史', value: 'history', color: '#a0d911' },
{ label: '地理', value: 'geography', color: '#52c41a' },
],
// 性别
gender: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
],
// 状态
status: [
{ label: '启用', value: 'enabled', color: '#52c41a' },
{ label: '禁用', value: 'disabled', color: '#f5222d' },
],
// 类型
type: [
{ label: '系统', value: 'system', color: '#108ee9' },
{ label: '用户', value: 'user', color: '#2db7f5' },
]
}
/**
*
* @param dictType
* @returns
*/
export function getDictItems(dictType: string): DictItem[] {
return staticDicts[dictType] || []
}
/**
*
* @param dictType
* @param value
* @returns
*/
export function getDictLabel(dictType: string, value: string | number): string {
const dictItems = getDictItems(dictType)
const item = dictItems.find(item => item.value === value)
return item ? item.label : ''
}
/**
*
* @param dictType
* @param label
* @returns
*/
export function getDictValue(dictType: string, label: string): string | number | undefined {
const dictItems = getDictItems(dictType)
const item = dictItems.find(item => item.label === label)
return item ? item.value : undefined
}
/**
*
* @param dictType
* @param value
* @returns
*/
export function getDictColor(dictType: string, value: string | number): string {
const dictItems = getDictItems(dictType)
const item = dictItems.find(item => item.value === value)
return item ? item.color || '' : ''
}
/**
*
* @returns
*/
export function getAllDictTypes(): string[] {
return Object.keys(staticDicts)
}
/**
*
* @param dictType
* @param value
* @returns
*/
export function getDictItem(dictType: string, value: string | number): DictItem | undefined {
const dictItems = getDictItems(dictType)
return dictItems.find(item => item.value === value)
}
// 导出静态字典
export { staticDicts }