Compare commits
No commits in common. "7f27542057999b833eb1685a066a07d3ca88d035" and "f1d30a2916c1e740d2a445bd4930c311094c5fbd" have entirely different histories.
7f27542057
...
f1d30a2916
|
|
@ -4,7 +4,7 @@
|
||||||
VITE_API_BASE_URL=/api
|
VITE_API_BASE_URL=/api
|
||||||
|
|
||||||
# Proxy Target (Where the /api requests are forwarded to) 改完后需要重启
|
# Proxy Target (Where the /api requests are forwarded to) 改完后需要重启
|
||||||
VITE_API_PROXY_TARGET=http://localhost:8081
|
VITE_API_PROXY_TARGET=http://localhost:8080
|
||||||
|
|
||||||
# Other development-specific variables
|
# Other development-specific variables
|
||||||
VITE_APP_TITLE=My App (Development)
|
VITE_APP_TITLE=My App (Development)
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
---
|
|
||||||
title: 艺考招生管理系统 API
|
|
||||||
language_tabs:
|
|
||||||
- shell: Shell
|
|
||||||
- http: HTTP
|
|
||||||
- javascript: JavaScript
|
|
||||||
- ruby: Ruby
|
|
||||||
- python: Python
|
|
||||||
- php: PHP
|
|
||||||
- java: Java
|
|
||||||
- go: Go
|
|
||||||
toc_footers: []
|
|
||||||
includes: []
|
|
||||||
search: true
|
|
||||||
code_clipboard: true
|
|
||||||
highlight_theme: darkula
|
|
||||||
headingLevel: 2
|
|
||||||
generator: "@tarslib/widdershins v4.0.30"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 艺考招生管理系统 API
|
|
||||||
|
|
||||||
提供用户认证、院校专业、历年招生、计算专业的管理接口
|
|
||||||
|
|
||||||
Base URLs:
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
|
|
||||||
# 用户专业
|
|
||||||
|
|
||||||
## GET 获取当前用户可检索列表
|
|
||||||
|
|
||||||
GET /user/major/list
|
|
||||||
|
|
||||||
### 请求参数
|
|
||||||
|
|
||||||
|名称|位置|类型|必选|说明|
|
|
||||||
|---|---|---|---|---|
|
|
||||||
|page|query|integer| 否 |页码|
|
|
||||||
|size|query|integer| 否 |每页数量|
|
|
||||||
|batch|query|string| 否 |批次(本科提前批/本科A段/本科B段/本科/高职高专)|
|
|
||||||
|probability|query|string| 否 |录取概率类型(难录取/可冲击/较稳妥/可保底)|
|
|
||||||
|
|
||||||
> 返回示例
|
|
||||||
|
|
||||||
> 200 Response
|
|
||||||
|
|
||||||
```
|
|
||||||
{"code":200,"message":"success","data":{"list":[{"schoolCode":"1495","schoolName":"哈尔滨工业大学","majorCode":"130508","majorName":"数字媒体艺术","majorType":"美术与设计类","majorTypeChild":"","planNum":2,"mainSubjects":"","limitation":"","chineseScoreLimitation":0,"englishScoreLimitation":0,"culturalScoreLimitation":0,"professionalScoreLimitation":0,"enrollmentCode":"02","tuition":"8000元/年","detail":"","category":"理科","batch":"提前批","rulesEnrollProbability":"专过文排","probabilityOperator":"文*1+专*0","rulesEnrollProbabilitySx":"专过文排","kslx":"","state":"1","historyMajorEnrollMap":{"2024":{"year":"2024","enrollmentCode":"03","enrollmentCount":0,"rulesEnrollProbability":"专过文排","probabilityOperator":"文*1+专*0","admissionLine":546.225,"controlLine":300}},"enrollProbability":64.5751,"studentScore":0}],"total":18,"page":1,"size":10}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 返回结果
|
|
||||||
|
|
||||||
|状态码|状态码含义|说明|数据模型|
|
|
||||||
|---|---|---|---|
|
|
||||||
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|Inline|
|
|
||||||
|
|
||||||
### 返回数据结构
|
|
||||||
|
|
||||||
状态码 **200**
|
|
||||||
|
|
||||||
|名称|类型|必选|约束|中文名|说明|
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
|» code|integer|true|none||none|
|
|
||||||
|» message|string|true|none||none|
|
|
||||||
|» data|object|true|none||none|
|
|
||||||
|»» list|[object]|true|none||none|
|
|
||||||
|»»» schoolCode|string|true|none|院校代码|none|
|
|
||||||
|»»» schoolName|string|true|none|院校名称|none|
|
|
||||||
|»»» majorCode|string|true|none||none|
|
|
||||||
|»»» majorName|string|true|none|专业名称|none|
|
|
||||||
|»»» majorType|string|true|none||none|
|
|
||||||
|»»» majorTypeChild|string|true|none||none|
|
|
||||||
|»»» planNum|integer|true|none|计划人数|none|
|
|
||||||
|»»» mainSubjects|string|true|none||none|
|
|
||||||
|»»» limitation|string|true|none||none|
|
|
||||||
|»»» chineseScoreLimitation|integer|true|none||none|
|
|
||||||
|»»» englishScoreLimitation|integer|true|none||none|
|
|
||||||
|»»» culturalScoreLimitation|integer|true|none||none|
|
|
||||||
|»»» professionalScoreLimitation|integer|true|none||none|
|
|
||||||
|»»» enrollmentCode|string|true|none|招录专业代码|none|
|
|
||||||
|»»» tuition|string|true|none|学费|none|
|
|
||||||
|»»» detail|string|true|none|详情|none|
|
|
||||||
|»»» category|string|true|none|文理分科|none|
|
|
||||||
|»»» batch|string|true|none|批次|none|
|
|
||||||
|»»» rulesEnrollProbability|string|true|none|录取方式|none|
|
|
||||||
|»»» probabilityOperator|string|true|none|计算规则|none|
|
|
||||||
|»»» rulesEnrollProbabilitySx|string|true|none|录取方式简写|none|
|
|
||||||
|»»» kslx|string|true|none||none|
|
|
||||||
|»»» state|string|true|none||none|
|
|
||||||
|»»» historyMajorEnrollMap|object|true|none|历年信息|none|
|
|
||||||
|»»»» 2024|object|true|none|年份|none|
|
|
||||||
|»»»»» year|string|true|none|年份|none|
|
|
||||||
|»»»»» enrollmentCode|string|true|none|招录专业代码|none|
|
|
||||||
|»»»»» enrollmentCount|integer|true|none|录取人数|none|
|
|
||||||
|»»»»» rulesEnrollProbability|string|true|none|录取方式|none|
|
|
||||||
|»»»»» probabilityOperator|string|true|none|计算规则|none|
|
|
||||||
|»»»»» admissionLine|number|true|none|录取线|none|
|
|
||||||
|»»»»» controlLine|integer|true|none|省控线|none|
|
|
||||||
|»»»» 2025|object|true|none||none|
|
|
||||||
|»»»»» year|string|true|none||none|
|
|
||||||
|»»»»» enrollmentCode|string|true|none||none|
|
|
||||||
|»»»»» enrollmentCount|integer|true|none||none|
|
|
||||||
|»»»»» rulesEnrollProbability|string|true|none||none|
|
|
||||||
|»»»»» probabilityOperator|string|true|none||none|
|
|
||||||
|»»»»» admissionLine|integer|true|none||none|
|
|
||||||
|»»»»» controlLine|number|true|none||none|
|
|
||||||
|»»» enrollProbability|number|true|none|通过率|none|
|
|
||||||
|»»» studentScore|integer|true|none|折合分|none|
|
|
||||||
|»» total|integer|true|none||none|
|
|
||||||
|»» page|integer|true|none||none|
|
|
||||||
|»» size|integer|true|none||none|
|
|
||||||
|
|
||||||
# 数据模型
|
|
||||||
|
|
||||||
|
|
@ -1,13 +1,39 @@
|
||||||
|
# Project Codebase
|
||||||
|
|
||||||
|
## Function Overview
|
||||||
|
|
||||||
|
### `src/utils/message.ts`
|
||||||
|
- **Purpose**: Provides a global interface for showing toast messages.
|
||||||
|
- **Methods**: `success(msg, duration, position)`, `error(msg, duration, position)`, `warning(msg, duration, position)`, `info(msg, duration, position)`.
|
||||||
|
- **Implementation**: Programmatically mounts `WMessage.vue`.
|
||||||
|
- **New Feature**: Supports `position` argument: `'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right'`.
|
||||||
|
|
||||||
|
### `src/utils/loading.ts`
|
||||||
|
- **Purpose**: Provides a global interface for showing a full-screen blocking loading overlay.
|
||||||
|
- **Methods**: `show()`, `hide()`.
|
||||||
|
- **Implementation**: Programmatically mounts `WLoading.vue` with a reference counter to handle concurrent requests.
|
||||||
|
|
||||||
|
### `src/service/request/index.ts`
|
||||||
|
- **Purpose**: Encapsulates Axios for API requests.
|
||||||
|
- **Features**:
|
||||||
|
- Request Interceptor:
|
||||||
|
- Adds `Authorization: Bearer <token>`.
|
||||||
|
- Adds `X-App-Sign` and `X-App-Timestamp` headers.
|
||||||
|
- Starts `NProgress`.
|
||||||
|
- **New**: Shows blocking loading overlay if `config.showLoading` is true.
|
||||||
|
- Response Interceptor: Unwraps `data`; handles global errors (401, 403, 500); prioritizes backend error messages; stops `NProgress`.
|
||||||
|
- **New**: Hides blocking loading overlay if `config.showLoading` is true.
|
||||||
|
- Config: `showLoading`, `showError`.
|
||||||
|
|
||||||
|
### `src/service/api/auth.ts`
|
||||||
|
- **Purpose**: API definitions for authentication.
|
||||||
|
- **Methods**: `login`, `logout`, `getUserInfo`.
|
||||||
|
|
||||||
### `src/service/api/score.ts`
|
### `src/service/api/score.ts`
|
||||||
- **Purpose**: API definitions for score management.
|
- **Purpose**: API definitions for score management.
|
||||||
- **Methods**: `getScore`, `saveScore`.
|
- **Methods**: `getScore`, `saveScore`.
|
||||||
- **Types**: `SaveScoreRequest`, `ScoreInfo`.
|
- **Types**: `SaveScoreRequest`, `ScoreInfo`.
|
||||||
|
|
||||||
### `src/service/api/major.ts`
|
|
||||||
- **Purpose**: API definitions for user major recommendations.
|
|
||||||
- **Methods**: `getUserMajorList`.
|
|
||||||
- **Types**: `UserMajorListRequest`, `UserMajorListResponse`, `MajorItem`.
|
|
||||||
|
|
||||||
### `src/stores/user.ts`
|
### `src/stores/user.ts`
|
||||||
- **Purpose**: Manages user session state.
|
- **Purpose**: Manages user session state.
|
||||||
- **State**: `token`, `userInfo`.
|
- **State**: `token`, `userInfo`.
|
||||||
|
|
@ -36,9 +62,3 @@
|
||||||
### `src/pages/privacy-policy.vue`
|
### `src/pages/privacy-policy.vue`
|
||||||
- **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/pages/simulate.vue`
|
|
||||||
- **Purpose**: Volunteer simulation page.
|
|
||||||
- **Features**:
|
|
||||||
- Panel A: Displays recommended majors list fetched from API (`/user/major/list`). Supports infinite scroll and filtering by probability.
|
|
||||||
- Panel B: Displays user's selected volunteers (Mock data for now).
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,63 @@
|
||||||
|
# Project Doing
|
||||||
|
|
||||||
## 2026-01-02
|
## 2025-12-18
|
||||||
|
|
||||||
### [Task 6] User Recommended Major List API Integration
|
### [Task 1] Global Message Component, API Encapsulation, and Login/Logout Integration
|
||||||
- **Time**: 2026-01-02
|
- **Time**: 2025-12-18
|
||||||
- **Goal**: Integrate the user recommended major list API into the simulation page.
|
- **Goal**: Implement global message, encapsulate axios, and integrate login.
|
||||||
- **Scope**:
|
- **Scope**:
|
||||||
- `src/service/api/major.ts` (Create)
|
- `src/components/ui/WMessage.vue` (Review/Update)
|
||||||
- `src/pages/simulate.vue` (Update)
|
- `package.json` (Add axios)
|
||||||
- **Result**:
|
- `src/service/request/index.ts` (Create)
|
||||||
- Created `src/service/api/major.ts` encapsulating `/user/major/list` API.
|
- `src/service/api/auth.ts` (Create)
|
||||||
- Updated `src/pages/simulate.vue` to fetch and display data in Panel A using the new API.
|
- `src/stores/user.ts` (Update)
|
||||||
- Implemented infinite scroll and filtering by probability.
|
- `src/components/TheNavigation.vue` (Update)
|
||||||
- Mapped API response fields to the UI table.
|
|
||||||
|
|
||||||
### [Task 7] Update Major List API Response Structure
|
### [Task 2] Score API Encapsulation and Business Integration
|
||||||
- **Time**: 2026-01-02
|
- **Time**: 2025-12-18
|
||||||
- **Goal**: Adapt to the updated API response structure and implement dynamic tab counting.
|
- **Goal**: Encapsulate Score API and integrate into components.
|
||||||
- **Scope**:
|
- **Scope**:
|
||||||
- `src/service/api/major.ts` (Update interface)
|
- `src/service/api/score.ts` (Create)
|
||||||
- `src/pages/simulate.vue` (Update logic)
|
- `src/stores/score.ts` (Create)
|
||||||
- **Result**:
|
- `src/components/TheNavigation.vue` (Update)
|
||||||
- Updated `UserMajorListResponse` to support `{ list: { items: [], probCount: {} } }` structure.
|
- `src/components/ScoreForm.vue` (Update)
|
||||||
- Added 'stable' (较稳妥) tab to `simulate.vue`.
|
- `project_task.md` (Update)
|
||||||
- Implemented dynamic update of tab counts using `probCount` from API response.
|
|
||||||
|
### [Task 2] Verification and Documentation
|
||||||
|
- **Time**: 2025-12-18
|
||||||
|
- **Goal**: Verify score integration and update documentation.
|
||||||
|
- **Scope**:
|
||||||
|
- `project_task.md` (Update status)
|
||||||
|
|
||||||
|
### [Task 2] Fix Score Refresh Issue
|
||||||
|
- **Time**: 2025-12-18
|
||||||
|
- **Goal**: Ensure score data is fetched when ScoreForm mounts if store is empty.
|
||||||
|
- **Scope**:
|
||||||
|
- `src/components/ScoreForm.vue` (Update onMounted)
|
||||||
|
|
||||||
|
### [Task 3] Enhance WMessage Component
|
||||||
|
- **Time**: 2025-12-18
|
||||||
|
- **Goal**: Add position configuration support (top/bottom/left/right/center).
|
||||||
|
- **Scope**:
|
||||||
|
- `src/utils/message.ts` (Update logic to support multiple containers)
|
||||||
|
- `src/components/ui/WMessage.vue` (No changes needed, styles handled by container)
|
||||||
|
|
||||||
|
### [Task 4] Implement Fullscreen Loading
|
||||||
|
- **Time**: 2025-12-18
|
||||||
|
- **Goal**: Add blocking loading overlay controlled by request config.
|
||||||
|
- **Scope**:
|
||||||
|
- `src/components/ui/WLoading.vue` (Create)
|
||||||
|
- `src/utils/loading.ts` (Create)
|
||||||
|
- `src/service/request/index.ts` (Update interceptors)
|
||||||
|
|
||||||
|
## 2025-12-27
|
||||||
|
|
||||||
|
### [Task 5] Improve Sidebar and Add Legal Pages
|
||||||
|
- **Time**: 2025-12-27
|
||||||
|
- **Goal**: Enhance user experience for non-logged-in users and ensure legal compliance.
|
||||||
|
- **Scope**:
|
||||||
|
- `src/pages/index.vue` (Redesign empty sidebar)
|
||||||
|
- `src/pages/agreement.vue` (Create user agreement page)
|
||||||
|
- `src/pages/privacy-policy.vue` (Create privacy policy page)
|
||||||
|
- `src/pages/agreement.vue` (Update company info)
|
||||||
|
- `src/pages/privacy-policy.vue` (Update company info and contact details)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
- `src/service/request/index.ts`: Axios wrapper.
|
- `src/service/request/index.ts`: Axios wrapper.
|
||||||
- `src/service/api/auth.ts`: Authentication API.
|
- `src/service/api/auth.ts`: Authentication API.
|
||||||
- `src/service/api/score.ts`: Score API.
|
- `src/service/api/score.ts`: Score API.
|
||||||
- `src/service/api/major.ts`: User major recommendation API.
|
|
||||||
- `src/stores/user.ts`: User Pinia store.
|
- `src/stores/user.ts`: User Pinia store.
|
||||||
- `src/stores/score.ts`: Score Pinia store.
|
- `src/stores/score.ts`: Score Pinia store.
|
||||||
- `src/components/ui/WMessage.vue`: Message component UI.
|
- `src/components/ui/WMessage.vue`: Message component UI.
|
||||||
|
|
@ -15,4 +14,3 @@
|
||||||
- `src/pages/index.vue`: Home page with dashboard/welcome sidebar.
|
- `src/pages/index.vue`: Home page with dashboard/welcome sidebar.
|
||||||
- `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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,38 @@
|
||||||
|
# Project Tasks
|
||||||
|
|
||||||
- [x] [Task 6] User Recommended Major List API Integration <!-- id: 32 -->
|
- [x] [Task 1] Global Message Component, API Encapsulation, and Login/Logout Integration <!-- id: 0 -->
|
||||||
- [x] Create `src/service/api/major.ts` with types and API method <!-- id: 33 -->
|
- [x] Check/Implement Global Message Component <!-- id: 1 -->
|
||||||
- [x] Integrate API in `src/pages/simulate.vue` (Panel A) <!-- id: 34 -->
|
- [x] Install axios <!-- id: 2 -->
|
||||||
- [x] Update template to display real data <!-- id: 35 -->
|
- [x] Create `src/service/request/index.ts` <!-- id: 3 -->
|
||||||
|
- [x] Create `src/service/api/auth.ts` <!-- id: 4 -->
|
||||||
|
- [x] Update `src/stores/user.ts` (Pinia + Persistence) <!-- id: 5 -->
|
||||||
|
- [x] Integrate Login/Logout in `TheNavigation.vue` <!-- id: 6 -->
|
||||||
|
- [x] Install crypto-js and types <!-- id: 7 -->
|
||||||
|
- [x] Configure API secret in environment variables <!-- id: 8 -->
|
||||||
|
- [x] Update Axios request interceptor with signature logic <!-- id: 9 -->
|
||||||
|
- [x] Fix CORS by updating VITE_API_BASE_URL <!-- id: 10 -->
|
||||||
|
- [x] Refactor vite.config.ts to use loadEnv for dynamic proxy configuration <!-- id: 11 -->
|
||||||
|
- [x] Update .env.development with VITE_API_PROXY_TARGET <!-- id: 12 -->
|
||||||
|
- [x] Prioritize backend error message in Axios response interceptor <!-- id: 13 -->
|
||||||
|
- [x] Check TheNavigation.vue for reactive user state usage <!-- id: 14 -->
|
||||||
|
- [x] Update TheNavigation.vue template to toggle Login/User info based on store state <!-- id: 15 -->
|
||||||
|
|
||||||
|
- [x] [Task 2] Score API Encapsulation and Business Integration <!-- id: 16 -->
|
||||||
|
- [x] Create `src/service/api/score.ts` with types <!-- id: 17 -->
|
||||||
|
- [x] Create `src/stores/score.ts` <!-- id: 18 -->
|
||||||
|
- [x] Integrate Get Score in `TheNavigation.vue` <!-- id: 19 -->
|
||||||
|
- [x] Integrate Save Score in `ScoreForm.vue` <!-- id: 20 -->
|
||||||
|
|
||||||
|
- [x] [Task 3] Enhance WMessage Component <!-- id: 21 -->
|
||||||
|
- [x] Add position support to `src/utils/message.ts` (top/bottom/left/right/center) <!-- id: 22 -->
|
||||||
|
|
||||||
|
- [x] [Task 4] Implement Fullscreen Loading <!-- id: 23 -->
|
||||||
|
- [x] Create `src/components/ui/WLoading.vue` <!-- id: 24 -->
|
||||||
|
- [x] Create `src/utils/loading.ts` logic <!-- id: 25 -->
|
||||||
|
- [x] Update `src/service/request/index.ts` to handle `showLoading` parameter <!-- id: 26 -->
|
||||||
|
|
||||||
|
- [x] [Task 5] Improve Sidebar and Add Legal Pages <!-- id: 27 -->
|
||||||
|
- [x] Improve empty sidebar layout in `src/pages/index.vue` <!-- id: 28 -->
|
||||||
|
- [x] Create `src/pages/agreement.vue` with content <!-- id: 29 -->
|
||||||
|
- [x] Create `src/pages/privacy-policy.vue` with content <!-- id: 30 -->
|
||||||
|
- [x] Update legal pages with company info from `Task3.md` <!-- id: 31 -->
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FilterState } from '~/components/FilterBar.vue'
|
import type { FilterState } from '~/components/FilterBar.vue'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { getUserMajorList, type MajorItem } from '~/service/api/major'
|
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | '本科' | '专科'
|
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | '本科' | '专科'
|
||||||
|
|
@ -14,8 +13,7 @@ interface YearData {
|
||||||
method?: string
|
method?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅用于 Panel B 的本地模拟数据接口
|
interface School {
|
||||||
interface VolunteerSchool {
|
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
|
@ -78,7 +76,7 @@ const activePanel = ref<PanelType>('market') // 当前激活的面板
|
||||||
|
|
||||||
// 模拟“我的志愿”数据 (为了演示Panel B,初始化一些数据)
|
// 模拟“我的志愿”数据 (为了演示Panel B,初始化一些数据)
|
||||||
// 在实际业务中,这应该由 selectedMajorCodes 对应的完整数据填充
|
// 在实际业务中,这应该由 selectedMajorCodes 对应的完整数据填充
|
||||||
const myVolunteers = ref<VolunteerSchool[]>([])
|
const myVolunteers = ref<School[]>([])
|
||||||
|
|
||||||
const volunteerCurrentTab = ref<TabKey>('本科')
|
const volunteerCurrentTab = ref<TabKey>('本科')
|
||||||
const volunteerTabs = [
|
const volunteerTabs = [
|
||||||
|
|
@ -86,27 +84,22 @@ const volunteerTabs = [
|
||||||
{ key: '专科', label: '专科', count: 2, max: 64 },
|
{ key: '专科', label: '专科', count: 2, max: 64 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const currentTab = ref<TabKey>('stable')
|
const currentTab = ref<TabKey>('all')
|
||||||
const tabs = ref([
|
const tabs = [
|
||||||
{ key: 'safe', label: '可保底', count: 1 },
|
{ key: 'all', label: '全部', count: 157 },
|
||||||
{ key: 'stable', label: '较稳妥', count: 11 },
|
|
||||||
{ key: 'risky', label: '可冲击', count: 11 },
|
|
||||||
{ key: 'hard', label: '难录取', count: 145 },
|
{ key: 'hard', label: '难录取', count: 145 },
|
||||||
])
|
{ key: 'risky', label: '可冲击', count: 11 },
|
||||||
const oldYears = ref(['2025','2024','2023'])
|
{ key: 'safe', label: '可保底', count: 1 },
|
||||||
|
]
|
||||||
|
|
||||||
// Panel A 列表数据
|
const schools = ref<School[]>([]) // 列表数据
|
||||||
const schools = ref<MajorItem[]>([])
|
|
||||||
const page = ref(1)
|
|
||||||
const size = ref(10)
|
|
||||||
const total = ref(0)
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isFinished = ref(false)
|
const isFinished = ref(false)
|
||||||
const scrollContainer = ref<HTMLElement | null>(null)
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// 弹窗相关状态
|
// 弹窗相关状态
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const currentSchool = ref<MajorItem | null>(null)
|
const currentSchool = ref<School | null>(null)
|
||||||
const modalLoading = ref(false)
|
const modalLoading = ref(false)
|
||||||
const modalMajors = ref<MajorDetail[]>([])
|
const modalMajors = ref<MajorDetail[]>([])
|
||||||
|
|
||||||
|
|
@ -115,86 +108,52 @@ const selectedMajorCodes = ref<string[]>([]) // 存储已选的专业Code,数
|
||||||
const showSaveConfirm = ref(false) // 控制气泡确认框显示
|
const showSaveConfirm = ref(false) // 控制气泡确认框显示
|
||||||
const isSaving = ref(false) // 保存接口Loading状态
|
const isSaving = ref(false) // 保存接口Loading状态
|
||||||
|
|
||||||
// --- 辅助函数 ---
|
// --- 模拟数据生成器 ---
|
||||||
function getProbabilityLabel(prob: number): string {
|
function generateMockData(count: number, startId: number) {
|
||||||
if (prob >= 93) return '保'
|
const result: School[] = []
|
||||||
if (prob >= 73) return '稳'
|
for (let i = 0; i < count; i++) {
|
||||||
if (prob >= 60) return '冲'
|
const id = startId + i
|
||||||
return '难'
|
result.push({
|
||||||
}
|
id,
|
||||||
|
name: `模拟院校名称 ${id}号`,
|
||||||
function getStatusColor(status: string) {
|
tags: i % 2 === 0 ? ['湖北', '公办', '师范'] : ['陕西', '民办', '综合'],
|
||||||
switch (status) {
|
code: (5000 + id).toString(),
|
||||||
case '保':
|
probability: (Math.random() * 100).toFixed(2),
|
||||||
return 'border-green-500 text-green-600 bg-green-50'
|
statusLabel: Math.random() > 0.5 ? '保' : '冲',
|
||||||
case '稳':
|
calcScore: 450 + Math.floor(Math.random() * 50),
|
||||||
return 'border-blue-500 text-blue-600 bg-blue-50'
|
diffScore: 20 + Math.floor(Math.random() * 30),
|
||||||
case '冲':
|
majorName: i % 3 === 0 ? '音乐学 (师范)' : '计算机科学与技术',
|
||||||
return 'border-orange-500 text-orange-500 bg-orange-50'
|
requirements: '再选: 不限',
|
||||||
default:
|
tuition: '5000元/年',
|
||||||
return 'border-gray-400 text-gray-400 bg-gray-50'
|
majorCode: `0${i % 9}`,
|
||||||
|
planCount: 5 + Math.floor(Math.random() * 10),
|
||||||
|
history: {
|
||||||
|
2024: { count: 8, minScore: 450.8, diff: 64.2, method: '文*0.6+专*1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 数据加载逻辑 ---
|
// --- 无限滚动逻辑 ---
|
||||||
async function loadMore(reset = false) {
|
async function loadMore() {
|
||||||
if (isLoading.value) return
|
if (isLoading.value || isFinished.value)
|
||||||
if (!reset && isFinished.value) return
|
return
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
if (reset) {
|
|
||||||
page.value = 1
|
|
||||||
schools.value = []
|
|
||||||
isFinished.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// 模拟网络延迟
|
||||||
// 映射 Tab 到 API 参数
|
setTimeout(() => {
|
||||||
let probability: string | undefined
|
const newItems = generateMockData(5, schools.value.length + 1)
|
||||||
if (currentTab.value === 'hard') probability = '难录取'
|
schools.value = [...schools.value, ...newItems]
|
||||||
if (currentTab.value === 'risky') probability = '可冲击'
|
|
||||||
if (currentTab.value === 'stable') probability = '较稳妥'
|
|
||||||
if (currentTab.value === 'safe') probability = '可保底'
|
|
||||||
|
|
||||||
const res = await getUserMajorList({
|
|
||||||
page: page.value,
|
|
||||||
size: size.value,
|
|
||||||
probability
|
|
||||||
})
|
|
||||||
console.warn(res)
|
|
||||||
if (res && res.list && res.list.items) {
|
|
||||||
schools.value.push(...res.list.items)
|
|
||||||
total.value = res.total
|
|
||||||
|
|
||||||
// 更新 Tabs 计数
|
|
||||||
if (res.list.probCount) {
|
|
||||||
tabs.value.forEach(tab => {
|
|
||||||
if (tab.key && res.list.probCount[tab.key] !== undefined) {
|
|
||||||
tab.count = res.list.probCount[tab.key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schools.value.length >= res.total || res.list.items.length < size.value) {
|
|
||||||
isFinished.value = true
|
|
||||||
} else {
|
|
||||||
page.value++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isFinished.value = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load major list:', error)
|
|
||||||
isFinished.value = true
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听 Tab 切换,重新加载
|
// 假设总数达到 50 就停止加载
|
||||||
watch(currentTab, () => {
|
if (schools.value.length >= 25) {
|
||||||
loadMore(true)
|
isFinished.value = true
|
||||||
})
|
}
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (!scrollContainer.value)
|
if (!scrollContainer.value)
|
||||||
|
|
@ -207,7 +166,7 @@ function handleScroll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 弹窗逻辑 ---
|
// --- 弹窗逻辑 ---
|
||||||
async function openMajorModal(school: MajorItem) {
|
async function openMajorModal(school: School) {
|
||||||
currentSchool.value = school
|
currentSchool.value = school
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
modalLoading.value = true
|
modalLoading.value = true
|
||||||
|
|
@ -217,9 +176,7 @@ async function openMajorModal(school: MajorItem) {
|
||||||
showSaveConfirm.value = false
|
showSaveConfirm.value = false
|
||||||
|
|
||||||
// 模拟获取该学校其他专业(更丰富的数据)
|
// 模拟获取该学校其他专业(更丰富的数据)
|
||||||
// TODO: 这里应该调用另外一个 API 获取详情
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Mock data for now
|
|
||||||
modalMajors.value = [
|
modalMajors.value = [
|
||||||
{
|
{
|
||||||
code: '02',
|
code: '02',
|
||||||
|
|
@ -231,6 +188,46 @@ async function openMajorModal(school: MajorItem) {
|
||||||
req: '历史+不限',
|
req: '历史+不限',
|
||||||
tuition: '4000/年',
|
tuition: '4000/年',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
code: '03',
|
||||||
|
name: '学前教育',
|
||||||
|
prob: 92,
|
||||||
|
score: 455,
|
||||||
|
diff: 40,
|
||||||
|
plan: 8,
|
||||||
|
req: '历史/政治',
|
||||||
|
tuition: '4000/年',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '04',
|
||||||
|
name: '英语 (师范)',
|
||||||
|
prob: 75,
|
||||||
|
score: 470,
|
||||||
|
diff: 55,
|
||||||
|
plan: 5,
|
||||||
|
req: '不限',
|
||||||
|
tuition: '5000/年',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '05',
|
||||||
|
name: '网络工程',
|
||||||
|
prob: 60,
|
||||||
|
score: 440,
|
||||||
|
diff: 25,
|
||||||
|
plan: 20,
|
||||||
|
req: '物理+化学',
|
||||||
|
tuition: '5500/年',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '06',
|
||||||
|
name: '数据科学',
|
||||||
|
prob: 45,
|
||||||
|
score: 480,
|
||||||
|
diff: 65,
|
||||||
|
plan: 4,
|
||||||
|
req: '物理+化学',
|
||||||
|
tuition: '6000/年',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
modalLoading.value = false
|
modalLoading.value = false
|
||||||
}, 400)
|
}, 400)
|
||||||
|
|
@ -283,6 +280,17 @@ function closeModal() {
|
||||||
showSaveConfirm.value = false
|
showSaveConfirm.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case '保':
|
||||||
|
return 'border-green-500 text-green-600 bg-green-50'
|
||||||
|
case '冲':
|
||||||
|
return 'border-orange-500 text-orange-500 bg-orange-50'
|
||||||
|
default:
|
||||||
|
return 'border-gray-400 text-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 为了节省流量,我们可以加一个简单的判断:只有是 PC 端时才初始加载数据
|
// 为了节省流量,我们可以加一个简单的判断:只有是 PC 端时才初始加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查窗口宽度是否大于 1024px
|
// 检查窗口宽度是否大于 1024px
|
||||||
|
|
@ -291,9 +299,10 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成一些模拟的“我的志愿”数据
|
// 生成一些模拟的“我的志愿”数据
|
||||||
// const initialVols = generateMockData(5, 1000)
|
const initialVols = generateMockData(5, 1000)
|
||||||
// // 为了模拟真实志愿,给它们加上自定义的序号或者直接利用数组索引
|
// 为了模拟真实志愿,给它们加上自定义的序号或者直接利用数组索引
|
||||||
// myVolunteers.value = initialVols
|
myVolunteers.value = initialVols
|
||||||
|
|
||||||
// 监听窗口大小变化(可选:如果用户旋转屏幕或拖拽窗口,动态决定是否加载)
|
// 监听窗口大小变化(可选:如果用户旋转屏幕或拖拽窗口,动态决定是否加载)
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
|
|
@ -311,7 +320,6 @@ function handleResize() {
|
||||||
|
|
||||||
const currentFilters = ref<FilterState | null>(null)
|
const currentFilters = ref<FilterState | null>(null)
|
||||||
const currentKeyword = ref('')
|
const currentKeyword = ref('')
|
||||||
|
|
||||||
function handleDataChange(data: { keyword: string, filters: FilterState }) {
|
function handleDataChange(data: { keyword: string, filters: FilterState }) {
|
||||||
console.warn('发起请求:', data.keyword, data.filters)
|
console.warn('发起请求:', data.keyword, data.filters)
|
||||||
currentKeyword.value = data.keyword
|
currentKeyword.value = data.keyword
|
||||||
|
|
@ -560,14 +568,20 @@ function deletePlan(planId: string) {
|
||||||
录取概率
|
录取概率
|
||||||
</th>
|
</th>
|
||||||
<th class="w-24 border-r border-slate-200 p-4">
|
<th class="w-24 border-r border-slate-200 p-4">
|
||||||
26省内<br>招生
|
25省内<br>招生
|
||||||
</th>
|
</th>
|
||||||
<!-- 假设中间有很多历年数据列,撑开宽度 -->
|
<!-- 假设中间有很多历年数据列,撑开宽度 -->
|
||||||
<th class="w-20 border-r border-slate-200 p-4">
|
<th class="w-20 border-r border-slate-200 p-4">
|
||||||
历年
|
历年
|
||||||
</th>
|
</th>
|
||||||
<th class="w-32 border-r border-slate-200 p-4" v-for="(item) in oldYears" :key="item">
|
<th class="w-32 border-r border-slate-200 p-4">
|
||||||
{{item}}
|
2024
|
||||||
|
</th>
|
||||||
|
<th class="w-32 border-r border-slate-200 p-4">
|
||||||
|
2023
|
||||||
|
</th>
|
||||||
|
<th class="w-32 border-r border-slate-200 p-4">
|
||||||
|
2022
|
||||||
</th>
|
</th>
|
||||||
<!-- 右侧冻结列:操作 -->
|
<!-- 右侧冻结列:操作 -->
|
||||||
<th
|
<th
|
||||||
|
|
@ -580,7 +594,7 @@ function deletePlan(planId: string) {
|
||||||
|
|
||||||
<!-- 表格内容 -->
|
<!-- 表格内容 -->
|
||||||
<tbody class="divide-y divide-slate-200">
|
<tbody class="divide-y divide-slate-200">
|
||||||
<template v-for="school in schools" :key="school.schoolCode + school.majorCode">
|
<template v-for="school in schools" :key="school.id">
|
||||||
<!-- Row 1 -->
|
<!-- Row 1 -->
|
||||||
<tr class="group transition-colors hover:bg-slate-50">
|
<tr class="group transition-colors hover:bg-slate-50">
|
||||||
<!-- Sticky Left: 院校信息 -->
|
<!-- Sticky Left: 院校信息 -->
|
||||||
|
|
@ -589,15 +603,15 @@ function deletePlan(planId: string) {
|
||||||
rowspan="4"
|
rowspan="4"
|
||||||
class="sticky left-0 z-20 border-r border-slate-200 bg-white p-4 align-top shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50"
|
class="sticky left-0 z-20 border-r border-slate-200 bg-white p-4 align-top shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<div class="mb-1 w-56 truncate text-left text-base text-slate-900 font-bold" :title="school.schoolName">
|
<div class="mb-1 w-56 truncate text-left text-base text-slate-900 font-bold" :title="school.name">
|
||||||
{{ school.schoolName }}
|
{{ school.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 flex flex-wrap gap-1 text-xs text-slate-500">
|
<div class="mb-2 flex flex-wrap gap-1 text-xs text-slate-500">
|
||||||
<span v-for="tag in [school.batch, school.category].filter(Boolean)" :key="tag" class="rounded bg-slate-100 px-1 py-0.5">{{ tag
|
<span v-for="tag in school.tags" :key="tag" class="rounded bg-slate-100 px-1 py-0.5">{{ tag
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-left text-xs text-slate-400">
|
<div class="text-left text-xs text-slate-400">
|
||||||
代码 {{ school.schoolCode }}
|
代码 {{ school.code }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
@ -608,10 +622,10 @@ function deletePlan(planId: string) {
|
||||||
{{ school.majorName }}
|
{{ school.majorName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 text-xs text-slate-500">
|
<div class="mb-1 text-xs text-slate-500">
|
||||||
{{ school.limitation }}
|
{{ school.requirements }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-xs text-slate-400">
|
<div class="mt-2 text-xs text-slate-400">
|
||||||
{{ `[${school.enrollmentCode}]` }} {{ school.tuition }}
|
{{ school.tuition }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
@ -620,14 +634,14 @@ function deletePlan(planId: string) {
|
||||||
class="border-r border-slate-100 bg-white p-4 text-center align-top group-hover:bg-slate-50"
|
class="border-r border-slate-100 bg-white p-4 text-center align-top group-hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<div class="mb-2 text-lg font-bold">
|
<div class="mb-2 text-lg font-bold">
|
||||||
{{ school.enrollProbability }}%
|
{{ school.probability }}%
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 flex justify-center">
|
<div class="mb-2 flex justify-center">
|
||||||
<div
|
<div
|
||||||
class="h-8 w-8 flex items-center justify-center border-2 rounded-full text-xs font-bold"
|
class="h-8 w-8 flex items-center justify-center border-2 rounded-full text-xs font-bold"
|
||||||
:class="getStatusColor(getProbabilityLabel(school.enrollProbability))"
|
:class="getStatusColor(school.statusLabel)"
|
||||||
>
|
>
|
||||||
{{ getProbabilityLabel(school.enrollProbability) }}
|
{{ school.statusLabel }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -637,7 +651,7 @@ function deletePlan(planId: string) {
|
||||||
class="border-r border-slate-100 bg-white p-4 text-center align-top group-hover:bg-slate-50"
|
class="border-r border-slate-100 bg-white p-4 text-center align-top group-hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<div class="text-lg font-medium">
|
<div class="text-lg font-medium">
|
||||||
{{ school.planNum }}人
|
{{ school.planCount }}人
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
@ -647,8 +661,14 @@ function deletePlan(planId: string) {
|
||||||
>
|
>
|
||||||
招生人数
|
招生人数
|
||||||
</td>
|
</td>
|
||||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||||
{{ school.historyMajorEnrollMap?.[year]?.enrollmentCount || "-" }}
|
{{ school.history["2024"]?.count || "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||||
|
-
|
||||||
</td>
|
</td>
|
||||||
<!-- Sticky Right: 操作 -->
|
<!-- Sticky Right: 操作 -->
|
||||||
<td
|
<td
|
||||||
|
|
@ -671,9 +691,11 @@ function deletePlan(planId: string) {
|
||||||
>
|
>
|
||||||
最低分数
|
最低分数
|
||||||
</td>
|
</td>
|
||||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm font-medium" v-for="year in oldYears" :key="year">
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm font-medium">
|
||||||
{{ school.historyMajorEnrollMap?.[year]?.admissionLine || "-" }}
|
{{ school.history["2024"]?.minScore || "-" }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Row 3: 历年线差 -->
|
<!-- Row 3: 历年线差 -->
|
||||||
|
|
@ -683,11 +705,11 @@ function deletePlan(planId: string) {
|
||||||
>
|
>
|
||||||
历年线差
|
历年线差
|
||||||
</td>
|
</td>
|
||||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||||
{{ (school.historyMajorEnrollMap?.[year]?.admissionLine && school.historyMajorEnrollMap?.[year]?.controlLine)
|
{{ school.history["2024"]?.diff || "-" }}
|
||||||
? (school.historyMajorEnrollMap[year].admissionLine - school.historyMajorEnrollMap[year].controlLine).toFixed(1)
|
|
||||||
: "-" }}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Row 4: 录取方式 -->
|
<!-- Row 4: 录取方式 -->
|
||||||
|
|
@ -697,9 +719,11 @@ function deletePlan(planId: string) {
|
||||||
>
|
>
|
||||||
录取方式
|
录取方式
|
||||||
</td>
|
</td>
|
||||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-xs text-slate-600" v-for="year in oldYears" :key="year">
|
<td class="border-r border-slate-100 px-2 py-2 text-center text-xs text-slate-600">
|
||||||
{{ school.historyMajorEnrollMap?.[year]?.rulesEnrollProbability || "-" }}
|
{{ school.history["2024"]?.method || "-" }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
|
<td class="border-r border-slate-100 px-2 py-2" />
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import request from '../request'
|
|
||||||
|
|
||||||
export interface UserMajorListRequest {
|
|
||||||
page?: number
|
|
||||||
size?: number
|
|
||||||
batch?: string
|
|
||||||
probability?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryMajorEnroll {
|
|
||||||
year: string
|
|
||||||
enrollmentCode: string
|
|
||||||
enrollmentCount: number
|
|
||||||
rulesEnrollProbability: string
|
|
||||||
probabilityOperator: string
|
|
||||||
admissionLine: number
|
|
||||||
controlLine: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MajorItem {
|
|
||||||
schoolCode: string
|
|
||||||
schoolName: string
|
|
||||||
majorCode: string
|
|
||||||
majorName: string
|
|
||||||
majorType: string
|
|
||||||
majorTypeChild: string
|
|
||||||
planNum: number
|
|
||||||
mainSubjects: string
|
|
||||||
limitation: string
|
|
||||||
chineseScoreLimitation: number
|
|
||||||
englishScoreLimitation: number
|
|
||||||
culturalScoreLimitation: number
|
|
||||||
professionalScoreLimitation: number
|
|
||||||
enrollmentCode: string
|
|
||||||
tuition: string
|
|
||||||
detail: string
|
|
||||||
category: string
|
|
||||||
batch: string
|
|
||||||
rulesEnrollProbability: string
|
|
||||||
probabilityOperator: string
|
|
||||||
rulesEnrollProbabilitySx: string
|
|
||||||
kslx: string
|
|
||||||
state: string
|
|
||||||
historyMajorEnrollMap: Record<string, HistoryMajorEnroll>
|
|
||||||
enrollProbability: number
|
|
||||||
studentScore: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserMajorListResponse {
|
|
||||||
list: {
|
|
||||||
items: MajorItem[]
|
|
||||||
probCount: Record<string, number>
|
|
||||||
}
|
|
||||||
total: number
|
|
||||||
page: number
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserMajorList(params: UserMajorListRequest) {
|
|
||||||
return request.get<UserMajorListResponse>('/user/major/list', {
|
|
||||||
params,
|
|
||||||
showLoading: false, // 页面内部处理 loading,不需要全屏遮罩
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +1,64 @@
|
||||||
# Task Detail
|
# Task Detail
|
||||||
|
|
||||||
## Session 2026-01-02 (1)
|
## 2025-12-18 Session 7
|
||||||
|
|
||||||
- **Execution Reason**: User requested to encapsulate the "User Recommended Major List API" and integrate it into the `simulate.vue` page (Panel A).
|
### Execution Reason
|
||||||
- **Execution Process**:
|
User requested to implement the score API (Task 2) and integrate it into the application.
|
||||||
1. Analyzed the project structure and existing API encapsulation pattern.
|
|
||||||
2. Created `src/service/api/major.ts` defining `UserMajorListRequest`, `UserMajorListResponse`, `MajorItem` interfaces and `getUserMajorList` function.
|
|
||||||
3. Updated `src/pages/simulate.vue`:
|
|
||||||
- Imported the new API.
|
|
||||||
- Refactored `schools` state to use `MajorItem` type.
|
|
||||||
- Implemented `loadMore` function to fetch data from API with pagination and filtering.
|
|
||||||
- Added helper functions `getProbabilityLabel` and `getStatusColor`.
|
|
||||||
- Updated the template to bind correct fields from `MajorItem` (e.g., `schoolName`, `enrollProbability`, `historyMajorEnrollMap`).
|
|
||||||
4. Updated project documentation (`project_index.md`, `project_codebase.md`, `project_task.md`, `project_doing.md`).
|
|
||||||
- **Execution Result**: Successfully integrated the recommended major list API. Panel A in `simulate.vue` now displays real data structure (mapped from API) and supports loading more data.
|
|
||||||
|
|
||||||
## Session 2026-01-02 (2)
|
### Execution Process
|
||||||
|
1. **API Encapsulation**:
|
||||||
|
- Found `成绩修改api.md` describing `/user/score` (GET) and `/user/score/save-score` (POST).
|
||||||
|
- Created `src/service/api/score.ts` defining `SaveScoreRequest` and `ScoreInfo` types and request functions.
|
||||||
|
2. **State Management**:
|
||||||
|
- Created `src/stores/score.ts` using Pinia to manage score data (`fetchScore`, `saveScore`).
|
||||||
|
3. **Navigation Integration**:
|
||||||
|
- Modified `src/components/TheNavigation.vue` to use `useScoreStore`.
|
||||||
|
- On login/mount, it fetches the score.
|
||||||
|
- On logout, it clears the score.
|
||||||
|
- Displayed score info (Category, Cultural Score) in the navigation bar instead of hardcoded values.
|
||||||
|
4. **Form Integration**:
|
||||||
|
- Updated `src/components/ScoreForm.vue` to use `useScoreStore`.
|
||||||
|
- Populated form with existing score data on mount/watch.
|
||||||
|
- Implemented `handleSubmit` to call `scoreStore.saveScore` before emitting `confirm`.
|
||||||
|
- Mapped frontend form data to backend API request structure.
|
||||||
|
|
||||||
- **Execution Reason**: User updated the API response structure (`list` -> `list.items`) and requested dynamic tab counts from `list.probCount`.
|
### Execution Result
|
||||||
- **Execution Process**:
|
- Score management is now fully integrated.
|
||||||
1. Updated `src/service/api/major.ts`: Modified `UserMajorListResponse` to include nested `items` and `probCount`.
|
- User can view their score in the navigation bar.
|
||||||
2. Updated `src/pages/simulate.vue`:
|
- User can edit and save their score using the score form.
|
||||||
- Updated `tabs` definition to include 'stable' (较稳妥) and made it reactive.
|
|
||||||
- Updated `loadMore` function to access data from `res.list.items`.
|
## 2025-12-18 Session 8
|
||||||
- Added logic to update `tabs[].count` using `res.list.probCount`.
|
|
||||||
- Added 'stable' case to filter logic.
|
### Execution Reason
|
||||||
- **Execution Result**: The application now correctly handles the new API structure and dynamically updates the tab counts based on backend data.
|
User re-iterated the request to ensure score API encapsulation and integration, likely for verification or summary.
|
||||||
|
|
||||||
|
### Execution Process
|
||||||
|
1. **Verification**:
|
||||||
|
- Verified `src/service/api/score.ts` implementation matches `成绩修改api.md`.
|
||||||
|
- Verified `src/stores/score.ts` correctly handles async actions.
|
||||||
|
- Verified `src/components/ScoreForm.vue` integrates with the store for both loading and saving data.
|
||||||
|
2. **Documentation Update**:
|
||||||
|
- Updated `project_task.md` to mark all Task 2 items as completed.
|
||||||
|
- Updated `project_doing.md` with verification step.
|
||||||
|
|
||||||
|
### Execution Result
|
||||||
|
- Confirmed all score-related tasks are completed and code is correct.
|
||||||
|
- Documentation is up to date.
|
||||||
|
|
||||||
|
## 2025-12-18 Session 9
|
||||||
|
|
||||||
|
### Execution Reason
|
||||||
|
User reported that score data is lost (becomes null) in `ScoreForm.vue` after page refresh.
|
||||||
|
|
||||||
|
### Execution Process
|
||||||
|
1. **Analysis**:
|
||||||
|
- Identified that `src/stores/score.ts` lacks persistence (by design) and relies on runtime fetching.
|
||||||
|
- Found that `ScoreForm.vue` only calls `initForm` on mount, which does nothing if `scoreStore.scoreInfo` is null.
|
||||||
|
- On page refresh, the store is reset, so the form remains empty.
|
||||||
|
2. **Fix**:
|
||||||
|
- Modified `src/components/ScoreForm.vue`'s `onMounted` hook.
|
||||||
|
- Added logic: if `scoreStore.scoreInfo` is missing, call `scoreStore.fetchScore()`.
|
||||||
|
- Existing watcher on `scoreInfo` will handle populating the form once data arrives.
|
||||||
|
|
||||||
|
### Execution Result
|
||||||
|
- `ScoreForm` now robustly handles page refreshes by fetching data on demand if needed.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue