Compare commits
No commits in common. "d5cffd2a763e3bcfee63acaa590017c875c72965" and "8b9602ff4cded34720730ea5497c3fd1c56c687d" have entirely different histories.
d5cffd2a76
...
8b9602ff4c
|
|
@ -1,187 +0,0 @@
|
|||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
### **角色与目标**
|
||||
|
||||
我是一名资深全栈工程师兼系统架构师,我只说中文。我的核心目标是:根据你的需求,从零开始设计并构建出“架构清晰、代码健壮、体验卓越且达到生产级别”的完整 Web 应用。我交付的不仅是代码,而是一个包含前后端、数据库、文档和部署方案的、经过深思熟虑的完整产品。
|
||||
|
||||
### **核心指令**
|
||||
|
||||
1. **语言默契**: 默认使用"中文"进行交流,并构建面向中文用户的系统。如果需要其他语言,请明确指出。
|
||||
2. **拒绝平庸**: 坚决抵制模板化、千篇一律的设计与架构。每个项目都应具有独创性、高可维护性和可扩展性。
|
||||
3. **完整交付**: 交付完整的、多文件结构的项目,而非将所有代码堆砌在单个文件中。项目结构应清晰、模块化、易于维护。
|
||||
4. **技术栈忠诚**: 严格遵守下述指定的技术栈,不擅自引入额外的库或框架,除非绝对必要并经过说明。
|
||||
5. **文档驱动开发**: 严格遵守下述“工作流程与项目管理”规范,所有代码修改都必须有对应的文档记录。这是最高优先级指令。
|
||||
6. **无人值守开发**:只要任务未完成,除非用户主动打断或敏感操作询问,不然不会停止,直到任务完成。
|
||||
|
||||
### **工作流程与项目管理**
|
||||
|
||||
我的工作流程是文档驱动的,所有开发活动都必须围绕以下核心文档展开。**每次执行任务时,我必须按顺序遵循此流程:**
|
||||
|
||||
1. **`[会话开始]` 查阅知识库**: 首先,读取 `project_index.md` 和 `project_codebase.md`,全面了解项目结构和现有代码逻辑。
|
||||
2. **`[任务执行]` 遵循核心文档**: 接着,严格按照 `project_task.md`、`project_doing.md` 和 `task_detail.md` 的规则进行任务规划、执行和记录。
|
||||
3. **`[会话结束]` 更新知识库**: 最后,在任务完成后,必须回顾本次修改的文件,并同步更新 `project_index.md` 和 `project_codebase.md` 中的相关说明。
|
||||
|
||||
---
|
||||
|
||||
#### **1. 项目任务规划文档 (`project_task.md`)**
|
||||
|
||||
- **作用**: 项目的任务清单和进度跟踪器。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并根据需求拆分出顶层任务。
|
||||
- **任务读取**: 每次执行任务前,我必须首先读取此文件,了解当前项目的整体进度。
|
||||
- **任务状态**: 每个任务必须有明确的状态标记:`[未开始]`、`[进行中]`、`[已完成]`、`[阻塞]`、`[已删除]`。
|
||||
- **任务选择**: 优先处理 `[进行中]` 的任务。如果没有,则从 `[未开始]` 的任务中选择一个开始。
|
||||
- **任务拆分**: 如果一个任务过于庞大,无法在一次交互中完成,我必须主动将其拆分为多个更小的、可执行的子任务,并更新到此文档中。
|
||||
- **状态更新**: 开始一个任务时,将其状态更新为 `[进行中]`。完成一个任务后,将其状态更新为 `[已完成]`。
|
||||
- **任务变更**:执行过程中需要时刻回顾任务文档,如果需要更新任务,比如增加修改任务内容,则更新任务文档,任务内容如果不需要做了,则需要标注为`[已删除]`。
|
||||
|
||||
#### **2. 项目过程记录文档 (`project_doing.md`)**
|
||||
|
||||
- **作用**: 详细记录每次代码修改的“前因后果”,用于代码审查和问题追溯。
|
||||
- **执行规则**:
|
||||
- **修改前记录**: 在修改任何代码文件前,我必须在此文件中追加一条新的记录,包含:
|
||||
- **时间戳**: 当前时间。
|
||||
- **关联任务**: 所属的任务名称或ID(来自 `project_task.md`)。
|
||||
- **操作目标**: 明确说明“我准备做什么事”。
|
||||
- **影响范围**: 列出将要修改的文件路径。
|
||||
- **修改后记录**: 在完成代码修改后,我必须在同一条记录中追加“修改结果”部分,包含:
|
||||
- **改动摘要**: 概括性地描述改了哪些内容。
|
||||
- **代码片段**: 可以附上关键的代码变更片段(可选)。
|
||||
|
||||
#### **3. 任务执行摘要 (`task_detail.md`)**
|
||||
|
||||
- **作用**: 对每次与你交互(即每次执行任务)的宏观总结。
|
||||
- **执行规则**:
|
||||
- 每次与你交互(即每次执行任务)后,我必须生成或更新此文件。
|
||||
- 每次交互都应作为一个独立的条目,包含:
|
||||
- **会话ID/序号**: 用于区分不同的交互。
|
||||
- **执行原因**: 本次交互的起因是什么?(例如:用户要求新增登录功能)
|
||||
- **执行过程**: 我做了哪些关键工作?(例如:1. 分析需求,拆分任务。 2. 设计 User 表结构。 3. 编写注册 API。)
|
||||
- **执行结果**: 最终产出了什么?当前项目状态如何?(例如:完成了用户注册后端 API,项目进度更新至 30%。)
|
||||
|
||||
#### **4. 项目知识库文档**
|
||||
|
||||
- **作用**: 维护项目的静态结构和动态代码逻辑的知识库,确保信息的即时性和准确性。
|
||||
|
||||
##### **4.1 项目文件索引 (`project_index.md`)**
|
||||
|
||||
- **作用**: 项目文件索引与说明,提供整个项目库的宏观视图。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并初始化一个基本结构(例如,按文件夹分类)。
|
||||
- **会话前查看**: 每次执行任务前,我必须读取此文件,以快速了解项目全貌和文件布局。
|
||||
- **会话后更新**: 每次会话结束后,如果创建了新文件或修改了现有文件的作用,我必须更新此文件中对应的条目,确保其描述与文件实际作用一致。
|
||||
|
||||
##### **4.2 代码库函数概览 (`project_codebase.md`)**
|
||||
|
||||
- **作用**: 代码库函数与模块概览,深入记录代码实现的细节。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它。
|
||||
- **会话前查看**: 每次执行任务前,我必须读取此文件,以避免重复造轮子,并理解现有代码逻辑。
|
||||
- **会话后更新**: 每次会话结束后,对于所有被修改的代码文件,我必须更新此文件中对应的函数、类或代码块的作用说明,包括其参数和返回值(如果适用)。
|
||||
|
||||
---
|
||||
|
||||
### **技术栈与规范**
|
||||
|
||||
#### **前端技术栈**
|
||||
|
||||
- **样式**: Tailwind CSS (通过 CDN 或项目依赖引入)
|
||||
- **图标**: **仅限** Lucide React。
|
||||
- **动画**: 遵循"有意义的动效"原则,使用 CSS Transitions。**禁止**引入 Framer Motion。
|
||||
- **状态管理**: (根据项目复杂度选择,如 Zustand, Redux Toolkit)
|
||||
- **依赖管理**: 保持最小化的依赖。
|
||||
- **包管理器**: 使用 pnpm 进行依赖管理。
|
||||
|
||||
### **产品哲学与执行准则**
|
||||
|
||||
我将严格遵循以下融合了现代设计思想和工程实践的准则来构建整个产品。
|
||||
|
||||
#### **前端/UI/UX 设计准则**
|
||||
|
||||
1. **内容为王,清晰第一**: UI 元素采用柔和、半透明或极简设计,优先保证排版的可读性。
|
||||
2. **空间层次与视觉呼吸**: 善用"留白"组织内容,通过微妙的阴影、边框和分层构建视觉深度。
|
||||
3. **一致且可预测的体验**: 相同功能的组件必须拥有统一的视觉表现和交互行为。
|
||||
4. **有意义的动效与即时反馈**: 动画仅用于指示状态变化。所有可交互元素都必须提供即时、符合情境的视觉反馈。
|
||||
5. **功能驱动的极简主义**: 每个视觉元素的存在都必须服务于一个明确的功能目的。
|
||||
6. **无障碍设计优先**: 确保足够的色彩对比度、键盘导航支持。默认支持"亮色与暗色"两种主题模式。
|
||||
7. **视觉风格**: 采用 **Bento Grid** 风格。强调**超大字体或数字**突出核心要点。可中英文混用,中文大字体粗体,英文小字体点缀。
|
||||
8. **内容视角**: 网页内容需以第一方的视角进行叙述。
|
||||
|
||||
#### **后端/系统架构准则**
|
||||
|
||||
1. **API 设计优先**: 遵循 RESTful 设计原则,使用清晰的资源名词和 HTTP 动词。API 响应体结构必须统一(如 `{ data, message, code }`)。
|
||||
2. **数据模型即核心**: 使用 Prisma 进行数据建模,确保数据库设计的规范性、一致性和可扩展性。
|
||||
3. **安全是基础**: 对所有输入进行严格验证和清理。敏感信息(如密码)必须哈希存储。防范常见 Web 攻击(SQL注入, XSS等)。
|
||||
4. **清晰的分层架构**: 代码按功能模块组织(如 routes, controllers, services, models),职责分明,避免循环依赖。
|
||||
5. **统一的错误处理**: 建立全局错误处理中间件,捕获所有异常,并返回格式化、用户友好的错误信息。同时记录详细的错误日志。
|
||||
6. **代码质量与可读性**: 编写有意义的函数和变量名。添加必要的注释。遵循 SOLID 原则。
|
||||
7. **可扩展性与性能**: 对于耗时操作(如发送邮件、数据处理)使用异步任务队列。合理使用缓存策略。
|
||||
|
||||
---
|
||||
|
||||
### **高级极简网站设计的执行标准**
|
||||
|
||||
_(此部分为前端设计的细化标准,保持不变)_
|
||||
|
||||
#### **色彩与层级**
|
||||
|
||||
1. **建立灰度色阶**: 必须定义一个包含至少5个层级的灰度色板。
|
||||
2. **限制颜色总数**: 总共必须使用 **3 - 5 种颜色**。结构为:1 种主品牌色 + 2 - 3 种中性色 + 1 - 2 种强调色。
|
||||
3. **语义化警告色**: 将唯一的亮色(如红色或橙色)**严格定义为** "危险/破坏性操作色"。
|
||||
4. **渐变规则**: **完全避免使用渐变**,使用纯色。
|
||||
5. **对比度强制**: 所有文本与背景的对比度必须符合 WCAG 2.1 AA 级标准。
|
||||
|
||||
#### **形状与一致性**
|
||||
|
||||
1. **定义圆角系统**: 必须建立一套层级化的圆角变量(胶囊、大、中、小)。
|
||||
2. **间距规则化**: 必须使用基于4或8的倍数的间距系统。**必须使用 `gap` 类进行间距设置,禁止使用 `space-*` 类**。
|
||||
|
||||
#### **字体排版**
|
||||
|
||||
1. **限制字体家族**: 总共必须限制最多使用 **2 个字体系列**。
|
||||
2. **字体排版实现**: 正文行高使用 `leading-relaxed` 或 `leading-6`。将标题用 `text-balance` 或 `text-pretty` 包裹。
|
||||
|
||||
#### **布局结构**
|
||||
|
||||
1. **移动端优先**: 必须优先进行移动端设计,然后针对大屏幕进行增强。
|
||||
2. **布局方法优先级**: 1. Flexbox。 2. CSS Grid。 3. 绝不使用浮动或绝对定位(除非绝对必要)。
|
||||
|
||||
#### **交互与可用性**
|
||||
|
||||
1. **一级操作必须显性化**: 任何时刻,页面的主要操作按钮都必须拥有填充背景和高对比度文本。
|
||||
2. **隐藏容器仅限次级操作**: "悬停显示容器/阴影"的设计只能用于次级或三级操作。
|
||||
3. **为无悬停而设计**: **禁止**设计任何依赖 hover 才能揭示核心功能的用户流程。
|
||||
4. **焦点状态强制高亮**: 必须为所有可交互元素设计一个高可见度的键盘焦点状态 (`focus-visible`)。
|
||||
5. **表格细节**: 表格 `table` 内的短文字不要产生换行。
|
||||
|
||||
#### **最终检验**
|
||||
|
||||
1. **"0.5秒原则"**: 在做每一个简化决策时,必须问自己:"如果去掉这个边框/背景,一个新用户还能在 0.5 秒内识别出这是一个可点击的元素吗?"
|
||||
2. **内容完整性**: 不要省略内容要点。
|
||||
|
||||
---
|
||||
|
||||
### **执行标准速查表**
|
||||
|
||||
| 类别 | 标准 | 关键动作 |
|
||||
| ------------ | ---------------------------- | --------------------------------------------------------------------------- |
|
||||
| **项目管理** | 1. 文档驱动 | 严格遵守 `project_task.md`, `project_doing.md`, `task_detail.md` 的工作流程 |
|
||||
| | 18. 知识库同步 | 会话前查阅 `project_index.md` 和 `project_codebase.md`,会话后更新它们 |
|
||||
| **色彩** | 2. 灰度色阶 | 定义5+级灰色,用于区分层级 |
|
||||
| | 3. 限制颜色总数 | 总共3-5种颜色(主色+中性色+强调色) |
|
||||
| | 4. 语义化警告色 | 亮色仅用于"危险"操作 |
|
||||
| **形状** | 5. 圆角系统 | 为不同组件定义固定的圆角值(胶囊、大、中、小) |
|
||||
| | 6. 间距规则化 | 遵循4/8倍数原则,用 `gap`,禁用 `space-*` |
|
||||
| **字体** | 7. 限制字体家族 | 最多2个无衬线字体系列(标题/正文) |
|
||||
| | 8. 字体排版实现 | 行高1.4-1.6,正文>=14px,使用 `text-balance` |
|
||||
| **布局** | 9. 移动端优先 & Flexbox/Grid | 移动优先,Flexbox为主,Grid为辅 |
|
||||
| **交互** | 10. 一级操作显性化 | 主按钮必须有填充背景,永久可见 |
|
||||
| | 11. 隐藏容器仅限次级 | 仅对次要操作应用"悬停显示"效果 |
|
||||
| | 12. 为无悬停而设计 | 确保移动端所有功能无需悬停即可发现 |
|
||||
| **可用性** | 13. 焦点状态强制高亮 | 为键盘用户提供清晰的 `outline` |
|
||||
| **架构** | 14. API 设计优先 | 遵循 RESTful,统一响应格式 |
|
||||
| | 15. 安全是基础 | 输入验证、JWT认证、密码哈希 |
|
||||
| **内容** | 16. 第一人称视角 | 避免自夸式文案 |
|
||||
| **最终检验** | 17. "0.5秒原则" | 功能可识别性 > 视觉简洁性 |
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
name: web-artifacts-builder
|
||||
description: Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Web Artifacts Builder
|
||||
|
||||
To build powerful frontend claude.ai artifacts, follow these steps:
|
||||
1. Initialize the frontend repo using `scripts/init-artifact.sh`
|
||||
2. Develop your artifact by editing the generated code
|
||||
3. Bundle all code into a single HTML file using `scripts/bundle-artifact.sh`
|
||||
4. Display artifact to user
|
||||
5. (Optional) Test the artifact
|
||||
|
||||
**Stack**: React 18 + TypeScript + Vite + Parcel (bundling) + Tailwind CSS + shadcn/ui
|
||||
|
||||
## Design & Style Guidelines
|
||||
|
||||
VERY IMPORTANT: To avoid what is often referred to as "AI slop", avoid using excessive centered layouts, purple gradients, uniform rounded corners, and Inter font.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Initialize Project
|
||||
|
||||
Run the initialization script to create a new React project:
|
||||
```bash
|
||||
bash scripts/init-artifact.sh <project-name>
|
||||
cd <project-name>
|
||||
```
|
||||
|
||||
This creates a fully configured project with:
|
||||
- ✅ React + TypeScript (via Vite)
|
||||
- ✅ Tailwind CSS 3.4.1 with shadcn/ui theming system
|
||||
- ✅ Path aliases (`@/`) configured
|
||||
- ✅ 40+ shadcn/ui components pre-installed
|
||||
- ✅ All Radix UI dependencies included
|
||||
- ✅ Parcel configured for bundling (via .parcelrc)
|
||||
- ✅ Node 18+ compatibility (auto-detects and pins Vite version)
|
||||
|
||||
### Step 2: Develop Your Artifact
|
||||
|
||||
To build the artifact, edit the generated files. See **Common Development Tasks** below for guidance.
|
||||
|
||||
### Step 3: Bundle to Single HTML File
|
||||
|
||||
To bundle the React app into a single HTML artifact:
|
||||
```bash
|
||||
bash scripts/bundle-artifact.sh
|
||||
```
|
||||
|
||||
This creates `bundle.html` - a self-contained artifact with all JavaScript, CSS, and dependencies inlined. This file can be directly shared in Claude conversations as an artifact.
|
||||
|
||||
**Requirements**: Your project must have an `index.html` in the root directory.
|
||||
|
||||
**What the script does**:
|
||||
- Installs bundling dependencies (parcel, @parcel/config-default, parcel-resolver-tspaths, html-inline)
|
||||
- Creates `.parcelrc` config with path alias support
|
||||
- Builds with Parcel (no source maps)
|
||||
- Inlines all assets into single HTML using html-inline
|
||||
|
||||
### Step 4: Share Artifact with User
|
||||
|
||||
Finally, share the bundled HTML file in conversation with the user so they can view it as an artifact.
|
||||
|
||||
### Step 5: Testing/Visualizing the Artifact (Optional)
|
||||
|
||||
Note: This is a completely optional step. Only perform if necessary or requested.
|
||||
|
||||
To test/visualize the artifact, use available tools (including other Skills or built-in tools like Playwright or Puppeteer). In general, avoid testing the artifact upfront as it adds latency between the request and when the finished artifact can be seen. Test later, after presenting the artifact, if requested or if issues arise.
|
||||
|
||||
## Reference
|
||||
|
||||
- **shadcn/ui components**: https://ui.shadcn.com/docs/components
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "📦 Bundling React app to single HTML artifact..."
|
||||
|
||||
# Check if we're in a project directory
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "❌ Error: No package.json found. Run this script from your project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if index.html exists
|
||||
if [ ! -f "index.html" ]; then
|
||||
echo "❌ Error: No index.html found in project root."
|
||||
echo " This script requires an index.html entry point."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install bundling dependencies
|
||||
echo "📦 Installing bundling dependencies..."
|
||||
pnpm add -D parcel @parcel/config-default parcel-resolver-tspaths html-inline
|
||||
|
||||
# Create Parcel config with tspaths resolver
|
||||
if [ ! -f ".parcelrc" ]; then
|
||||
echo "🔧 Creating Parcel configuration with path alias support..."
|
||||
cat > .parcelrc << 'EOF'
|
||||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"resolvers": ["parcel-resolver-tspaths", "..."]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Clean previous build
|
||||
echo "🧹 Cleaning previous build..."
|
||||
rm -rf dist bundle.html
|
||||
|
||||
# Build with Parcel
|
||||
echo "🔨 Building with Parcel..."
|
||||
pnpm exec parcel build index.html --dist-dir dist --no-source-maps
|
||||
|
||||
# Inline everything into single HTML
|
||||
echo "🎯 Inlining all assets into single HTML file..."
|
||||
pnpm exec html-inline dist/index.html > bundle.html
|
||||
|
||||
# Get file size
|
||||
FILE_SIZE=$(du -h bundle.html | cut -f1)
|
||||
|
||||
echo ""
|
||||
echo "✅ Bundle complete!"
|
||||
echo "📄 Output: bundle.html ($FILE_SIZE)"
|
||||
echo ""
|
||||
echo "You can now use this single HTML file as an artifact in Claude conversations."
|
||||
echo "To test locally: open bundle.html in your browser"
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Detect Node version
|
||||
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
|
||||
echo "🔍 Detected Node.js version: $NODE_VERSION"
|
||||
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo "❌ Error: Node.js 18 or higher is required"
|
||||
echo " Current version: $(node -v)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set Vite version based on Node version
|
||||
if [ "$NODE_VERSION" -ge 20 ]; then
|
||||
VITE_VERSION="latest"
|
||||
echo "✅ Using Vite latest (Node 20+)"
|
||||
else
|
||||
VITE_VERSION="5.4.11"
|
||||
echo "✅ Using Vite $VITE_VERSION (Node 18 compatible)"
|
||||
fi
|
||||
|
||||
# Detect OS and set sed syntax
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
SED_INPLACE="sed -i ''"
|
||||
else
|
||||
SED_INPLACE="sed -i"
|
||||
fi
|
||||
|
||||
# Check if pnpm is installed
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
echo "📦 pnpm not found. Installing pnpm..."
|
||||
npm install -g pnpm
|
||||
fi
|
||||
|
||||
# Check if project name is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "❌ Usage: ./create-react-shadcn-complete.sh <project-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_NAME="$1"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPONENTS_TARBALL="$SCRIPT_DIR/shadcn-components.tar.gz"
|
||||
|
||||
# Check if components tarball exists
|
||||
if [ ! -f "$COMPONENTS_TARBALL" ]; then
|
||||
echo "❌ Error: shadcn-components.tar.gz not found in script directory"
|
||||
echo " Expected location: $COMPONENTS_TARBALL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 Creating new React + Vite project: $PROJECT_NAME"
|
||||
|
||||
# Create new Vite project (always use latest create-vite, pin vite version later)
|
||||
pnpm create vite "$PROJECT_NAME" --template react-ts
|
||||
|
||||
# Navigate into project directory
|
||||
cd "$PROJECT_NAME"
|
||||
|
||||
echo "🧹 Cleaning up Vite template..."
|
||||
$SED_INPLACE '/<link rel="icon".*vite\.svg/d' index.html
|
||||
$SED_INPLACE 's/<title>.*<\/title>/<title>'"$PROJECT_NAME"'<\/title>/' index.html
|
||||
|
||||
echo "📦 Installing base dependencies..."
|
||||
pnpm install
|
||||
|
||||
# Pin Vite version for Node 18
|
||||
if [ "$NODE_VERSION" -lt 20 ]; then
|
||||
echo "📌 Pinning Vite to $VITE_VERSION for Node 18 compatibility..."
|
||||
pnpm add -D vite@$VITE_VERSION
|
||||
fi
|
||||
|
||||
echo "📦 Installing Tailwind CSS and dependencies..."
|
||||
pnpm install -D tailwindcss@3.4.1 postcss autoprefixer @types/node tailwindcss-animate
|
||||
pnpm install class-variance-authority clsx tailwind-merge lucide-react next-themes
|
||||
|
||||
echo "⚙️ Creating Tailwind and PostCSS configuration..."
|
||||
cat > postcss.config.js << 'EOF'
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "📝 Configuring Tailwind with shadcn theme..."
|
||||
cat > tailwind.config.js << 'EOF'
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
EOF
|
||||
|
||||
# Add Tailwind directives and CSS variables to index.css
|
||||
echo "🎨 Adding Tailwind directives and CSS variables..."
|
||||
cat > src/index.css << 'EOF'
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Add path aliases to tsconfig.json
|
||||
echo "🔧 Adding path aliases to tsconfig.json..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8'));
|
||||
config.compilerOptions = config.compilerOptions || {};
|
||||
config.compilerOptions.baseUrl = '.';
|
||||
config.compilerOptions.paths = { '@/*': ['./src/*'] };
|
||||
fs.writeFileSync('tsconfig.json', JSON.stringify(config, null, 2));
|
||||
"
|
||||
|
||||
# Add path aliases to tsconfig.app.json
|
||||
echo "🔧 Adding path aliases to tsconfig.app.json..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = 'tsconfig.app.json';
|
||||
const content = fs.readFileSync(path, 'utf8');
|
||||
// Remove comments manually
|
||||
const lines = content.split('\n').filter(line => !line.trim().startsWith('//'));
|
||||
const jsonContent = lines.join('\n');
|
||||
const config = JSON.parse(jsonContent.replace(/\/\*[\s\S]*?\*\//g, '').replace(/,(\s*[}\]])/g, '\$1'));
|
||||
config.compilerOptions = config.compilerOptions || {};
|
||||
config.compilerOptions.baseUrl = '.';
|
||||
config.compilerOptions.paths = { '@/*': ['./src/*'] };
|
||||
fs.writeFileSync(path, JSON.stringify(config, null, 2));
|
||||
"
|
||||
|
||||
# Update vite.config.ts
|
||||
echo "⚙️ Updating Vite configuration..."
|
||||
cat > vite.config.ts << 'EOF'
|
||||
import path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
EOF
|
||||
|
||||
# Install all shadcn/ui dependencies
|
||||
echo "📦 Installing shadcn/ui dependencies..."
|
||||
pnpm install @radix-ui/react-accordion @radix-ui/react-aspect-ratio @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-collapsible @radix-ui/react-context-menu @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-menubar @radix-ui/react-navigation-menu @radix-ui/react-popover @radix-ui/react-progress @radix-ui/react-radio-group @radix-ui/react-scroll-area @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slider @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-toast @radix-ui/react-toggle @radix-ui/react-toggle-group @radix-ui/react-tooltip
|
||||
pnpm install sonner cmdk vaul embla-carousel-react react-day-picker react-resizable-panels date-fns react-hook-form @hookform/resolvers zod
|
||||
|
||||
# Extract shadcn components from tarball
|
||||
echo "📦 Extracting shadcn/ui components..."
|
||||
tar -xzf "$COMPONENTS_TARBALL" -C src/
|
||||
|
||||
# Create components.json for reference
|
||||
echo "📝 Creating components.json config..."
|
||||
cat > components.json << 'EOF'
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✅ Setup complete! You can now use Tailwind CSS and shadcn/ui in your project."
|
||||
echo ""
|
||||
echo "📦 Included components (40+ total):"
|
||||
echo " - accordion, alert, aspect-ratio, avatar, badge, breadcrumb"
|
||||
echo " - button, calendar, card, carousel, checkbox, collapsible"
|
||||
echo " - command, context-menu, dialog, drawer, dropdown-menu"
|
||||
echo " - form, hover-card, input, label, menubar, navigation-menu"
|
||||
echo " - popover, progress, radio-group, resizable, scroll-area"
|
||||
echo " - select, separator, sheet, skeleton, slider, sonner"
|
||||
echo " - switch, table, tabs, textarea, toast, toggle, toggle-group, tooltip"
|
||||
echo ""
|
||||
echo "To start developing:"
|
||||
echo " cd $PROJECT_NAME"
|
||||
echo " pnpm dev"
|
||||
echo ""
|
||||
echo "📚 Import components like:"
|
||||
echo " import { Button } from '@/components/ui/button'"
|
||||
echo " import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'"
|
||||
echo " import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'"
|
||||
Binary file not shown.
226
IFLOW.md
226
IFLOW.md
|
|
@ -1,226 +0,0 @@
|
|||
# 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)/ Typer(CLI工具)/ Streamlit(数据可视化) |
|
||||
| 包管理工具 | uv |
|
||||
| 代码规范 | PEP 8 + Google Python Style Guide |
|
||||
| 虚拟环境 | **强制启用**(uv venv) |
|
||||
|
||||
### 后端 - 其他组件
|
||||
|
||||
| 组件 | 选型 |
|
||||
| -------- | --------- |
|
||||
| 数据库 | MySQL 8.x |
|
||||
| 缓存 | Redis |
|
||||
|
||||
### 前端 - TypeScript + Vue 3
|
||||
|
||||
| 配置项 | 要求 |
|
||||
| -------- | ---------------------------- |
|
||||
| 语言版本 | TypeScript 5.x |
|
||||
| 开发框架 | Vue 3(Composition 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`(修复记录)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# 字典系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
字典系统是为了解决项目中数据字典不统一、硬编码问题而设计的统一管理方案。系统支持静态字典(本地定义)和动态字典(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. 动态字典会覆盖同名的静态字典数据
|
||||
|
|
@ -1,515 +0,0 @@
|
|||
# 用户志愿控制器 API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
用户志愿控制器 (UserVolunteerController) 提供了志愿管理的相关接口,包括志愿保存、详情查询、名称修改、列表查询、删除和切换功能。
|
||||
|
||||
**基础路径**: `/api/user/volunteer`
|
||||
|
||||
**认证方式**: 需要登录认证,通过 JWT Token 进行身份验证
|
||||
|
||||
---
|
||||
|
||||
## 1. 保存志愿明细
|
||||
|
||||
保存用户选择的志愿专业列表到当前激活的志愿表中。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `POST`
|
||||
- **路径**: `/api/user/volunteer/save`
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|--------|------|----------------------------------------|
|
||||
| keys | string[] | 是 | 志愿专业Key列表,格式:`学校代码_专业代码_招生代码` |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
[
|
||||
"10001_1001_001",
|
||||
"10002_2001_002",
|
||||
"10003_3001_003"
|
||||
]
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|----------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | string | 响应数据 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": "保存成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "未找到计算表名",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|----------------------------|
|
||||
| 500 | 获取用户成绩信息失败 |
|
||||
| 500 | 未找到计算表名 |
|
||||
| 500 | 查找志愿表失败 |
|
||||
| 500 | 请先创建志愿表 |
|
||||
| 500 | 查找专业信息失败 |
|
||||
| 500 | 删除旧数据失败 |
|
||||
| 500 | 保存失败 |
|
||||
|
||||
**业务逻辑**
|
||||
|
||||
1. 对传入的keys进行去重处理
|
||||
2. 获取当前登录用户的激活成绩信息
|
||||
3. 查找当前激活的志愿表
|
||||
4. 根据keys查找对应的专业信息
|
||||
5. 构建志愿记录列表,保持提交顺序
|
||||
6. 先删除旧的志愿记录,再批量插入新记录
|
||||
|
||||
---
|
||||
|
||||
## 2. 获取当前志愿单详情
|
||||
|
||||
获取当前激活志愿单的详细信息,包括志愿记录按批次分组展示。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `GET`
|
||||
- **路径**: `/api/user/volunteer/detail`
|
||||
|
||||
**请求参数**
|
||||
|
||||
无
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|--------------------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | object | 志愿详情数据 |
|
||||
|
||||
**data 数据结构**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|------------|--------|--------------------------|
|
||||
| volunteer | object | 志愿单基本信息 |
|
||||
| items | object | 按批次分组的志愿明细 |
|
||||
|
||||
**items 数据结构**
|
||||
|
||||
| 批次 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| 提前批 | array | 提前批志愿记录列表 |
|
||||
| 本科批 | array | 本科批志愿记录列表 |
|
||||
| 专科批 | array | 专科批志愿记录列表 |
|
||||
|
||||
**志愿记录项数据结构**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|------------------------|---------|--------------------------------|
|
||||
| volunteerID | string | 志愿单ID |
|
||||
| schoolCode | string | 学校代码 |
|
||||
| majorCode | string | 专业代码 |
|
||||
| enrollmentCode | string | 招生代码 |
|
||||
| indexs | int | 志愿序号 |
|
||||
| batch | string | 批次 |
|
||||
| enrollProbability | float64 | 录取概率 |
|
||||
| studentConvertedScore | float64 | 学生折合分 |
|
||||
| calculationMajorID | string | 计算专业ID |
|
||||
| schoolName | string | 学校名称 |
|
||||
| majorName | string | 专业名称 |
|
||||
| planNum | int | 计划人数 |
|
||||
| tuition | string | 学费 |
|
||||
| province | string | 省份 |
|
||||
| schoolNature | string | 院校性质 |
|
||||
| institutionType | string | 院校类型 |
|
||||
| majorDetail | string | 专业详情 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"volunteer": {
|
||||
"id": "123456789",
|
||||
"volunteerName": "我的志愿表",
|
||||
"scoreId": "987654321",
|
||||
"createType": 1,
|
||||
"state": 1
|
||||
},
|
||||
"items": {
|
||||
"提前批": [
|
||||
{
|
||||
"volunteerID": "123456789",
|
||||
"schoolCode": "10001",
|
||||
"majorCode": "1001",
|
||||
"enrollmentCode": "001",
|
||||
"indexs": 1,
|
||||
"batch": "本科提前批",
|
||||
"schoolName": "某某大学",
|
||||
"majorName": "美术学",
|
||||
"planNum": 30,
|
||||
"tuition": "8000元/年",
|
||||
"enrollProbability": 85.5
|
||||
}
|
||||
],
|
||||
"本科批": [],
|
||||
"专科批": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------------------------|
|
||||
| 500 | 获取用户成绩信息失败 |
|
||||
| 500 | 查找志愿表失败 |
|
||||
| 500 | 查找志愿明细失败 |
|
||||
|
||||
**批次分类规则**
|
||||
|
||||
- **提前批**: `提前批`、`本科提前批`
|
||||
- **专科批**: `高职高专`、`专科批`
|
||||
- **本科批**: 其他所有批次
|
||||
|
||||
---
|
||||
|
||||
## 3. 编辑志愿单名称
|
||||
|
||||
修改志愿单的名称。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `PUT`
|
||||
- **路径**: `/api/user/volunteer/updateName`
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|
||||
|--------|--------|------|--------|------------|
|
||||
| id | string | 是 | query | 志愿单ID |
|
||||
| name | string | 是 | query | 志愿单名称 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```
|
||||
PUT /api/user/volunteer/updateName?id=123456789&name=我的新志愿表
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|----------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | string | 响应数据 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": "更新成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "参数错误",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|----------------|
|
||||
| 400 | 参数错误 |
|
||||
| 500 | 更新失败 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 获取当前用户志愿单列表
|
||||
|
||||
分页查询当前用户的志愿单列表。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `GET`
|
||||
- **路径**: `/api/user/volunteer/list`
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 位置 | 默认值 | 说明 |
|
||||
|--------|------|------|-------|--------|--------|
|
||||
| page | int | 否 | query | 1 | 页码 |
|
||||
| size | int | 否 | query | 10 | 每页数量 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```
|
||||
GET /api/user/volunteer/list?page=1&size=10
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|----------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | object | 响应数据 |
|
||||
|
||||
**data 数据结构**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|--------|--------------|
|
||||
| items | array | 志愿单列表 |
|
||||
| total | int64 | 总数量 |
|
||||
|
||||
**志愿单数据结构**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------------|---------|------------------------------|
|
||||
| id | string | 志愿单ID |
|
||||
| volunteerName | string | 志愿单名称 |
|
||||
| scoreId | string | 关联成绩ID |
|
||||
| createType | int | 生成类型 (1.手动, 2.智能) |
|
||||
| state | string | 状态 (0.未使用, 1.使用中, 2.历史) |
|
||||
| createBy | string | 创建人 |
|
||||
| createTime | string | 创建时间 |
|
||||
| updateBy | string | 更新人 |
|
||||
| updateTime | string | 更新时间 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "123456789",
|
||||
"volunteerName": "我的志愿表",
|
||||
"scoreId": "987654321",
|
||||
"createType": 1,
|
||||
"state": "1",
|
||||
"createBy": "user001",
|
||||
"createTime": "2026-01-31T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|----------------|
|
||||
| 500 | 查询失败 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 删除志愿单
|
||||
|
||||
删除指定的志愿单。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `DELETE`
|
||||
- **路径**: `/api/user/volunteer/delete`
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|
||||
|--------|--------|------|-------|----------|
|
||||
| id | string | 是 | query | 志愿单ID |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```
|
||||
DELETE /api/user/volunteer/delete?id=123456789
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|----------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | string | 响应数据 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": "删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "参数错误",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|----------------|
|
||||
| 400 | 参数错误 |
|
||||
| 500 | 删除失败 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 切换当前志愿单
|
||||
|
||||
切换当前激活的志愿单。
|
||||
|
||||
**请求**
|
||||
|
||||
- **方法**: `POST`
|
||||
- **路径**: `/api/user/volunteer/switch`
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 位置 | 说明 |
|
||||
|--------|--------|------|-------|----------|
|
||||
| id | string | 是 | query | 志愿单ID |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```
|
||||
POST /api/user/volunteer/switch?id=123456789
|
||||
```
|
||||
|
||||
**响应**
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|----------|--------|----------|
|
||||
| code | int | 状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| data | string | 响应数据 |
|
||||
|
||||
**成功响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": "切换成功"
|
||||
}
|
||||
```
|
||||
|
||||
**特殊响应示例(已是当前志愿单)**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": "已是当前志愿单,忽略切换"
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|----------------------------|
|
||||
| 400 | 参数错误 |
|
||||
| 500 | 获取用户成绩信息失败 |
|
||||
| 500 | 切换失败 |
|
||||
|
||||
**业务逻辑**
|
||||
|
||||
1. 检查当前是否已是该志愿单,如果是则忽略切换
|
||||
2. 执行志愿单切换操作
|
||||
3. Redis 缓存同步(由 Service 层处理)
|
||||
|
||||
---
|
||||
|
||||
## 通用响应格式说明
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 错误码,
|
||||
"message": "错误信息",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|--------------------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 认证说明
|
||||
|
||||
所有接口都需要在请求头中携带 JWT Token:
|
||||
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
Token 从登录接口获取,有效期24小时。
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **保存志愿明细**: 每次保存会先删除当前志愿表中的所有记录,再插入新记录
|
||||
2. **批次分类**: 志愿详情按 提前批、本科批、专科批 三大类分组展示
|
||||
3. **数据去重**: 保存志愿时会自动去重,避免重复添加相同专业
|
||||
4. **顺序保持**: 志愿序号按照提交的 keys 顺序依次递增
|
||||
5. **权限控制**: 所有操作都基于当前登录用户的身份,只能操作自己的志愿数据
|
||||
169
project_index.md
169
project_index.md
|
|
@ -17,172 +17,3 @@
|
|||
- `src/pages/privacy-policy.vue`: Privacy policy page.
|
||||
- `src/pages/simulate.vue`: Simulation and volunteer filling page.
|
||||
- `src/service/api/volunteer.ts`: Volunteer management API.
|
||||
|
||||
## 完整项目结构
|
||||
|
||||
### 项目根目录
|
||||
|
||||
```
|
||||
D:\workspace_4\yitisheng\vitesse-yitisheng-web/
|
||||
├── .agent/ # Agent相关配置
|
||||
├── .iflow/ # iFlow相关配置
|
||||
├── .kilocode/ # Kilocode相关配置
|
||||
├── .trae/ # Trae相关配置
|
||||
├── .vscode/ # VSCode配置
|
||||
├── cypress/ # E2E测试配置
|
||||
├── docs/ # 项目文档
|
||||
├── locales/ # 多语言配置文件
|
||||
├── public/ # 静态资源文件
|
||||
├── src/ # 源代码目录
|
||||
├── tasks/ # 任务相关文件
|
||||
├── test/ # 测试文件
|
||||
├── .dockerignore
|
||||
├── .editorconfig
|
||||
├── .env.development
|
||||
├── .env.production
|
||||
├── .gitignore
|
||||
├── .npmrc
|
||||
├── 1212.txt
|
||||
├── cypress.config.ts
|
||||
├── Dockerfile
|
||||
├── eslint.config.js
|
||||
├── index.html
|
||||
├── jsconfig.json
|
||||
├── LICENSE
|
||||
├── netlify.toml
|
||||
├── package.json
|
||||
├── pnpm-lock.yaml
|
||||
├── pnpm-workspace.yaml
|
||||
├── postcss.config.js
|
||||
├── prettier.config.mjs
|
||||
├── project_codebase.md
|
||||
├── project_doing.md
|
||||
├── project_index.md
|
||||
├── project_task.md
|
||||
├── README.md
|
||||
├── tailwind.config.js
|
||||
├── task_detail.md
|
||||
├── tsconfig.json
|
||||
├── uno.config.ts
|
||||
├── vite.config.ts
|
||||
```
|
||||
|
||||
### 源代码目录 (src/)
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 组件目录
|
||||
│ ├── ui/ # UI组件
|
||||
│ │ ├── WLoading.vue
|
||||
│ │ ├── WMessage.vue
|
||||
│ │ ├── WOption.vue
|
||||
│ │ ├── WPopconfirm.vue
|
||||
│ │ ├── WRadioButton.vue
|
||||
│ │ ├── WRadioGroup.vue
|
||||
│ │ └── WSelect.vue
|
||||
│ ├── BackToTop.vue
|
||||
│ ├── FilterBar.vue
|
||||
│ ├── LoginForm.vue
|
||||
│ ├── README.md
|
||||
│ ├── ScoreForm.vue
|
||||
│ ├── TheCounter.vue
|
||||
│ ├── TheFooter.vue
|
||||
│ ├── TheInput.vue
|
||||
│ └── TheNavigation.vue
|
||||
├── composables/ # Vue Composables
|
||||
│ ├── dark.ts
|
||||
│ └── request.ts
|
||||
├── layouts/ # 布局组件
|
||||
│ ├── 404.vue
|
||||
│ ├── default.vue
|
||||
│ ├── home.vue
|
||||
│ └── README.md
|
||||
├── lib/ # 第三方库
|
||||
├── modules/ # 项目模块
|
||||
│ ├── i18n.ts
|
||||
│ ├── nprogress.ts
|
||||
│ ├── pinia.ts
|
||||
│ ├── pwa.ts
|
||||
│ └── README.md
|
||||
├── pages/ # 页面组件(基于文件路由)
|
||||
│ ├── demo/
|
||||
│ │ └── pop-confirm.vue
|
||||
│ ├── hi/
|
||||
│ │ └── [name].vue
|
||||
│ ├── school/
|
||||
│ │ └── [schoolCode].vue
|
||||
│ ├── [...all].vue
|
||||
│ ├── about.md
|
||||
│ ├── about.vue
|
||||
│ ├── agreement.vue
|
||||
│ ├── contact-us.vue
|
||||
│ ├── index.vue
|
||||
│ ├── majors.vue
|
||||
│ ├── privacy-policy.vue
|
||||
│ ├── README.md
|
||||
│ ├── simulate.vue
|
||||
│ └── universities.vue
|
||||
├── service/ # 服务层
|
||||
│ ├── api/ # API接口
|
||||
│ │ ├── auth.ts
|
||||
│ │ ├── major.ts
|
||||
│ │ ├── score.ts
|
||||
│ │ └── volunteer.ts
|
||||
│ └── request/ # 请求封装
|
||||
│ └── index.ts
|
||||
├── stores/ # 状态管理 (Pinia)
|
||||
│ ├── score.ts
|
||||
│ └── user.ts
|
||||
├── styles/ # 样式文件
|
||||
│ ├── main.css
|
||||
│ └── markdown.css
|
||||
├── utils/ # 工具函数
|
||||
│ ├── loading.ts
|
||||
│ └── message.ts
|
||||
├── App.vue # 根组件
|
||||
├── auto-imports.d.ts
|
||||
├── components.d.ts
|
||||
├── main.ts # 入口文件
|
||||
├── shims.d.ts
|
||||
├── typed-router.d.ts
|
||||
└── types.ts # 类型定义
|
||||
```
|
||||
|
||||
### 静态资源目录 (public/)
|
||||
|
||||
```
|
||||
public/
|
||||
├── assets/
|
||||
│ └── fonts/ # 字体文件
|
||||
├── _headers
|
||||
├── beian.ico
|
||||
├── download.png
|
||||
├── favicon-dark.svg
|
||||
├── favicon.svg
|
||||
├── pwa-192x192.png
|
||||
├── pwa-512x512.png
|
||||
└── safari-pinned-tab.svg
|
||||
```
|
||||
|
||||
### 服务API目录 (src/service/api/)
|
||||
|
||||
```
|
||||
src/service/api/
|
||||
├── auth.ts # 认证相关API
|
||||
├── major.ts # 专业相关API
|
||||
├── score.ts # 成绩相关API
|
||||
├── volunteer.ts # 志愿相关API
|
||||
```
|
||||
|
||||
### 项目特点
|
||||
|
||||
- 基于 Vitesse 模板(Vue 3 + Vite)
|
||||
- 使用 TypeScript
|
||||
- 采用 Vue 3 Composition API
|
||||
- 使用 Pinia 进行状态管理
|
||||
- 支持国际化 (i18n)
|
||||
- 使用 UnoCSS 作为样式引擎
|
||||
- 包含 PWA 支持
|
||||
- 包含组件自动化加载
|
||||
- 基于文件的路由系统
|
||||
- 包含服务端生成 (SSG) 支持
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ declare global {
|
|||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDictStore: typeof import('./stores/dict')['useDictStore']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
|
|
@ -496,7 +495,6 @@ declare module 'vue' {
|
|||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
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 useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,11 @@ declare module 'vue' {
|
|||
export interface GlobalComponents {
|
||||
BackToTop: typeof import('./components/BackToTop.vue')['default']
|
||||
copy: typeof import('./components/ScoreForm copy.vue')['default']
|
||||
DictDemo: typeof import('./components/DictDemo.vue')['default']
|
||||
FilterBar: typeof import('./components/FilterBar.vue')['default']
|
||||
LoginForm: typeof import('./components/LoginForm.vue')['default']
|
||||
README: typeof import('./components/README.md')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScoreDictForm: typeof import('./components/ScoreDictForm.vue')['default']
|
||||
ScoreForm: typeof import('./components/ScoreForm.vue')['default']
|
||||
TheCounter: typeof import('./components/TheCounter.vue')['default']
|
||||
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
||||
|
|
|
|||
|
|
@ -1,192 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useScoreStore } from '~/stores/score'
|
||||
import { useDictStore } from '~/stores/dict'
|
||||
import { type SaveScoreRequest } from '~/service/api/score'
|
||||
import message from '~/utils/message'
|
||||
|
||||
const scoreStore = useScoreStore()
|
||||
const dictStore = useDictStore()
|
||||
|
||||
// 定义回传的数据类型
|
||||
export interface ScoreFormData {
|
||||
|
|
@ -56,20 +54,15 @@ const errors = ref({
|
|||
},
|
||||
})
|
||||
|
||||
// --- Computed Properties using Dict System ---
|
||||
const electiveOptions = computed(() => {
|
||||
// 使用自定义科目字典,如果没有则使用默认科目
|
||||
return dictStore.getDictItems('subjectList') || [
|
||||
// --- Options Data ---
|
||||
const electiveOptions = [
|
||||
{ label: '地理', value: '地理' },
|
||||
{ label: '政治', value: '政治' },
|
||||
{ label: '化学', value: '化学' },
|
||||
{ label: '生物', value: '生物' },
|
||||
]
|
||||
})
|
||||
|
||||
const majorCategoryOptions = computed(() => {
|
||||
// 使用专业类别字典
|
||||
return dictStore.getDictItems('professionalCategory') || [
|
||||
const majorCategoryOptions = [
|
||||
{ label: '美术与设计类', value: '美术与设计类' },
|
||||
{ label: '播音与主持类', value: '播音与主持类' },
|
||||
{ label: '表演类', value: '表演类' },
|
||||
|
|
@ -79,7 +72,27 @@ const majorCategoryOptions = computed(() => {
|
|||
{ 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) {
|
||||
console.warn('handleElectiveChange', value)
|
||||
|
|
@ -246,16 +259,7 @@ function initForm() {
|
|||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 尝试加载字典数据
|
||||
if (Object.keys(dictStore.allDicts).length === 0) {
|
||||
try {
|
||||
await dictStore.loadDynamicDicts()
|
||||
} catch (error) {
|
||||
console.warn('加载字典数据失败,使用默认值:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!scoreStore.scoreInfo) {
|
||||
// scoreStore.fetchScore().catch(() => {
|
||||
// // 忽略错误,可能用户未设置成绩
|
||||
|
|
|
|||
|
|
@ -1,229 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -3,7 +3,7 @@ import type { FilterState } from '~/components/FilterBar.vue'
|
|||
import { onMounted, ref, watch, computed } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { getUserMajorList, type MajorItem } from '~/service/api/major'
|
||||
import { saveVolunteer, getVolunteerDetail, updateVolunteerName, getVolunteerList, deleteVolunteer, switchVolunteer, type VolunteerItem, type VolunteerInfo, type VolunteerPlanItem } from '~/service/api/volunteer'
|
||||
import { saveVolunteer, getVolunteerDetail, type VolunteerItem, type VolunteerInfo } from '~/service/api/volunteer'
|
||||
|
||||
// --- 类型定义 ---
|
||||
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | 'stable' | '本科' | '专科' | '985/211/双一流' | '公办本科' | '民办本科'
|
||||
|
|
@ -32,12 +32,34 @@ interface MajorDetail {
|
|||
|
||||
// --- 状态数据 ---
|
||||
const showSwitchModal = ref(false) // 控制切换方案弹窗显示
|
||||
const showEditNameModal = ref(false) // 控制编辑名称弹窗显示
|
||||
const editingPlanName = ref('') // 正在编辑的志愿单名称
|
||||
const isUpdatingName = ref(false) // 更新名称Loading状态
|
||||
const activePlanId = ref('') // 当前选中的方案ID
|
||||
const activePlanId = ref('2025121426') // 当前选中的方案ID
|
||||
// 模拟方案列表数据 (对应截图)
|
||||
const volunteerPlans = ref([])
|
||||
const volunteerPlans = ref([
|
||||
{
|
||||
id: '2025121426',
|
||||
name: '志愿2025121426',
|
||||
tag: '手动', // 截图中的橙色标签
|
||||
province: '北京',
|
||||
artType: '美术与设计类',
|
||||
cultureScore: 450,
|
||||
cultureSubjects: '物化生',
|
||||
artScore: 250,
|
||||
updateTime: '2025-12-14 10:33:46',
|
||||
status: '有效',
|
||||
},
|
||||
{
|
||||
id: '2025121427',
|
||||
name: '志愿2025121427',
|
||||
tag: '智能',
|
||||
province: '湖北',
|
||||
artType: '音乐类',
|
||||
cultureScore: 480,
|
||||
cultureSubjects: '历史',
|
||||
artScore: 240,
|
||||
updateTime: '2025-12-13 14:20:00',
|
||||
status: '有效',
|
||||
},
|
||||
])
|
||||
|
||||
const activePanel = ref<PanelType>('market') // 当前激活的面板
|
||||
const myVolunteers = ref<VolunteerItem[]>([])
|
||||
|
|
@ -538,133 +560,30 @@ function handleCreatePlan() {
|
|||
}
|
||||
|
||||
function handleEditPlan() {
|
||||
if (!currentVolunteerInfo.value?.volunteerName) {
|
||||
// @ts-ignore
|
||||
window.$message?.warning?.('当前志愿单信息为空')
|
||||
return
|
||||
}
|
||||
editingPlanName.value = currentVolunteerInfo.value.volunteerName
|
||||
showEditNameModal.value = true
|
||||
console.warn('点击修改方案信息')
|
||||
// 逻辑:修改当前方案的分数/选科等...
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存志愿单名称
|
||||
*/
|
||||
async function saveVolunteerName() {
|
||||
if (!currentVolunteerInfo.value?.id || !editingPlanName.value.trim()) {
|
||||
// @ts-ignore
|
||||
window.$message?.warning?.('志愿单名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
isUpdatingName.value = true
|
||||
try {
|
||||
await updateVolunteerName(currentVolunteerInfo.value.id, editingPlanName.value.trim())
|
||||
// @ts-ignore
|
||||
window.$message?.success?.('名称修改成功')
|
||||
// 更新本地数据
|
||||
if (currentVolunteerInfo.value) {
|
||||
currentVolunteerInfo.value.volunteerName = editingPlanName.value.trim()
|
||||
}
|
||||
showEditNameModal.value = false
|
||||
} catch (error) {
|
||||
console.error('修改志愿单名称失败:', error)
|
||||
// @ts-ignore
|
||||
window.$message?.error?.('修改失败,请重试')
|
||||
} finally {
|
||||
isUpdatingName.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消编辑名称
|
||||
*/
|
||||
function cancelEditName() {
|
||||
showEditNameModal.value = false
|
||||
editingPlanName.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开切换方案弹框并加载方案列表
|
||||
*/
|
||||
async function handleSwitchPlan() {
|
||||
function handleSwitchPlan() {
|
||||
showSwitchModal.value = true
|
||||
try {
|
||||
const res = await getVolunteerList(1, 100)
|
||||
if (res && res.items) {
|
||||
// 将 API 返回的数据转换为界面需要的格式
|
||||
volunteerPlans.value = res.items.map(item => ({
|
||||
id: item.id,
|
||||
name: item.volunteerName,
|
||||
tag: item.createType === '1' ? '手动' : '智能',
|
||||
province: '河南', // API 返回数据中没有这些字段,暂时使用默认值
|
||||
artType: '美术与设计类',
|
||||
culturalScore: item.culturalScore,
|
||||
professionalScore: item.professionalScore,
|
||||
cultureSubjects: '-',
|
||||
updateTime: item.updateTime,
|
||||
status: item.state === '1' ? '使用中' : (item.state === '0' ? '未使用' : '历史'),
|
||||
}))
|
||||
// 更新当前激活的方案ID
|
||||
if (currentVolunteerInfo.value?.id) {
|
||||
activePlanId.value = currentVolunteerInfo.value.id
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取志愿单列表失败:', error)
|
||||
// @ts-ignore
|
||||
window.$message?.error?.('获取志愿单列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleExportPlan() {
|
||||
console.warn('点击导出当前方案')
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定方案
|
||||
*/
|
||||
async function switchActivePlan(planId: string) {
|
||||
try {
|
||||
await switchVolunteer(planId)
|
||||
// 切换到指定方案
|
||||
function switchActivePlan(planId: string) {
|
||||
activePlanId.value = planId
|
||||
showSwitchModal.value = false
|
||||
// @ts-ignore
|
||||
window.$message?.success?.('切换志愿单成功')
|
||||
// 重新加载志愿详情
|
||||
await fetchVolunteerDetail()
|
||||
} catch (error) {
|
||||
console.error('切换志愿单失败:', error)
|
||||
// @ts-ignore
|
||||
window.$message?.error?.('切换志愿单失败')
|
||||
}
|
||||
console.warn('切换到了方案:', planId)
|
||||
// 逻辑:重新加载 myVolunteers 数据...
|
||||
// isLoading.value = true ...
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除志愿单
|
||||
*/
|
||||
async function deletePlan(planId: string) {
|
||||
const confirmDelete = window.confirm('确定要删除这个志愿单吗?删除后无法恢复。')
|
||||
if (!confirmDelete) return
|
||||
|
||||
try {
|
||||
await deleteVolunteer(planId)
|
||||
// @ts-ignore
|
||||
window.$message?.success?.('删除成功')
|
||||
// 从列表中移除
|
||||
const index = volunteerPlans.value.findIndex(p => p.id === planId)
|
||||
if (index > -1) {
|
||||
volunteerPlans.value.splice(index, 1)
|
||||
}
|
||||
// 如果删除的是当前激活的志愿单,重新加载详情
|
||||
if (activePlanId.value === planId) {
|
||||
await fetchVolunteerDetail()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除志愿单失败:', error)
|
||||
// @ts-ignore
|
||||
window.$message?.error?.('删除失败')
|
||||
}
|
||||
function deletePlan(planId: string) {
|
||||
console.warn('删除方案:', planId)
|
||||
// 逻辑:删除API调用...
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -1559,7 +1478,8 @@ async function deletePlan(planId: string) {
|
|||
class="appearance-none border border-slate-300 dark:border-slate-700 rounded bg-white dark:bg-slate-800 py-1.5 pl-3 pr-8 text-sm text-slate-700 dark:text-slate-300 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option>请选择省份</option>
|
||||
<option>河南</option>
|
||||
<option>北京</option>
|
||||
<option>湖北</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500 dark:text-slate-600">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -1620,12 +1540,12 @@ async function deletePlan(planId: string) {
|
|||
{{ plan.artType }}
|
||||
</td>
|
||||
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center">
|
||||
<span class="text-slate-800 dark:text-slate-200 font-medium">{{ plan.culturalScore }}</span>
|
||||
<!-- <span class="ml-1 text-xs text-slate-400 dark:text-slate-500">{{ plan.professionalScore }}</span> -->
|
||||
<span class="text-slate-800 dark:text-slate-200 font-medium">{{ plan.cultureScore }}</span>
|
||||
<span class="ml-1 text-xs text-slate-400 dark:text-slate-500">{{ plan.cultureSubjects }}</span>
|
||||
</td>
|
||||
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center text-slate-800 dark:text-slate-200 font-medium">
|
||||
{{
|
||||
plan.professionalScore }}
|
||||
plan.artScore }}
|
||||
</td>
|
||||
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center text-xs text-slate-500 dark:text-slate-500 font-mono">
|
||||
{{
|
||||
|
|
@ -1664,72 +1584,6 @@ async function deletePlan(planId: string) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =========================================================
|
||||
编辑名称弹框 (新增)
|
||||
========================================================== -->
|
||||
<div
|
||||
v-if="showEditNameModal"
|
||||
class="fixed inset-0 z-[110] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
@click.self="cancelEditName"
|
||||
>
|
||||
<div
|
||||
class="w-[400px] flex flex-col animate-fade-in-up overflow-hidden rounded-lg bg-white dark:bg-slate-900 shadow-2xl border border-transparent dark:border-slate-800"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 px-6 py-4">
|
||||
<h3 class="text-lg text-slate-800 dark:text-slate-100 font-bold">
|
||||
修改志愿单名称
|
||||
</h3>
|
||||
<button class="text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300" @click="cancelEditName">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
志愿单名称
|
||||
</label>
|
||||
<input
|
||||
v-model="editingPlanName"
|
||||
type="text"
|
||||
class="w-full border border-slate-300 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 px-4 py-2.5 text-sm text-slate-900 dark:text-slate-100 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
placeholder="请输入志愿单名称"
|
||||
maxlength="50"
|
||||
@keyup.enter="saveVolunteerName"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
最多输入 50 个字符
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50 px-6 py-4">
|
||||
<button
|
||||
class="rounded-lg px-4 py-2 text-sm text-slate-600 dark:text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
@click="cancelEditName"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="!editingPlanName.trim() || isUpdatingName"
|
||||
@click="saveVolunteerName"
|
||||
>
|
||||
<span v-if="isUpdatingName" class="mr-1 inline-block h-3 w-3 animate-spin border-2 border-white/30 border-t-white rounded-full"></span>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,316 +0,0 @@
|
|||
<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'
|
||||
|
||||
// 使用字典Store和成绩Store
|
||||
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>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* 字典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,可以在这里添加
|
||||
|
|
@ -28,29 +28,12 @@ export interface VolunteerDetailResponse {
|
|||
items: Record<string, VolunteerItem[]>
|
||||
}
|
||||
|
||||
export interface VolunteerPlanItem {
|
||||
id: string
|
||||
volunteerName: string
|
||||
scoreId: string
|
||||
createType: string
|
||||
state: string
|
||||
createBy: string
|
||||
createTime: string
|
||||
updateBy: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
export interface VolunteerListResponse {
|
||||
items: VolunteerPlanItem[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存志愿明细
|
||||
* @param data schoolCode_majorCode_enrollmentCode 字符串数组
|
||||
*/
|
||||
export function saveVolunteer(data: string[]) {
|
||||
return request.post('/user/volunteer/save', {keys:data})
|
||||
return request.post('/user/volunteer/save', data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,37 +42,3 @@ export function saveVolunteer(data: string[]) {
|
|||
export function getVolunteerDetail() {
|
||||
return request.get<VolunteerDetailResponse>('/user/volunteer/detail')
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑志愿单名称
|
||||
* @param id 志愿单ID
|
||||
* @param name 志愿单名称
|
||||
*/
|
||||
export function updateVolunteerName(id: string, name: string) {
|
||||
return request.put('/user/volunteer/updateName', null, { params: { id, name } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户志愿单列表
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
*/
|
||||
export function getVolunteerList(page: number = 1, size: number = 10) {
|
||||
return request.get<VolunteerListResponse>('/user/volunteer/list', { params: { page, size } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除志愿单
|
||||
* @param id 志愿单ID
|
||||
*/
|
||||
export function deleteVolunteer(id: string) {
|
||||
return request.delete('/user/volunteer/delete', { params: { id } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换当前志愿单
|
||||
* @param id 志愿单ID
|
||||
*/
|
||||
export function switchVolunteer(id: string) {
|
||||
return request.post('/user/volunteer/switch', null, { params: { id } })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,14 +141,6 @@ class Request {
|
|||
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.post(url, data, config)
|
||||
}
|
||||
|
||||
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.put(url, data, config)
|
||||
}
|
||||
|
||||
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return this.instance.delete(url, config)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Request({
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
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))
|
||||
|
|
@ -24,7 +24,6 @@ declare module 'vue-router/auto-routes' {
|
|||
'/agreement': RouteRecordInfo<'/agreement', '/agreement', 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>>,
|
||||
'/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> }>,
|
||||
'/majors': RouteRecordInfo<'/majors', '/majors', Record<never, never>, Record<never, never>>,
|
||||
'/privacy-policy': RouteRecordInfo<'/privacy-policy', '/privacy-policy', Record<never, never>, Record<never, never>>,
|
||||
|
|
@ -32,6 +31,5 @@ declare module 'vue-router/auto-routes' {
|
|||
'/school/[schoolCode]': RouteRecordInfo<'/school/[schoolCode]', '/school/:schoolCode', { schoolCode: ParamValue<true> }, { schoolCode: ParamValue<false> }>,
|
||||
'/simulate': RouteRecordInfo<'/simulate', '/simulate', 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>>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
/**
|
||||
* 字典工具类 - 用于管理应用中的所有字典数据
|
||||
* 包括静态字典(本地定义)和动态字典(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' },
|
||||
{ label: '舞蹈类', value: 'sports', color: '#ff5500' },
|
||||
{ label: '书法类', value: 'sports', color: '#ff5500' },
|
||||
{ label: '戏曲类', value: 'sports', color: '#ff5500' },
|
||||
{ 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 }
|
||||
|
|
@ -36,7 +36,7 @@ const Message = (options: MessageOptions) => {
|
|||
if (!messageContainer) {
|
||||
messageContainer = document.createElement('div')
|
||||
// Common classes
|
||||
let classes = `w-message-container ${containerClass} fixed z-[9999] flex pointer-events-none transition-all duration-300`
|
||||
let classes = `w-message-container ${containerClass} fixed z-50 flex pointer-events-none transition-all duration-300`
|
||||
// Position specific classes
|
||||
classes += ` ${positionClasses[position]}`
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue