Compare commits

...

2 Commits

Author SHA1 Message Date
zhouwentao 7f27542057 模拟报志愿列表对接接口完成 2026-01-02 21:59:46 +08:00
zhouwentao c2ef1cad84 update 2026-01-02 21:38:55 +08:00
9 changed files with 361 additions and 330 deletions

View File

@ -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)

View File

@ -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|
# 数据模型

View File

@ -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).

View File

@ -1,63 +1,25 @@
# 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)
- `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.
### [Task 2] Score API Encapsulation and Business Integration
- **Time**: 2025-12-18
- **Goal**: Encapsulate Score API and integrate into components.
### [Task 7] Update Major List API Response Structure
- **Time**: 2026-01-02
- **Goal**: Adapt to the updated API response structure and implement dynamic tab counting.
- **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` (Update interface)
- `src/pages/simulate.vue` (Update logic)
- **Result**:
- Updated `UserMajorListResponse` to support `{ list: { items: [], probCount: {} } }` structure.
- Added 'stable' (较稳妥) tab to `simulate.vue`.
- Implemented dynamic update of tab counts using `probCount` from API response.

View File

@ -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.

View File

@ -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 -->

View File

@ -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 = [
@ -84,22 +86,27 @@ const volunteerTabs = [
{ key: '专科', label: '专科', count: 2, max: 64 },
]
const currentTab = ref<TabKey>('all')
const tabs = [
{ key: 'all', label: '全部', count: 157 },
{ key: 'hard', label: '难录取', count: 145 },
{ key: 'risky', label: '可冲击', count: 11 },
const currentTab = ref<TabKey>('stable')
const tabs = ref([
{ key: 'safe', label: '可保底', count: 1 },
]
{ key: 'stable', label: '较稳妥', count: 11 },
{ key: 'risky', label: '可冲击', count: 11 },
{ key: 'hard', label: '难录取', count: 145 },
])
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,87 @@ 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 === 'stable') probability = '较稳妥'
if (currentTab.value === 'safe') probability = '可保底'
// 50
if (schools.value.length >= 25) {
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
}
}, 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 +207,7 @@ function handleScroll() {
}
// --- ---
async function openMajorModal(school: School) {
async function openMajorModal(school: MajorItem) {
currentSchool.value = school
showModal.value = true
modalLoading.value = true
@ -176,7 +217,9 @@ async function openMajorModal(school: School) {
showSaveConfirm.value = false
//
// TODO: API
setTimeout(() => {
// Mock data for now
modalMajors.value = [
{
code: '02',
@ -188,46 +231,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 +283,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 +291,9 @@ onMounted(() => {
}
//
const initialVols = generateMockData(5, 1000)
//
myVolunteers.value = initialVols
// const initialVols = generateMockData(5, 1000)
// //
// myVolunteers.value = initialVols
//
window.addEventListener('resize', handleResize)
})
@ -320,6 +311,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 +560,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 +580,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 +589,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 +608,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 +620,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 +637,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 +647,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 +671,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 +683,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 +697,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>

64
src/service/api/major.ts Normal file
View File

@ -0,0 +1,64 @@
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不需要全屏遮罩
})
}

View File

@ -1,64 +1,28 @@
# Task Detail
## 2025-12-18 Session 7
## Session 2026-01-02 (1)
### Execution Reason
User requested to implement the score API (Task 2) and integrate it into the application.
- **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.
### 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.
## Session 2026-01-02 (2)
### 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 updated the API response structure (`list` -> `list.items`) and requested dynamic tab counts from `list.probCount`.
- **Execution Process**:
1. Updated `src/service/api/major.ts`: Modified `UserMajorListResponse` to include nested `items` and `probCount`.
2. Updated `src/pages/simulate.vue`:
- Updated `tabs` definition to include 'stable' (较稳妥) and made it reactive.
- Updated `loadMore` function to access data from `res.list.items`.
- Added logic to update `tabs[].count` using `res.list.probCount`.
- 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.