update
This commit is contained in:
parent
f1d30a2916
commit
c2ef1cad84
|
|
@ -4,7 +4,7 @@
|
|||
VITE_API_BASE_URL=/api
|
||||
|
||||
# Proxy Target (Where the /api requests are forwarded to) 改完后需要重启
|
||||
VITE_API_PROXY_TARGET=http://localhost:8080
|
||||
VITE_API_PROXY_TARGET=http://localhost:8081
|
||||
|
||||
# Other development-specific variables
|
||||
VITE_APP_TITLE=My App (Development)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
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,39 +1,13 @@
|
|||
# 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`
|
||||
- **Purpose**: API definitions for score management.
|
||||
- **Methods**: `getScore`, `saveScore`.
|
||||
- **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`
|
||||
- **Purpose**: Manages user session state.
|
||||
- **State**: `token`, `userInfo`.
|
||||
|
|
@ -62,3 +36,9 @@
|
|||
### `src/pages/privacy-policy.vue`
|
||||
- **Purpose**: Displays the Privacy Policy.
|
||||
- **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,63 +1,14 @@
|
|||
# Project Doing
|
||||
|
||||
## 2025-12-18
|
||||
## 2026-01-02
|
||||
|
||||
### [Task 1] Global Message Component, API Encapsulation, and Login/Logout Integration
|
||||
- **Time**: 2025-12-18
|
||||
- **Goal**: Implement global message, encapsulate axios, and integrate login.
|
||||
### [Task 6] User Recommended Major List API Integration
|
||||
- **Time**: 2026-01-02
|
||||
- **Goal**: Integrate the user recommended major list API into the simulation page.
|
||||
- **Scope**:
|
||||
- `src/components/ui/WMessage.vue` (Review/Update)
|
||||
- `package.json` (Add axios)
|
||||
- `src/service/request/index.ts` (Create)
|
||||
- `src/service/api/auth.ts` (Create)
|
||||
- `src/stores/user.ts` (Update)
|
||||
- `src/components/TheNavigation.vue` (Update)
|
||||
|
||||
### [Task 2] Score API Encapsulation and Business Integration
|
||||
- **Time**: 2025-12-18
|
||||
- **Goal**: Encapsulate Score API and integrate into components.
|
||||
- **Scope**:
|
||||
- `src/service/api/score.ts` (Create)
|
||||
- `src/stores/score.ts` (Create)
|
||||
- `src/components/TheNavigation.vue` (Update)
|
||||
- `src/components/ScoreForm.vue` (Update)
|
||||
- `project_task.md` (Update)
|
||||
|
||||
### [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)
|
||||
- `src/service/api/major.ts` (Create)
|
||||
- `src/pages/simulate.vue` (Update)
|
||||
- **Result**:
|
||||
- Created `src/service/api/major.ts` encapsulating `/user/major/list` API.
|
||||
- Updated `src/pages/simulate.vue` to fetch and display data in Panel A using the new API.
|
||||
- Implemented infinite scroll and filtering by probability.
|
||||
- Mapped API response fields to the UI table.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- `src/service/request/index.ts`: Axios wrapper.
|
||||
- `src/service/api/auth.ts`: Authentication 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/score.ts`: Score Pinia store.
|
||||
- `src/components/ui/WMessage.vue`: Message component UI.
|
||||
|
|
@ -14,3 +15,4 @@
|
|||
- `src/pages/index.vue`: Home page with dashboard/welcome sidebar.
|
||||
- `src/pages/agreement.vue`: User agreement page.
|
||||
- `src/pages/privacy-policy.vue`: Privacy policy page.
|
||||
- `src/pages/simulate.vue`: Simulation and volunteer filling page.
|
||||
|
|
|
|||
|
|
@ -1,38 +1,5 @@
|
|||
# Project Tasks
|
||||
|
||||
- [x] [Task 1] Global Message Component, API Encapsulation, and Login/Logout Integration <!-- id: 0 -->
|
||||
- [x] Check/Implement Global Message Component <!-- id: 1 -->
|
||||
- [x] Install axios <!-- id: 2 -->
|
||||
- [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 -->
|
||||
- [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] Integrate API in `src/pages/simulate.vue` (Panel A) <!-- id: 34 -->
|
||||
- [x] Update template to display real data <!-- id: 35 -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { FilterState } from '~/components/FilterBar.vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getUserMajorList, type MajorItem } from '~/service/api/major'
|
||||
|
||||
// --- 类型定义 ---
|
||||
type TabKey = 'all' | 'hard' | 'risky' | 'safe' | '本科' | '专科'
|
||||
|
|
@ -13,7 +14,8 @@ interface YearData {
|
|||
method?: string
|
||||
}
|
||||
|
||||
interface School {
|
||||
// 仅用于 Panel B 的本地模拟数据接口
|
||||
interface VolunteerSchool {
|
||||
id: number
|
||||
name: string
|
||||
tags: string[]
|
||||
|
|
@ -76,7 +78,7 @@ const activePanel = ref<PanelType>('market') // 当前激活的面板
|
|||
|
||||
// 模拟“我的志愿”数据 (为了演示Panel B,初始化一些数据)
|
||||
// 在实际业务中,这应该由 selectedMajorCodes 对应的完整数据填充
|
||||
const myVolunteers = ref<School[]>([])
|
||||
const myVolunteers = ref<VolunteerSchool[]>([])
|
||||
|
||||
const volunteerCurrentTab = ref<TabKey>('本科')
|
||||
const volunteerTabs = [
|
||||
|
|
@ -91,15 +93,20 @@ const tabs = [
|
|||
{ key: 'risky', label: '可冲击', count: 11 },
|
||||
{ key: 'safe', label: '可保底', count: 1 },
|
||||
]
|
||||
const oldYears = ref(['2025','2024','2023'])
|
||||
|
||||
const schools = ref<School[]>([]) // 列表数据
|
||||
// Panel A 列表数据
|
||||
const schools = ref<MajorItem[]>([])
|
||||
const page = ref(1)
|
||||
const size = ref(10)
|
||||
const total = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const isFinished = ref(false)
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// 弹窗相关状态
|
||||
const showModal = ref(false)
|
||||
const currentSchool = ref<School | null>(null)
|
||||
const currentSchool = ref<MajorItem | null>(null)
|
||||
const modalLoading = ref(false)
|
||||
const modalMajors = ref<MajorDetail[]>([])
|
||||
|
||||
|
|
@ -108,53 +115,77 @@ const selectedMajorCodes = ref<string[]>([]) // 存储已选的专业Code,数
|
|||
const showSaveConfirm = ref(false) // 控制气泡确认框显示
|
||||
const isSaving = ref(false) // 保存接口Loading状态
|
||||
|
||||
// --- 模拟数据生成器 ---
|
||||
function generateMockData(count: number, startId: number) {
|
||||
const result: School[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = startId + i
|
||||
result.push({
|
||||
id,
|
||||
name: `模拟院校名称 ${id}号`,
|
||||
tags: i % 2 === 0 ? ['湖北', '公办', '师范'] : ['陕西', '民办', '综合'],
|
||||
code: (5000 + id).toString(),
|
||||
probability: (Math.random() * 100).toFixed(2),
|
||||
statusLabel: Math.random() > 0.5 ? '保' : '冲',
|
||||
calcScore: 450 + Math.floor(Math.random() * 50),
|
||||
diffScore: 20 + Math.floor(Math.random() * 30),
|
||||
majorName: i % 3 === 0 ? '音乐学 (师范)' : '计算机科学与技术',
|
||||
requirements: '再选: 不限',
|
||||
tuition: '5000元/年',
|
||||
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
|
||||
// --- 辅助函数 ---
|
||||
function getProbabilityLabel(prob: number): string {
|
||||
if (prob >= 93) return '保'
|
||||
if (prob >= 73) return '稳'
|
||||
if (prob >= 60) return '冲'
|
||||
return '难'
|
||||
}
|
||||
|
||||
// --- 无限滚动逻辑 ---
|
||||
async function loadMore() {
|
||||
if (isLoading.value || isFinished.value)
|
||||
return
|
||||
function getStatusColor(status: string) {
|
||||
switch (status) {
|
||||
case '保':
|
||||
return 'border-green-500 text-green-600 bg-green-50'
|
||||
case '稳':
|
||||
return 'border-blue-500 text-blue-600 bg-blue-50'
|
||||
case '冲':
|
||||
return 'border-orange-500 text-orange-500 bg-orange-50'
|
||||
default:
|
||||
return 'border-gray-400 text-gray-400 bg-gray-50'
|
||||
}
|
||||
}
|
||||
|
||||
// --- 数据加载逻辑 ---
|
||||
async function loadMore(reset = false) {
|
||||
if (isLoading.value) return
|
||||
if (!reset && isFinished.value) return
|
||||
|
||||
isLoading.value = true
|
||||
if (reset) {
|
||||
page.value = 1
|
||||
schools.value = []
|
||||
isFinished.value = false
|
||||
}
|
||||
|
||||
// 模拟网络延迟
|
||||
setTimeout(() => {
|
||||
const newItems = generateMockData(5, schools.value.length + 1)
|
||||
schools.value = [...schools.value, ...newItems]
|
||||
isLoading.value = false
|
||||
try {
|
||||
// 映射 Tab 到 API 参数
|
||||
let probability: string | undefined
|
||||
if (currentTab.value === 'hard') probability = '难录取'
|
||||
if (currentTab.value === 'risky') probability = '可冲击'
|
||||
if (currentTab.value === 'safe') probability = '可保底'
|
||||
|
||||
// 假设总数达到 50 就停止加载
|
||||
if (schools.value.length >= 25) {
|
||||
const res = await getUserMajorList({
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
probability
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
schools.value.push(...res.list)
|
||||
total.value = res.total
|
||||
|
||||
if (schools.value.length >= res.total || res.list.length < size.value) {
|
||||
isFinished.value = true
|
||||
} else {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
isFinished.value = true
|
||||
}
|
||||
}, 800)
|
||||
} catch (error) {
|
||||
console.error('Failed to load major list:', error)
|
||||
isFinished.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 Tab 切换,重新加载
|
||||
watch(currentTab, () => {
|
||||
loadMore(true)
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollContainer.value)
|
||||
return
|
||||
|
|
@ -166,7 +197,7 @@ function handleScroll() {
|
|||
}
|
||||
|
||||
// --- 弹窗逻辑 ---
|
||||
async function openMajorModal(school: School) {
|
||||
async function openMajorModal(school: MajorItem) {
|
||||
currentSchool.value = school
|
||||
showModal.value = true
|
||||
modalLoading.value = true
|
||||
|
|
@ -176,7 +207,9 @@ async function openMajorModal(school: School) {
|
|||
showSaveConfirm.value = false
|
||||
|
||||
// 模拟获取该学校其他专业(更丰富的数据)
|
||||
// TODO: 这里应该调用另外一个 API 获取详情
|
||||
setTimeout(() => {
|
||||
// Mock data for now
|
||||
modalMajors.value = [
|
||||
{
|
||||
code: '02',
|
||||
|
|
@ -188,46 +221,6 @@ async function openMajorModal(school: School) {
|
|||
req: '历史+不限',
|
||||
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
|
||||
}, 400)
|
||||
|
|
@ -280,17 +273,6 @@ function closeModal() {
|
|||
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 端时才初始加载数据
|
||||
onMounted(() => {
|
||||
// 检查窗口宽度是否大于 1024px
|
||||
|
|
@ -299,10 +281,9 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
// 生成一些模拟的“我的志愿”数据
|
||||
const initialVols = generateMockData(5, 1000)
|
||||
// 为了模拟真实志愿,给它们加上自定义的序号或者直接利用数组索引
|
||||
myVolunteers.value = initialVols
|
||||
|
||||
// const initialVols = generateMockData(5, 1000)
|
||||
// // 为了模拟真实志愿,给它们加上自定义的序号或者直接利用数组索引
|
||||
// myVolunteers.value = initialVols
|
||||
// 监听窗口大小变化(可选:如果用户旋转屏幕或拖拽窗口,动态决定是否加载)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
|
@ -320,6 +301,7 @@ function handleResize() {
|
|||
|
||||
const currentFilters = ref<FilterState | null>(null)
|
||||
const currentKeyword = ref('')
|
||||
|
||||
function handleDataChange(data: { keyword: string, filters: FilterState }) {
|
||||
console.warn('发起请求:', data.keyword, data.filters)
|
||||
currentKeyword.value = data.keyword
|
||||
|
|
@ -568,20 +550,14 @@ function deletePlan(planId: string) {
|
|||
录取概率
|
||||
</th>
|
||||
<th class="w-24 border-r border-slate-200 p-4">
|
||||
25省内<br>招生
|
||||
26省内<br>招生
|
||||
</th>
|
||||
<!-- 假设中间有很多历年数据列,撑开宽度 -->
|
||||
<th class="w-20 border-r border-slate-200 p-4">
|
||||
历年
|
||||
</th>
|
||||
<th class="w-32 border-r border-slate-200 p-4">
|
||||
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 class="w-32 border-r border-slate-200 p-4" v-for="(item) in oldYears" :key="item">
|
||||
{{item}}
|
||||
</th>
|
||||
<!-- 右侧冻结列:操作 -->
|
||||
<th
|
||||
|
|
@ -594,7 +570,7 @@ function deletePlan(planId: string) {
|
|||
|
||||
<!-- 表格内容 -->
|
||||
<tbody class="divide-y divide-slate-200">
|
||||
<template v-for="school in schools" :key="school.id">
|
||||
<template v-for="school in schools" :key="school.schoolCode + school.majorCode">
|
||||
<!-- Row 1 -->
|
||||
<tr class="group transition-colors hover:bg-slate-50">
|
||||
<!-- Sticky Left: 院校信息 -->
|
||||
|
|
@ -603,15 +579,15 @@ function deletePlan(planId: string) {
|
|||
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"
|
||||
>
|
||||
<div class="mb-1 w-56 truncate text-left text-base text-slate-900 font-bold" :title="school.name">
|
||||
{{ school.name }}
|
||||
<div class="mb-1 w-56 truncate text-left text-base text-slate-900 font-bold" :title="school.schoolName">
|
||||
{{ school.schoolName }}
|
||||
</div>
|
||||
<div class="mb-2 flex flex-wrap gap-1 text-xs text-slate-500">
|
||||
<span v-for="tag in school.tags" :key="tag" class="rounded bg-slate-100 px-1 py-0.5">{{ tag
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-left text-xs text-slate-400">
|
||||
代码 {{ school.code }}
|
||||
代码 {{ school.schoolCode }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
|
@ -622,10 +598,10 @@ function deletePlan(planId: string) {
|
|||
{{ school.majorName }}
|
||||
</div>
|
||||
<div class="mb-1 text-xs text-slate-500">
|
||||
{{ school.requirements }}
|
||||
{{ school.limitation }}
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-400">
|
||||
{{ school.tuition }}
|
||||
{{ `[${school.enrollmentCode}]` }} {{ school.tuition }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
|
@ -634,14 +610,14 @@ function deletePlan(planId: string) {
|
|||
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">
|
||||
{{ school.probability }}%
|
||||
{{ school.enrollProbability }}%
|
||||
</div>
|
||||
<div class="mb-2 flex justify-center">
|
||||
<div
|
||||
class="h-8 w-8 flex items-center justify-center border-2 rounded-full text-xs font-bold"
|
||||
:class="getStatusColor(school.statusLabel)"
|
||||
:class="getStatusColor(getProbabilityLabel(school.enrollProbability))"
|
||||
>
|
||||
{{ school.statusLabel }}
|
||||
{{ getProbabilityLabel(school.enrollProbability) }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -651,7 +627,7 @@ function deletePlan(planId: string) {
|
|||
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">
|
||||
{{ school.planCount }}人
|
||||
{{ school.planNum }}人
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
|
@ -661,14 +637,8 @@ function deletePlan(planId: string) {
|
|||
>
|
||||
招生人数
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||
{{ 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 class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
||||
{{ school.historyMajorEnrollMap?.[year]?.enrollmentCount || "-" }}
|
||||
</td>
|
||||
<!-- Sticky Right: 操作 -->
|
||||
<td
|
||||
|
|
@ -691,11 +661,9 @@ function deletePlan(planId: string) {
|
|||
>
|
||||
最低分数
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm font-medium">
|
||||
{{ school.history["2024"]?.minScore || "-" }}
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm font-medium" v-for="year in oldYears" :key="year">
|
||||
{{ school.historyMajorEnrollMap?.[year]?.admissionLine || "-" }}
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
</tr>
|
||||
|
||||
<!-- Row 3: 历年线差 -->
|
||||
|
|
@ -705,11 +673,11 @@ function deletePlan(planId: string) {
|
|||
>
|
||||
历年线差
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm">
|
||||
{{ school.history["2024"]?.diff || "-" }}
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-sm" v-for="year in oldYears" :key="year">
|
||||
{{ (school.historyMajorEnrollMap?.[year]?.admissionLine && school.historyMajorEnrollMap?.[year]?.controlLine)
|
||||
? (school.historyMajorEnrollMap[year].admissionLine - school.historyMajorEnrollMap[year].controlLine).toFixed(1)
|
||||
: "-" }}
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
</tr>
|
||||
|
||||
<!-- Row 4: 录取方式 -->
|
||||
|
|
@ -719,11 +687,9 @@ function deletePlan(planId: string) {
|
|||
>
|
||||
录取方式
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2 text-center text-xs text-slate-600">
|
||||
{{ school.history["2024"]?.method || "-" }}
|
||||
<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">
|
||||
{{ school.historyMajorEnrollMap?.[year]?.rulesEnrollProbability || "-" }}
|
||||
</td>
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
<td class="border-r border-slate-100 px-2 py-2" />
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
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: MajorItem[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export function getUserMajorList(params: UserMajorListRequest) {
|
||||
return request.get<UserMajorListResponse>('/user/major/list', {
|
||||
params,
|
||||
showLoading: false, // 页面内部处理 loading,不需要全屏遮罩
|
||||
})
|
||||
}
|
||||
|
|
@ -1,64 +1,16 @@
|
|||
# Task Detail
|
||||
|
||||
## 2025-12-18 Session 7
|
||||
## Session 2026-01-02
|
||||
|
||||
### Execution Reason
|
||||
User requested to implement the score API (Task 2) and integrate it into the application.
|
||||
|
||||
### 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 Result
|
||||
- Score management is now fully integrated.
|
||||
- User can view their score in the navigation bar.
|
||||
- User can edit and save their score using the score form.
|
||||
|
||||
## 2025-12-18 Session 8
|
||||
|
||||
### Execution Reason
|
||||
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.
|
||||
- **Execution Reason**: User requested to encapsulate the "User Recommended Major List API" and integrate it into the `simulate.vue` page (Panel A).
|
||||
- **Execution Process**:
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue