Compare commits

..

3 Commits

Author SHA1 Message Date
zhouwentao d5cffd2a76 updates 2026-02-01 13:48:16 +08:00
zhouwentao 1b7baf85df updates 2026-01-31 19:31:41 +08:00
zhouwentao a67857a012 updates 2026-01-26 17:55:35 +08:00
27 changed files with 3761 additions and 88 deletions

187
.iflow/rules/rule.md Normal file
View File

@ -0,0 +1,187 @@
---
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秒原则" | 功能可识别性 > 视觉简洁性 |

View File

@ -0,0 +1,177 @@
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

View File

@ -0,0 +1,42 @@
---
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.

View File

@ -0,0 +1,202 @@
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.

View File

@ -0,0 +1,74 @@
---
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

View File

@ -0,0 +1,54 @@
#!/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"

View File

@ -0,0 +1,322 @@
#!/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'"

226
IFLOW.md Normal file
View File

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

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

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

View File

@ -0,0 +1,515 @@
# 用户志愿控制器 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. **权限控制**: 所有操作都基于当前登录用户的身份,只能操作自己的志愿数据

View File

@ -17,3 +17,172 @@
- `src/pages/privacy-policy.vue`: Privacy policy page. - `src/pages/privacy-policy.vue`: Privacy policy page.
- `src/pages/simulate.vue`: Simulation and volunteer filling page. - `src/pages/simulate.vue`: Simulation and volunteer filling page.
- `src/service/api/volunteer.ts`: Volunteer management API. - `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) 支持

View File

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

2
src/components.d.ts vendored
View File

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

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

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

View File

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

View File

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

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

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

View File

@ -3,7 +3,7 @@ import type { FilterState } from '~/components/FilterBar.vue'
import { onMounted, ref, watch, computed } from 'vue' import { onMounted, ref, watch, computed } from 'vue'
import { onBeforeRouteLeave } from 'vue-router' import { onBeforeRouteLeave } from 'vue-router'
import { getUserMajorList, type MajorItem } from '~/service/api/major' import { getUserMajorList, type MajorItem } from '~/service/api/major'
import { saveVolunteer, getVolunteerDetail, type VolunteerItem, type VolunteerInfo } from '~/service/api/volunteer' import { saveVolunteer, getVolunteerDetail, updateVolunteerName, getVolunteerList, deleteVolunteer, switchVolunteer, type VolunteerItem, type VolunteerInfo, type VolunteerPlanItem } from '~/service/api/volunteer'
// --- --- // --- ---
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | 'stable' | '本科' | '专科' | '985/211/双一流' | '公办本科' | '民办本科' type TabKey = 'all' | 'hard' | 'risky' | 'safe' | 'stable' | '本科' | '专科' | '985/211/双一流' | '公办本科' | '民办本科'
@ -32,34 +32,12 @@ interface MajorDetail {
// --- --- // --- ---
const showSwitchModal = ref(false) // const showSwitchModal = ref(false) //
const activePlanId = ref('2025121426') // ID const showEditNameModal = ref(false) //
const editingPlanName = ref('') //
const isUpdatingName = ref(false) // Loading
const activePlanId = ref('') // 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 activePanel = ref<PanelType>('market') //
const myVolunteers = ref<VolunteerItem[]>([]) const myVolunteers = ref<VolunteerItem[]>([])
@ -560,30 +538,133 @@ function handleCreatePlan() {
} }
function handleEditPlan() { function handleEditPlan() {
console.warn('点击修改方案信息') if (!currentVolunteerInfo.value?.volunteerName) {
// /... // @ts-ignore
window.$message?.warning?.('当前志愿单信息为空')
return
}
editingPlanName.value = currentVolunteerInfo.value.volunteerName
showEditNameModal.value = true
} }
function handleSwitchPlan() { /**
* 保存志愿单名称
*/
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() {
showSwitchModal.value = true 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() { function handleExportPlan() {
console.warn('点击导出当前方案') console.warn('点击导出当前方案')
} }
// /**
function switchActivePlan(planId: string) { * 切换到指定方案
*/
async function switchActivePlan(planId: string) {
try {
await switchVolunteer(planId)
activePlanId.value = planId activePlanId.value = planId
showSwitchModal.value = false showSwitchModal.value = false
console.warn('切换到了方案:', planId) // @ts-ignore
// myVolunteers ... window.$message?.success?.('切换志愿单成功')
// isLoading.value = true ... //
await fetchVolunteerDetail()
} catch (error) {
console.error('切换志愿单失败:', error)
// @ts-ignore
window.$message?.error?.('切换志愿单失败')
}
} }
function deletePlan(planId: string) { /**
console.warn('删除方案:', planId) * 删除志愿单
// API... */
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?.('删除失败')
}
} }
</script> </script>
@ -1478,8 +1559,7 @@ 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" 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>
<option>湖北</option>
</select> </select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500 dark:text-slate-600"> <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"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -1540,12 +1620,12 @@ function deletePlan(planId: string) {
{{ plan.artType }} {{ plan.artType }}
</td> </td>
<td class="border-r border-slate-100 dark:border-slate-800 px-4 py-3 text-center"> <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.cultureScore }}</span> <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.cultureSubjects }}</span> <!-- <span class="ml-1 text-xs text-slate-400 dark:text-slate-500">{{ plan.professionalScore }}</span> -->
</td> </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"> <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.artScore }} plan.professionalScore }}
</td> </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"> <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">
{{ {{
@ -1584,6 +1664,72 @@ function deletePlan(planId: string) {
</div> </div>
</div> </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> </Teleport>
</div> </div>
</template> </template>

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

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

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

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

View File

@ -28,12 +28,29 @@ export interface VolunteerDetailResponse {
items: Record<string, VolunteerItem[]> 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 * @param data schoolCode_majorCode_enrollmentCode
*/ */
export function saveVolunteer(data: string[]) { export function saveVolunteer(data: string[]) {
return request.post('/user/volunteer/save', data) return request.post('/user/volunteer/save', {keys:data})
} }
/** /**
@ -42,3 +59,37 @@ export function saveVolunteer(data: string[]) {
export function getVolunteerDetail() { export function getVolunteerDetail() {
return request.get<VolunteerDetailResponse>('/user/volunteer/detail') 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 } })
}

View File

@ -141,6 +141,14 @@ class Request {
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> { post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.instance.post(url, data, config) 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({ export default new Request({

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

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

View File

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

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

@ -0,0 +1,175 @@
/**
* -
* 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 }

View File

@ -36,7 +36,7 @@ const Message = (options: MessageOptions) => {
if (!messageContainer) { if (!messageContainer) {
messageContainer = document.createElement('div') messageContainer = document.createElement('div')
// Common classes // Common classes
let classes = `w-message-container ${containerClass} fixed z-50 flex pointer-events-none transition-all duration-300` let classes = `w-message-container ${containerClass} fixed z-[9999] flex pointer-events-none transition-all duration-300`
// Position specific classes // Position specific classes
classes += ` ${positionClasses[position]}` classes += ` ${positionClasses[position]}`