Compare commits
9 Commits
7f27542057
...
8b9602ff4c
| Author | SHA1 | Date |
|---|---|---|
|
|
8b9602ff4c | |
|
|
b02d66cc2b | |
|
|
796bb6b2f6 | |
|
|
738e969a98 | |
|
|
ca71de90f0 | |
|
|
bb475734f6 | |
|
|
57c6de808d | |
|
|
da16f9fdca | |
|
|
5d9bc3ba5a |
|
|
@ -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秒原则" | 功能可识别性 > 视觉简洁性 |
|
||||||
|
|
@ -37,8 +37,15 @@
|
||||||
- **Purpose**: Displays the Privacy Policy.
|
- **Purpose**: Displays the Privacy Policy.
|
||||||
- **Features**: Static content detailing data collection, usage, and protection. Includes contact information. Responsive layout.
|
- **Features**: Static content detailing data collection, usage, and protection. Includes contact information. Responsive layout.
|
||||||
|
|
||||||
|
### `src/service/api/volunteer.ts`
|
||||||
|
- **Purpose**: API definitions for volunteer filling management.
|
||||||
|
- **Methods**: `saveVolunteer`, `getVolunteerDetail`.
|
||||||
|
- **Types**: `VolunteerInfo`, `VolunteerItem`, `VolunteerDetailResponse`.
|
||||||
|
|
||||||
### `src/pages/simulate.vue`
|
### `src/pages/simulate.vue`
|
||||||
- **Purpose**: Volunteer simulation page.
|
- **Updated**:
|
||||||
- **Features**:
|
- Integrated `getVolunteerDetail` and `saveVolunteer`.
|
||||||
- Panel A: Displays recommended majors list fetched from API (`/user/major/list`). Supports infinite scroll and filtering by probability.
|
- Implemented `isModified` state for unsaved changes detection.
|
||||||
- Panel B: Displays user's selected volunteers (Mock data for now).
|
- Added route leave protection and panel switch protection.
|
||||||
|
- Updated Panel B template to dynamic matching backend data structure.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,42 @@
|
||||||
- Implemented infinite scroll and filtering by probability.
|
- Implemented infinite scroll and filtering by probability.
|
||||||
- Mapped API response fields to the UI table.
|
- Mapped API response fields to the UI table.
|
||||||
|
|
||||||
### [Task 7] Update Major List API Response Structure
|
- [Result]:
|
||||||
- **Time**: 2026-01-02
|
|
||||||
- **Goal**: Adapt to the updated API response structure and implement dynamic tab counting.
|
|
||||||
- **Scope**:
|
|
||||||
- `src/service/api/major.ts` (Update interface)
|
|
||||||
- `src/pages/simulate.vue` (Update logic)
|
|
||||||
- **Result**:
|
|
||||||
- Updated `UserMajorListResponse` to support `{ list: { items: [], probCount: {} } }` structure.
|
- Updated `UserMajorListResponse` to support `{ list: { items: [], probCount: {} } }` structure.
|
||||||
- Added 'stable' (较稳妥) tab to `simulate.vue`.
|
- Added 'stable' (较稳妥) tab to `simulate.vue`.
|
||||||
- Implemented dynamic update of tab counts using `probCount` from API response.
|
## 2026-01-23
|
||||||
|
|
||||||
|
### [Task 8] Fix TypeScript type error in `simulate.vue`
|
||||||
|
- **Time**: 2026-01-23
|
||||||
|
- **Goal**: Fix type error `Argument of type '"stable"' is not assignable to parameter of type 'TabKey'`.
|
||||||
|
- **Scope**: `src/pages/simulate.vue`
|
||||||
|
- **Result**: Updated `TabKey` definition to include `'stable'` and remove unused `'all'`.
|
||||||
|
|
||||||
|
## 2026-01-24
|
||||||
|
|
||||||
|
### [Task 9] Volunteer Filling Logic Perfection
|
||||||
|
- **Time**: 2026-01-24
|
||||||
|
- **Goal**: Implement real API integration for saving and fetching volunteers, with modification detection.
|
||||||
|
- **Scope**:
|
||||||
|
- `src/service/api/volunteer.ts` (New)
|
||||||
|
- `src/pages/simulate.vue` (Update)
|
||||||
|
- **Result**:
|
||||||
|
- Created `volunteer.ts` with `saveVolunteer` and `getVolunteerDetail`.
|
||||||
|
- Integrated these into `simulate.vue`.
|
||||||
|
- Added `isModified` logic for reordering and deletion.
|
||||||
|
- Added `onBeforeRouteLeave` and `watch(activePanel)` protection for unsaved changes.
|
||||||
|
- Updated Panel B template to use real API data structure.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-01-24
|
||||||
|
|
||||||
|
### [Task 10] Optimize Dark Mode for simulate.vue
|
||||||
|
- **Time**: 2026-01-24
|
||||||
|
- **Goal**: Full support for dark theme using dark: classes for all components in the simulation page.
|
||||||
|
- **Scope**: src/pages/simulate.vue
|
||||||
|
- **Result**:
|
||||||
|
- Applied detailed dark: classes to Sidebar, Panel A/B, and Data Tables.
|
||||||
|
- Optimized color contrast for sticky columns and scrollable areas in dark mode.
|
||||||
|
- Updated all modals (Major list, Switch plan) and Popconfirm UI for dark mode compatibility.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@
|
||||||
- `src/pages/agreement.vue`: User agreement page.
|
- `src/pages/agreement.vue`: User agreement page.
|
||||||
- `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.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
- [x] [Task 6] User Recommended Major List API Integration <!-- id: 32 -->
|
- [x] [Task 6] User Recommended Major List API Integration <!-- id: 32 -->
|
||||||
- [x] Create `src/service/api/major.ts` with types and API method <!-- id: 33 -->
|
- [x] [Task 8] Fix TypeScript type error in `simulate.vue` <!-- id: 36 -->
|
||||||
- [x] Integrate API in `src/pages/simulate.vue` (Panel A) <!-- id: 34 -->
|
- [x] [Task 9] Volunteer Filling Logic Perfection <!-- id: 39 -->
|
||||||
- [x] Update template to display real data <!-- id: 35 -->
|
- [x] Encapsulate volunteer APIs (`src/service/api/volunteer.ts`) <!-- id: 40 -->
|
||||||
|
- [x] Integrate save/detail logic in `simulate.vue` <!-- id: 41 -->
|
||||||
|
- [x] Implement modification detection and leave protection <!-- id: 42 -->
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
// 定义 Emits (声明组件会触发哪些事件)
|
// 定义 Emits (声明组件会触发哪些事件)
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'change', payload: { keyword: string, filters: FilterState }): void
|
(e: 'change', payload: { keyword: string, filters: FilterState }): void
|
||||||
|
|
||||||
// // 当点击下拉菜单里的“确定”时触发,回传筛选对象
|
|
||||||
// (e: 'confirm', filters: FilterState): void;
|
|
||||||
// // 当点击右侧“搜索”按钮时触发,回传搜索关键词
|
|
||||||
// (e: 'search', keyword: string): void;
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
|
|
@ -45,41 +40,10 @@ const filters: FilterConfig[] = [
|
||||||
|
|
||||||
// 按照截图还原的省份数据
|
// 按照截图还原的省份数据
|
||||||
const locations = [
|
const locations = [
|
||||||
'不限',
|
'不限', '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
|
||||||
'北京',
|
'上海', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南', '湖北',
|
||||||
'天津',
|
'湖南', '广东', '广西', '海南', '重庆', '四川', '贵州', '云南', '西藏',
|
||||||
'河北',
|
'陕西', '甘肃', '青海', '宁夏', '新疆', '台湾', '香港', '澳门',
|
||||||
'山西',
|
|
||||||
'内蒙古',
|
|
||||||
'辽宁',
|
|
||||||
'吉林',
|
|
||||||
'黑龙江',
|
|
||||||
'上海',
|
|
||||||
'江苏',
|
|
||||||
'浙江',
|
|
||||||
'安徽',
|
|
||||||
'福建',
|
|
||||||
'江西',
|
|
||||||
'山东',
|
|
||||||
'河南',
|
|
||||||
'湖北',
|
|
||||||
'湖南',
|
|
||||||
'广东',
|
|
||||||
'广西',
|
|
||||||
'海南',
|
|
||||||
'重庆',
|
|
||||||
'四川',
|
|
||||||
'贵州',
|
|
||||||
'云南',
|
|
||||||
'西藏',
|
|
||||||
'陕西',
|
|
||||||
'甘肃',
|
|
||||||
'青海',
|
|
||||||
'宁夏',
|
|
||||||
'新疆',
|
|
||||||
'台湾',
|
|
||||||
'香港',
|
|
||||||
'澳门',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// --- 状态 ---
|
// --- 状态 ---
|
||||||
|
|
@ -95,19 +59,13 @@ const selectedFilters = reactive({
|
||||||
sort: '默认',
|
sort: '默认',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 临时选中的值(用于在点击“确定”之前暂存,如果需要这种逻辑的话,本例为了简单直接修改 selectedFilters)
|
|
||||||
// 如果要严格复刻逻辑,通常点击选项时只变色,点确定才生效。这里简化为点击即选中高亮。
|
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
function getLabel(key: FilterKey) {
|
function getLabel(key: FilterKey) {
|
||||||
// 如果不是默认值,可以显示选中的值,否则显示标题
|
|
||||||
// 这里为了保持截图样式,保持显示标题
|
|
||||||
const map = { location: '位置', type: '类型', major: '专业', sort: '排序' }
|
const map = { location: '位置', type: '类型', major: '专业', sort: '排序' }
|
||||||
return map[key]
|
return map[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
const countSelected = computed(() => {
|
const countSelected = computed(() => {
|
||||||
// 简单计算:如果当前打开的是位置,且选的不是不限,则为1,否则0
|
|
||||||
if (activeFilter.value === 'location') {
|
if (activeFilter.value === 'location') {
|
||||||
return selectedFilters.location !== '不限' ? 1 : 0
|
return selectedFilters.location !== '不限' ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +73,6 @@ const countSelected = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function shouldShowFilter(filter: FilterConfig) {
|
function shouldShowFilter(filter: FilterConfig) {
|
||||||
// 显示所有非排序过滤器,或者当排序过滤器启用时显示排序过滤器
|
|
||||||
return filter.key !== 'sort' || (filter.key === 'sort' && props.sortEnabled)
|
return filter.key !== 'sort' || (filter.key === 'sort' && props.sortEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,15 +103,12 @@ function clearCurrentFilter() {
|
||||||
// 确定按钮
|
// 确定按钮
|
||||||
function confirmSelection() {
|
function confirmSelection() {
|
||||||
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
||||||
// 使用展开运算符 {...} 创建副本,防止父组件修改影响子组件(可选)
|
|
||||||
// emit('confirm', { ...selectedFilters });
|
|
||||||
activeFilter.value = null // 关闭菜单
|
activeFilter.value = null // 关闭菜单
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索按钮
|
// 搜索按钮
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
||||||
// emit('search', searchQuery.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 点击外部关闭逻辑 ---
|
// --- 点击外部关闭逻辑 ---
|
||||||
|
|
@ -174,8 +128,9 @@ onUnmounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 最外层容器,用于定位下拉菜单和监听点击外部事件 -->
|
<!-- 最外层容器 -->
|
||||||
<div ref="containerRef" class="relative mx-auto max-w-5xl w-full select-none text-sm text-gray-600 font-sans">
|
<!-- Update: text-gray-600 -> dark:text-slate-300 -->
|
||||||
|
<div ref="containerRef" class="relative mx-auto w-full max-w-5xl select-none font-sans text-sm text-gray-600 dark:text-slate-300">
|
||||||
<!-- 顶部栏:筛选按钮组 + 搜索框 -->
|
<!-- 顶部栏:筛选按钮组 + 搜索框 -->
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<!-- 左侧:4个下拉筛选器 -->
|
<!-- 左侧:4个下拉筛选器 -->
|
||||||
|
|
@ -187,14 +142,30 @@ onUnmounted(() => {
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
<!-- 筛选器按钮 -->
|
<!-- 筛选器按钮 -->
|
||||||
|
<!-- Update:
|
||||||
|
bg-white -> dark:bg-slate-800
|
||||||
|
border-gray-200 -> dark:border-slate-700
|
||||||
|
ring-blue-200 -> dark:ring-blue-900 (Focus ring 变深)
|
||||||
|
-->
|
||||||
<button
|
<button
|
||||||
class="h-9 w-24 flex items-center justify-between border rounded bg-white px-3 transition-colors hover:border-blue-400"
|
class="flex h-9 w-24 items-center justify-between rounded border px-3 transition-colors hover:border-blue-400"
|
||||||
:class="activeFilter === filter.key ? 'border-blue-500 ring-1 ring-blue-200' : 'border-gray-200'"
|
:class="[
|
||||||
|
activeFilter === filter.key
|
||||||
|
? 'border-blue-500 ring-1 ring-blue-200 dark:ring-blue-900'
|
||||||
|
: 'border-gray-200 dark:border-slate-700',
|
||||||
|
'bg-white dark:bg-slate-800'
|
||||||
|
]"
|
||||||
@click="toggleFilter(filter.key)"
|
@click="toggleFilter(filter.key)"
|
||||||
>
|
>
|
||||||
<span class="truncate">{{ getLabel(filter.key) }}</span>
|
<span class="truncate">{{ getLabel(filter.key) }}</span>
|
||||||
<!-- 下箭头图标 -->
|
<!-- 下箭头图标 -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-gray-400 transition-transform duration-200" :class="{ 'rotate-180': activeFilter === filter.key }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<!-- Update: text-gray-400 -> dark:text-slate-500 -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-3 w-3 text-gray-400 transition-transform duration-200 dark:text-slate-500"
|
||||||
|
:class="{ 'rotate-180': activeFilter === filter.key }"
|
||||||
|
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -205,17 +176,19 @@ onUnmounted(() => {
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
<!-- Update: Icon 颜色 -->
|
||||||
|
<svg class="h-4 w-4 text-gray-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Update: 输入框颜色适配 -->
|
||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
class="h-9 w-50 border border-gray-300 rounded-l bg-white py-2 pl-9 pr-4 text-gray-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 placeholder-gray-400"
|
class="h-9 w-50 rounded-l border border-gray-300 py-2 pl-9 pr-4 text-gray-700 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-200 dark:placeholder-slate-500 dark:focus:ring-blue-900"
|
||||||
placeholder="输入院校名称"
|
placeholder="输入院校名称"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="h-9 rounded-r bg-blue-500 px-6 text-white transition-colors active:bg-blue-700 hover:bg-blue-600 focus:outline-none"
|
class="h-9 rounded-r bg-blue-500 px-6 text-white transition-colors hover:bg-blue-600 focus:outline-none active:bg-blue-700"
|
||||||
@click="handleSearch"
|
@click="handleSearch"
|
||||||
>
|
>
|
||||||
搜索
|
搜索
|
||||||
|
|
@ -224,7 +197,6 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 下拉菜单面板 (绝对定位) -->
|
<!-- 下拉菜单面板 (绝对定位) -->
|
||||||
<!-- 使用 Transition 添加简单的淡入淡出效果 -->
|
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-100 ease-out"
|
enter-active-class="transition duration-100 ease-out"
|
||||||
enter-from-class="transform scale-95 opacity-0"
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
|
@ -233,25 +205,27 @@ onUnmounted(() => {
|
||||||
leave-from-class="transform scale-100 opacity-100"
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<!-- style="min-width: 600px;" -->
|
<!-- Update: 面板背景与边框 -->
|
||||||
<div
|
<div
|
||||||
v-if="activeFilter"
|
v-if="activeFilter"
|
||||||
class="absolute left-0 top-full z-50 mt-2 border border-gray-100 rounded-lg bg-white p-5 shadow-xl"
|
class="absolute left-0 top-full z-50 mt-2 border border-gray-100 bg-white p-5 shadow-xl rounded-lg dark:bg-slate-800 dark:border-slate-700"
|
||||||
>
|
>
|
||||||
<!-- 下拉内容区域 -->
|
<!-- 下拉内容区域 -->
|
||||||
|
|
||||||
<!-- 1. 位置 (Location) 内容 -->
|
<!-- 1. 位置 (Location) 内容 -->
|
||||||
<div v-if="activeFilter === 'location'">
|
<div v-if="activeFilter === 'location'">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<span class="mt-1 shrink-0 text-gray-400 font-medium">院校所属</span>
|
<!-- Update: 标签文字颜色 -->
|
||||||
<div class="xs:grid-cols-3 grid gap-2 lg:grid-cols-6 md:grid-cols-5 sm:grid-cols-3">
|
<span class="mt-1 shrink-0 font-medium text-gray-400 dark:text-slate-500">院校所属</span>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6 xs:grid-cols-3">
|
||||||
|
<!-- Update: 选项 Tag 的选中/未选中态 -->
|
||||||
<button
|
<button
|
||||||
v-for="city in locations"
|
v-for="city in locations"
|
||||||
:key="city"
|
:key="city"
|
||||||
class="rounded px-3 py-1.5 text-center transition-colors hover:text-blue-500"
|
class="rounded px-3 py-1.5 text-center transition-colors hover:text-blue-500 dark:hover:text-blue-400"
|
||||||
:class="selectedFilters.location === city
|
:class="selectedFilters.location === city
|
||||||
? 'bg-blue-50 text-blue-500 font-medium'
|
? 'bg-blue-50 text-blue-500 font-medium dark:bg-blue-900/40 dark:text-blue-400'
|
||||||
: 'text-gray-600 bg-gray-50 hover:bg-gray-100'"
|
: 'text-gray-600 bg-gray-50 hover:bg-gray-100 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'"
|
||||||
@click="selectOption('location', city)"
|
@click="selectOption('location', city)"
|
||||||
>
|
>
|
||||||
{{ city }}
|
{{ city }}
|
||||||
|
|
@ -260,19 +234,21 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. 其他筛选器占位内容 (类型、专业、排序) -->
|
<!-- 2. 其他筛选器占位内容 -->
|
||||||
<div v-else class="p-4 text-center text-gray-400">
|
<div v-else class="p-4 text-center text-gray-400 dark:text-slate-500">
|
||||||
这里是 {{ filters.find(f => f.key === activeFilter)?.label }} 的筛选选项
|
这里是 {{ filters.find(f => f.key === activeFilter)?.label }} 的筛选选项
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部操作按钮 -->
|
<!-- 底部操作按钮 -->
|
||||||
<div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4">
|
<!-- Update: 分割线颜色 -->
|
||||||
<div class="text-gray-500">
|
<div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4 dark:border-slate-700">
|
||||||
已选 <span class="text-blue-500 font-bold">{{ countSelected }}</span> 个
|
<div class="text-gray-500 dark:text-slate-400">
|
||||||
|
已选 <span class="font-bold text-blue-500 dark:text-blue-400">{{ countSelected }}</span> 个
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
|
<!-- Update: 清空按钮 Hover 背景 -->
|
||||||
<button
|
<button
|
||||||
class="border border-blue-500 rounded px-4 py-1.5 text-blue-500 transition-colors hover:bg-blue-50"
|
class="rounded border border-blue-500 px-4 py-1.5 text-blue-500 transition-colors hover:bg-blue-50 dark:text-blue-400 dark:border-blue-500 dark:hover:bg-blue-900/20"
|
||||||
@click="clearCurrentFilter"
|
@click="clearCurrentFilter"
|
||||||
>
|
>
|
||||||
清空已选
|
清空已选
|
||||||
|
|
@ -288,11 +264,4 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/*
|
|
||||||
如果你的 tailwind 配置没有自动移除按钮的默认样式,可能需要以下代码。
|
|
||||||
一般在 tailwind base 中已经处理好了。
|
|
||||||
*/
|
|
||||||
</style>
|
|
||||||
|
|
@ -232,7 +232,7 @@ async function handleSubmit() {
|
||||||
await scoreStore.saveScore(requestData)
|
await scoreStore.saveScore(requestData)
|
||||||
// 刷新本地数据
|
// 刷新本地数据
|
||||||
scoreStore.fetchScore()
|
scoreStore.fetchScore()
|
||||||
message.success('保存成功')
|
message.success('保存成功', 1000)
|
||||||
// 抛出事件
|
// 抛出事件
|
||||||
emit('confirm', formData)
|
emit('confirm', formData)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,13 @@ const isScoreModalOpen = ref(false)
|
||||||
// 处理表单提交的回调
|
// 处理表单提交的回调
|
||||||
function handleScoreFormConfirm(data: ScoreFormData) {
|
function handleScoreFormConfirm(data: ScoreFormData) {
|
||||||
console.warn('接收到表单数据:', data)
|
console.warn('接收到表单数据:', data)
|
||||||
|
isScoreModalOpen.value = false
|
||||||
// 这里可以进行 API 调用
|
// 成绩调整完,判断是不是在模拟填志愿页面,如果是,刷新页面
|
||||||
// api.submit(data).then(...)
|
if (route.path === '/simulate') {
|
||||||
|
setTimeout(()=>{
|
||||||
|
window.location.reload()
|
||||||
|
}, 1300)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function openScoreFormModal() {
|
function openScoreFormModal() {
|
||||||
isScoreModalOpen.value = true
|
isScoreModalOpen.value = true
|
||||||
|
|
|
||||||
|
|
@ -120,16 +120,14 @@ const overlayPositionClass = computed(() => {
|
||||||
|
|
||||||
// 2. 箭头的位置与旋转 (相对于 Popover)
|
// 2. 箭头的位置与旋转 (相对于 Popover)
|
||||||
const arrowPositionClass = computed(() => {
|
const arrowPositionClass = computed(() => {
|
||||||
const base = 'absolute h-3 w-3 bg-white border-slate-200 z-[-1]' // z-index -1 保证在阴影下方,但需要父级不遮挡
|
// Update: 增加了 dark:bg-slate-800 和 dark:border-slate-700
|
||||||
|
const base = 'absolute h-3 w-3 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 z-[-1]'
|
||||||
|
|
||||||
// 基础偏移量
|
// 基础偏移量
|
||||||
// const centerOffset = props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : ''
|
|
||||||
// 如果不是 PointAtCenter,根据 Left/Right 微调箭头位置,避免靠太边
|
|
||||||
const hAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('left') ? 'left-4' : props.placement.includes('right') ? 'right-4' : '')
|
const hAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('left') ? 'left-4' : props.placement.includes('right') ? 'right-4' : '')
|
||||||
const vAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('top') ? 'top-3' : props.placement.includes('bottom') ? 'bottom-3' : '')
|
const vAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('top') ? 'top-3' : props.placement.includes('bottom') ? 'bottom-3' : '')
|
||||||
|
|
||||||
// 12个方向的箭头逻辑
|
// 12个方向的箭头逻辑
|
||||||
// 核心原理:Top 气泡,箭头在 Bottom,边框是 右下 (旋转45度)
|
|
||||||
if (props.placement.startsWith('top')) {
|
if (props.placement.startsWith('top')) {
|
||||||
return `${base} -bottom-1.5 border-b border-r rotate-45 ${props.placement === 'top' || props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : hAlign}`
|
return `${base} -bottom-1.5 border-b border-r rotate-45 ${props.placement === 'top' || props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : hAlign}`
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +143,7 @@ const arrowPositionClass = computed(() => {
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. 过渡动画方向 (Transform Origin)
|
// 3. 过渡动画方向
|
||||||
const transitionOriginClass = computed(() => {
|
const transitionOriginClass = computed(() => {
|
||||||
if (props.placement.startsWith('top'))
|
if (props.placement.startsWith('top'))
|
||||||
return 'origin-bottom'
|
return 'origin-bottom'
|
||||||
|
|
@ -162,9 +160,13 @@ const transitionOriginClass = computed(() => {
|
||||||
const okBtnClass = computed(() => {
|
const okBtnClass = computed(() => {
|
||||||
const base = 'rounded px-3 py-1 text-xs text-white transition-colors'
|
const base = 'rounded px-3 py-1 text-xs text-white transition-colors'
|
||||||
switch (props.okType) {
|
switch (props.okType) {
|
||||||
case 'danger': return `${base} bg-red-500 hover:bg-red-600 border border-red-500`
|
case 'danger':
|
||||||
case 'default': return 'rounded px-3 py-1 text-xs text-slate-600 border border-slate-200 hover:border-blue-400 hover:text-blue-500 transition-colors bg-white'
|
return `${base} bg-red-500 hover:bg-red-600 border border-red-500`
|
||||||
default: return `${base} bg-blue-600 hover:bg-blue-700 border border-blue-600`
|
case 'default':
|
||||||
|
// Update: 默认按钮在暗色模式下背景透明/深色,文字变浅,边框变深
|
||||||
|
return 'rounded px-3 py-1 text-xs text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600 hover:border-blue-400 hover:text-blue-500 transition-colors bg-white dark:bg-slate-800'
|
||||||
|
default:
|
||||||
|
return `${base} bg-blue-600 hover:bg-blue-700 border border-blue-600`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -187,8 +189,13 @@ const okBtnClass = computed(() => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isVisible"
|
v-if="isVisible"
|
||||||
class="absolute z-50 w-45 cursor-default border border-slate-200 rounded-lg bg-white p-3 shadow-xl"
|
class="absolute z-50 w-45 cursor-default border rounded-lg p-3 shadow-xl"
|
||||||
:class="[overlayPositionClass, transitionOriginClass]"
|
:class="[
|
||||||
|
overlayPositionClass,
|
||||||
|
transitionOriginClass,
|
||||||
|
// Update: 容器背景、边框适配 Dark Mode
|
||||||
|
'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700'
|
||||||
|
]"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<!-- Arrow -->
|
<!-- Arrow -->
|
||||||
|
|
@ -198,18 +205,20 @@ const okBtnClass = computed(() => {
|
||||||
<div class="relative z-10 flex items-start gap-3">
|
<div class="relative z-10 flex items-start gap-3">
|
||||||
<div class="mt-0.5 flex-shrink-0">
|
<div class="mt-0.5 flex-shrink-0">
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
|
<!-- Update: Icon 颜色微调适配暗色 -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-500 dark:text-orange-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="break-words text-sm text-slate-800 font-bold">
|
<!-- Update: 文字颜色适配 -->
|
||||||
|
<div class="break-words text-sm font-bold text-slate-800 dark:text-slate-100">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="description || $slots.description" class="mt-1 break-words text-xs text-slate-500">
|
<div v-if="description || $slots.description" class="mt-1 break-words text-xs text-slate-500 dark:text-slate-400">
|
||||||
<slot name="description">
|
<slot name="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|
@ -222,7 +231,12 @@ const okBtnClass = computed(() => {
|
||||||
<slot name="cancelButton">
|
<slot name="cancelButton">
|
||||||
<button
|
<button
|
||||||
v-if="showCancel"
|
v-if="showCancel"
|
||||||
class="rounded px-2 py-1 text-xs text-slate-500 transition-colors hover:bg-slate-100"
|
class="rounded px-2 py-1 text-xs transition-colors"
|
||||||
|
:class="[
|
||||||
|
// Update: 取消按钮颜色适配
|
||||||
|
'text-slate-500 dark:text-slate-400',
|
||||||
|
'hover:bg-slate-100 dark:hover:bg-slate-700'
|
||||||
|
]"
|
||||||
:disabled="cancelButtonProps.disabled"
|
:disabled="cancelButtonProps.disabled"
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
|
|
@ -248,4 +262,4 @@ const okBtnClass = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,9 @@ export interface UserMajorListRequest {
|
||||||
page?: number
|
page?: number
|
||||||
size?: number
|
size?: number
|
||||||
batch?: string
|
batch?: string
|
||||||
|
batch2?: string
|
||||||
probability?: string
|
probability?: string
|
||||||
|
schoolCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryMajorEnroll {
|
export interface HistoryMajorEnroll {
|
||||||
|
|
@ -20,6 +22,9 @@ export interface HistoryMajorEnroll {
|
||||||
export interface MajorItem {
|
export interface MajorItem {
|
||||||
schoolCode: string
|
schoolCode: string
|
||||||
schoolName: string
|
schoolName: string
|
||||||
|
schoolNature: string
|
||||||
|
province: string
|
||||||
|
institutionType: string
|
||||||
majorCode: string
|
majorCode: string
|
||||||
majorName: string
|
majorName: string
|
||||||
majorType: string
|
majorType: string
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import request from '../request'
|
||||||
|
|
||||||
|
export interface VolunteerInfo {
|
||||||
|
id: string
|
||||||
|
volunteerName: string
|
||||||
|
scoreId: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolunteerItem {
|
||||||
|
id: string
|
||||||
|
schoolCode: string
|
||||||
|
schoolName: string
|
||||||
|
majorName: string
|
||||||
|
province: string
|
||||||
|
schoolNature: string
|
||||||
|
planNum: number
|
||||||
|
enrollProbability: number
|
||||||
|
indexs: number
|
||||||
|
majorCode?: string
|
||||||
|
enrollmentCode?: string
|
||||||
|
tuition?: string
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolunteerDetailResponse {
|
||||||
|
volunteer: VolunteerInfo
|
||||||
|
items: Record<string, VolunteerItem[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存志愿明细
|
||||||
|
* @param data schoolCode_majorCode_enrollmentCode 字符串数组
|
||||||
|
*/
|
||||||
|
export function saveVolunteer(data: string[]) {
|
||||||
|
return request.post('/user/volunteer/save', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前志愿单详情
|
||||||
|
*/
|
||||||
|
export function getVolunteerDetail() {
|
||||||
|
return request.get<VolunteerDetailResponse>('/user/volunteer/detail')
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ class Request {
|
||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
||||||
const customConfig = config as CustomRequestConfig
|
const customConfig = config as CustomRequestConfig
|
||||||
|
|
||||||
// NProgress runs by default for visual feedback
|
// NProgress runs by default for visual feedback
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ class Request {
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
customConfig.headers = customConfig.headers || {}
|
customConfig.headers = customConfig.headers || {}
|
||||||
|
|
||||||
if (userStore.token) {
|
if (userStore.token) {
|
||||||
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
|
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +64,9 @@ class Request {
|
||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse<ApiResponse>) => {
|
(response: AxiosResponse<ApiResponse>) => {
|
||||||
const config = response.config as CustomRequestConfig
|
const config = response.config as CustomRequestConfig
|
||||||
|
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
|
|
||||||
if (config.showLoading) {
|
if (config.showLoading) {
|
||||||
loading.hide()
|
loading.hide()
|
||||||
}
|
}
|
||||||
|
|
@ -83,51 +83,48 @@ class Request {
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const config = error.config as CustomRequestConfig
|
const config = error.config as CustomRequestConfig
|
||||||
|
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
|
|
||||||
if (config?.showLoading) {
|
if (config?.showLoading) {
|
||||||
loading.hide()
|
loading.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = 'Network Error'
|
let msg = '网络错误'
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// 优先使用后端返回的错误信息
|
const backendMsg = error.response.data?.message
|
||||||
const backendMsg = error.response.data?.message
|
|
||||||
|
switch (error.response.status) {
|
||||||
switch (error.response.status) {
|
case 401:
|
||||||
case 401:
|
msg = backendMsg || '未授权,请重新登录'
|
||||||
msg = backendMsg || 'Unauthorized'
|
const userStore = useUserStore()
|
||||||
const userStore = useUserStore()
|
userStore.clearToken()
|
||||||
userStore.clearToken()
|
userStore.clearUserInfo()
|
||||||
userStore.clearUserInfo()
|
window.location.href = '/'
|
||||||
window.location.href= '/'
|
break
|
||||||
// userStore.logout()
|
case 403:
|
||||||
// router push login?
|
msg = backendMsg || '拒绝访问'
|
||||||
break
|
break
|
||||||
case 403:
|
case 404:
|
||||||
msg = backendMsg || 'Forbidden'
|
msg = backendMsg || '请求地址出错'
|
||||||
break
|
break
|
||||||
case 404:
|
case 429:
|
||||||
msg = backendMsg || 'Not Found'
|
msg = backendMsg || '请求过于频繁'
|
||||||
break
|
break
|
||||||
case 429:
|
case 500:
|
||||||
msg = backendMsg || 'Too Many Requests'
|
msg = backendMsg || '服务器内部错误'
|
||||||
break
|
break
|
||||||
case 500:
|
default:
|
||||||
msg = backendMsg || 'Internal Server Error'
|
msg = backendMsg || `请求错误: ${error.response.status}`
|
||||||
break
|
}
|
||||||
default:
|
|
||||||
msg = backendMsg || `Error: ${error.response.status}`
|
|
||||||
}
|
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
msg = 'No response from server'
|
msg = '服务器未响应'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config?.showError !== false) {
|
if (config?.showError !== false) {
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,12 @@
|
||||||
- Added logic to update `tabs[].count` using `res.list.probCount`.
|
- Added logic to update `tabs[].count` using `res.list.probCount`.
|
||||||
- Added 'stable' case to filter logic.
|
- Added 'stable' case to filter logic.
|
||||||
- **Execution Result**: The application now correctly handles the new API structure and dynamically updates the tab counts based on backend data.
|
- **Execution Result**: The application now correctly handles the new API structure and dynamically updates the tab counts based on backend data.
|
||||||
|
|
||||||
|
## Session 2026-01-23 (1)
|
||||||
|
|
||||||
|
- **Execution Reason**: User inquired about my capabilities.
|
||||||
|
- **Execution Process**:
|
||||||
|
1. Performed a full project audit by reading `project_index.md`, `project_codebase.md`, `project_task.md`, and `task_detail.md`.
|
||||||
|
2. Confirmed the completion of "User Recommended Major List API Integration" and other core modules (Auth, Score).
|
||||||
|
3. Introduced my identity as a Senior Full-stack Engineer & Architect and clarified the document-driven development workflow.
|
||||||
|
- **Execution Result**: Established a common understanding of my capabilities and the current project state. Ready for new requirements.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue