updates
This commit is contained in:
parent
256c34d5af
commit
b046ea95d0
|
|
@ -2,6 +2,7 @@
|
|||
"cSpell.words": [
|
||||
"antfu",
|
||||
"beian",
|
||||
"cognitio",
|
||||
"demi",
|
||||
"iconify",
|
||||
"intlify",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
---
|
||||
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
|
||||
|
||||
# 用户分数操作
|
||||
|
||||
## POST 保存用户成绩
|
||||
|
||||
POST /user/score/save-score
|
||||
|
||||
> Body 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"cognitioPolyclinic": "文科",
|
||||
"subjectList": [
|
||||
"化学",
|
||||
"生物",
|
||||
"地理"
|
||||
],
|
||||
"professionalCategory": "表演类",
|
||||
"professionalCategoryChildren": [
|
||||
"服装表演",
|
||||
"戏剧影视表演"
|
||||
],
|
||||
"professionalCategoryChildrenScore": {
|
||||
"服装表演": 50,
|
||||
"戏剧影视表演": 10
|
||||
},
|
||||
"professionalScore": 250,
|
||||
"culturalScore": 500,
|
||||
"englishScore": 120,
|
||||
"chineseScore": 121,
|
||||
"province": "河南"
|
||||
}
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
|名称|位置|类型|必选|说明|
|
||||
|---|---|---|---|---|
|
||||
|body|body|[dto.SaveScoreRequest](#schemadto.savescorerequest)| 是 |none|
|
||||
|
||||
> 返回示例
|
||||
|
||||
> 200 Response
|
||||
|
||||
```
|
||||
{"code":200,"message":"success","data":{"id":"bdd80291-797e-451c-9bf0-c81705362dc9","type":"1","educationalLevel":"1","professionalCategory":"表演类","subjects":"化学,生物,地理","professionalScore":250,"culturalScore":500,"ranking":0,"createBy":"1779515858733772802","createTime":"0001-01-01T00:00:00Z","updateBy":"","updateTime":"0001-01-01T00:00:00Z","state":"1","province":"河南","cognitioPolyclinic":"文科","batch":"","englishScore":120,"chineseScore":121,"yybysy":0,"yybyqy":0,"yyjy":0,"xjysdy":0,"xjysby":10,"fzby":50,"professionalCategoryChildren":"服装表演,戏剧影视表演","kbdNum":0,"nlqNum":0,"kcjNum":0,"jwtNum":0,"calculationTableName":""}}
|
||||
```
|
||||
|
||||
### 返回结果
|
||||
|
||||
|状态码|状态码含义|说明|数据模型|
|
||||
|---|---|---|---|
|
||||
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[common.Response](#schemacommon.response)|
|
||||
|
||||
## GET 获取当前用户的当前分数
|
||||
|
||||
GET /user/score
|
||||
|
||||
> 返回示例
|
||||
|
||||
> 200 Response
|
||||
|
||||
```
|
||||
{"code":200,"message":"success","data":{"id":"2000794240905117697","type":"2","educationalLevel":"1","cognitioPolyclinic":"文科","subjectList":["政治","化学"],"professionalCategory":"美术与设计类","professionalCategoryChildren":[],"professionalCategoryChildrenScore":{},"professionalScore":234,"culturalScore":500,"englishScore":1,"chineseScore":1,"province":"河南","state":"1"}}
|
||||
```
|
||||
|
||||
### 返回结果
|
||||
|
||||
|状态码|状态码含义|说明|数据模型|
|
||||
|---|---|---|---|
|
||||
|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|
|
||||
|»» id|string|true|none||none|
|
||||
|»» type|string|true|none||none|
|
||||
|»» educationalLevel|string|true|none|1-本科,2-专科|none|
|
||||
|»» cognitioPolyclinic|string|true|none|文理分班(文科/理科)|none|
|
||||
|»» subjectList|[string]|true|none|专业选课|none|
|
||||
|»» professionalCategory|string|true|none|专业类别(美术与设计类、音乐类...)|none|
|
||||
|»» professionalCategoryChildren|[string]|true|none|专业子级列表|none|
|
||||
|»» professionalCategoryChildrenScore|object|true|none|专业子级成绩|none|
|
||||
|»» professionalScore|integer|true|none|统考成绩|none|
|
||||
|»» culturalScore|integer|true|none|文化成绩|none|
|
||||
|»» englishScore|integer|true|none|英语成绩|none|
|
||||
|»» chineseScore|integer|true|none|语文成绩|none|
|
||||
|»» province|string|true|none|地区|none|
|
||||
|»» state|string|true|none|状态(1-启用,2-关闭)|none|
|
||||
|
||||
# 数据模型
|
||||
|
||||
<h2 id="tocS_common.Response">common.Response</h2>
|
||||
|
||||
<a id="schemacommon.response"></a>
|
||||
<a id="schema_common.Response"></a>
|
||||
<a id="tocScommon.response"></a>
|
||||
<a id="tocscommon.response"></a>
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": null,
|
||||
"message": "string"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
|名称|类型|必选|约束|中文名|说明|
|
||||
|---|---|---|---|---|---|
|
||||
|code|integer|false|none||none|
|
||||
|data|any|false|none||none|
|
||||
|message|string|false|none||none|
|
||||
|
||||
<h2 id="tocS_dto.SaveScoreRequest">dto.SaveScoreRequest</h2>
|
||||
|
||||
<a id="schemadto.savescorerequest"></a>
|
||||
<a id="schema_dto.SaveScoreRequest"></a>
|
||||
<a id="tocSdto.savescorerequest"></a>
|
||||
<a id="tocsdto.savescorerequest"></a>
|
||||
|
||||
```json
|
||||
{
|
||||
"ChineseScore": 0,
|
||||
"CognitioPolyclinic": "string",
|
||||
"CulturalScore": 0,
|
||||
"EnglishScore": 0,
|
||||
"ProfessionalCategory": "string",
|
||||
"ProfessionalCategoryChildren": [
|
||||
"string"
|
||||
],
|
||||
"ProfessionalCategoryChildrenScore": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"ProfessionalScore": 0,
|
||||
"Province": "string",
|
||||
"SubjectList": [
|
||||
"string"
|
||||
],
|
||||
"createBy": "string"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
|名称|类型|必选|约束|中文名|说明|
|
||||
|---|---|---|---|---|---|
|
||||
|ChineseScore|number|true|none||none|
|
||||
|CognitioPolyclinic|string|true|none||none|
|
||||
|CulturalScore|number|true|none||none|
|
||||
|EnglishScore|number|true|none||none|
|
||||
|ProfessionalCategory|string|true|none||none|
|
||||
|ProfessionalCategoryChildren|[string]|true|none||none|
|
||||
|ProfessionalCategoryChildrenScore|object|true|none||none|
|
||||
|» **additionalProperties**|number(float64)|false|none||none|
|
||||
|ProfessionalScore|number|true|none||none|
|
||||
|Province|string|true|none||none|
|
||||
|SubjectList|[string]|true|none||none|
|
||||
|createBy|string|false|none||none|
|
||||
|
||||
|
|
@ -11,21 +11,34 @@
|
|||
- **Purpose**: Encapsulates Axios for API requests.
|
||||
- **Features**:
|
||||
- Request Interceptor:
|
||||
- Adds `token` to headers.
|
||||
- Adds `X-App-Sign` (MD5(timestamp + secret)) and `X-App-Timestamp` headers for security.
|
||||
- Adds `Authorization: Bearer <token>`.
|
||||
- Adds `X-App-Sign` and `X-App-Timestamp` headers.
|
||||
- Starts `NProgress`.
|
||||
- Response Interceptor: Unwraps `data`; handles global errors (401, 403, 500); stops `NProgress`.
|
||||
- Response Interceptor: Unwraps `data`; handles global errors (401, 403, 500); prioritizes backend error messages; stops `NProgress`.
|
||||
- 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/stores/user.ts`
|
||||
- **Purpose**: Manages user session state.
|
||||
- **State**: `token`, `userInfo`.
|
||||
- **Actions**: `login` (calls API), `logout`, `setToken`, `setUserInfo`.
|
||||
- **Persistence**: Loads/Saves state to `localStorage`.
|
||||
|
||||
### `src/stores/score.ts`
|
||||
- **Purpose**: Manages user score state.
|
||||
- **State**: `scoreInfo`.
|
||||
- **Actions**: `fetchScore`, `saveScore`, `clearScore`.
|
||||
|
||||
### `src/components/TheNavigation.vue`
|
||||
- **Updated**: Added integration with `userStore` for login/logout and `message` for notifications.
|
||||
- **Updated**: Added integration with `userStore` and `scoreStore`. Displays user info and score info. Handles login/logout logic.
|
||||
|
||||
### `src/components/ScoreForm.vue`
|
||||
- **Updated**: Integrated with `scoreStore` to load and save score data. Maps form state to backend API structure.
|
||||
|
|
|
|||
|
|
@ -12,3 +12,25 @@
|
|||
- `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)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
- `src/utils/message.ts`: Global message utility.
|
||||
- `src/service/request/index.ts`: Axios wrapper.
|
||||
- `src/service/api/auth.ts`: Authentication API.
|
||||
- `src/service/api/score.ts`: Score API.
|
||||
- `src/stores/user.ts`: User Pinia store.
|
||||
- `src/stores/score.ts`: Score Pinia store.
|
||||
- `src/components/ui/WMessage.vue`: Message component UI.
|
||||
- `src/components/TheNavigation.vue`: Main navigation component.
|
||||
- `src/components/ScoreForm.vue`: Score editing form.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
# Project Tasks
|
||||
|
||||
- [ ] [Task 1] Global Message Component, API Encapsulation, and Login/Logout Integration <!-- id: 0 -->
|
||||
- [ ] Check/Implement Global Message Component <!-- id: 1 -->
|
||||
- [ ] Install axios <!-- id: 2 -->
|
||||
- [ ] Create `src/service/request/index.ts` <!-- id: 3 -->
|
||||
- [ ] Create `src/service/api/auth.ts` <!-- id: 4 -->
|
||||
- [ ] Update `src/stores/user.ts` (Pinia + Persistence) <!-- id: 5 -->
|
||||
- [ ] Integrate Login/Logout in `TheNavigation.vue` <!-- id: 6 -->
|
||||
- [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 -->
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ declare global {
|
|||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||
const useScoreStore: typeof import('./stores/score')['useScoreStore']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
|
|
@ -570,6 +571,7 @@ declare module 'vue' {
|
|||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||
readonly useScoreStore: UnwrapRef<typeof import('./stores/score')['useScoreStore']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ export {}
|
|||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BackToTop: typeof import('./components/BackToTop.vue')['default']
|
||||
copy: typeof import('./components/ScoreForm copy.vue')['default']
|
||||
FilterBar: typeof import('./components/FilterBar.vue')['default']
|
||||
LoginForm: typeof import('./components/LoginForm.vue')['default']
|
||||
README: typeof import('./components/README.md')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
import { type LoginParams, type LoginResult } from '~/service/api/auth'
|
||||
import message from '~/utils/message'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', data: LoginResult): void
|
||||
}>()
|
||||
|
||||
// Score inputs
|
||||
const loginForm = ref<LoginParams>({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// Errors state
|
||||
const error = ref<string>('')
|
||||
|
||||
async function handleLogin() {
|
||||
if (!loginForm.value.username || !loginForm.value.password) {
|
||||
error.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
try {
|
||||
let loginResult = await userStore.login(loginForm.value)
|
||||
console.warn(loginResult)
|
||||
if (loginResult.token && loginResult.user) {
|
||||
message.success('登录成功')
|
||||
// 抛出事件
|
||||
emit('confirm', loginResult)
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '登录失败'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="exam-form-container">
|
||||
<!-- Modal Content -->
|
||||
<!-- <div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
登录
|
||||
</h2>
|
||||
<button
|
||||
class="text-2xl text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
@click="closeLoginModal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<div v-if="error" class="mb-4 text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">用户名</label>
|
||||
<input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入用户名"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">密码</label>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2 space-x-3">
|
||||
<button
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 你的样式代码 */
|
||||
</style>
|
||||
|
|
@ -26,7 +26,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
// --- Form State ---
|
||||
const examType = ref('历史组')
|
||||
const examType = ref('')
|
||||
const selectedElectives = ref<string[]>([])
|
||||
const majorCategory = ref('')
|
||||
const selectedSubMajors = ref<string[]>([])
|
||||
|
|
@ -260,7 +260,14 @@ function initForm() {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!scoreStore.scoreInfo) {
|
||||
// scoreStore.fetchScore().catch(() => {
|
||||
// // 忽略错误,可能用户未设置成绩
|
||||
// })
|
||||
}
|
||||
else {
|
||||
initForm()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => scoreStore.scoreInfo, () => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { ScoreFormData } from './ScoreForm.vue'
|
||||
import { useWindowScroll } from '@vueuse/core' // 假设你使用了 VueUse
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useScoreStore } from '../stores/score'
|
||||
import { LoginResult } from '~/service/api/auth'
|
||||
import message from '~/utils/message'
|
||||
import { logout as apiLogout } from '~/service/api/auth'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const scoreStore = useScoreStore()
|
||||
|
|
@ -14,9 +14,6 @@ const isLoginModalOpen = ref(false)
|
|||
// 新增:控制移动端菜单开关的状态
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
// 获取路由实例
|
||||
const router = useRouter()
|
||||
// 获取当前路由信息
|
||||
|
|
@ -69,35 +66,25 @@ watch(y, (newY, oldY) => {
|
|||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// ===========================
|
||||
// 处理登录提交的回调
|
||||
function handleLoginConfirm(data: LoginResult) {
|
||||
console.warn('接收到登录数据:', data)
|
||||
if(data.user && data.token){
|
||||
closeLoginModal()
|
||||
}
|
||||
}
|
||||
|
||||
function openLoginModal() {
|
||||
isLoginModalOpen.value = true
|
||||
// 打开登录弹窗时,顺便关闭移动端菜单
|
||||
isMobileMenuOpen.value = false
|
||||
username.value = ''
|
||||
password.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function closeLoginModal() {
|
||||
isLoginModalOpen.value = false
|
||||
username.value = ''
|
||||
password.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!username.value || !password.value) {
|
||||
error.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
try {
|
||||
await userStore.login({ username: username.value, password: password.value })
|
||||
message.success('登录成功')
|
||||
closeLoginModal()
|
||||
router.push('/')
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '登录失败'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
|
|
@ -121,6 +108,29 @@ function toggleMobileMenu() {
|
|||
function handleMobileLinkClick() {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
|
||||
// --- Init ---
|
||||
function initScore() {
|
||||
console.warn('initScore', scoreStore.scoreInfo)
|
||||
if (scoreStore.scoreInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!scoreStore.scoreInfo) {
|
||||
scoreStore.fetchScore().catch(() => {
|
||||
// 忽略错误,可能用户未设置成绩
|
||||
})
|
||||
}
|
||||
else {
|
||||
initScore()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => scoreStore.scoreInfo, () => {
|
||||
initScore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -171,9 +181,9 @@ function handleMobileLinkClick() {
|
|||
|
||||
<div v-else class="flex items-center space-x-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">音乐类</span>
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">{{ scoreStore.scoreInfo?.professionalCategory || '' }}</span>
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">文化成绩</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">334分</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">{{ scoreStore.scoreInfo?.culturalScore || '??' }}分</span>
|
||||
<a target="_blank" class="cursor-pointer text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300" @click="openScoreFormModal">
|
||||
<div i-carbon:edit class="text-xs" />
|
||||
</a>
|
||||
|
|
@ -292,66 +302,24 @@ function handleMobileLinkClick() {
|
|||
</nav>
|
||||
|
||||
<!-- Login Modal (Z-index 调高,确保覆盖导航栏) -->
|
||||
<div
|
||||
v-if="isLoginModalOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 px-4"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<div v-if="isLoginModalOpen" class="fixed inset-0 z-50 flex select-none items-center justify-center bg-black bg-opacity-50 px-4">
|
||||
<div class="max-w-md w-full border rounded-lg bg-white p-6 shadow-xl transition-all dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
<h3 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
登录
|
||||
</h2>
|
||||
</h3>
|
||||
<button
|
||||
class="text-2xl text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
class="text-3xl text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
@click="closeLoginModal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mb-4 text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
<!-- 使用组件并监听 confirm 事件 -->
|
||||
<LoginForm @confirm="handleLoginConfirm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">用户名</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入用户名"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">密码</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2 space-x-3">
|
||||
<button
|
||||
class="rounded-md bg-gray-200 px-4 py-2 text-sm text-gray-800 font-medium transition-colors dark:bg-gray-700 hover:bg-gray-300 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="closeLoginModal"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isScoreModalOpen" class="fixed inset-0 z-50 flex select-none items-center justify-center bg-black bg-opacity-50 px-4">
|
||||
<div class="max-w-md w-full border rounded-lg bg-white p-6 shadow-xl transition-all dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -35,5 +35,5 @@ export function saveScore(data: SaveScoreRequest) {
|
|||
}
|
||||
|
||||
export function getScore() {
|
||||
return request.get<ScoreInfo>('/user/score')
|
||||
return request.get<ScoreInfo>('/user/score', {params: { educationalLevel: '' }, showLoading: false})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ class Request {
|
|||
case 404:
|
||||
msg = backendMsg || 'Not Found'
|
||||
break
|
||||
case 429:
|
||||
msg = backendMsg || 'Too Many Requests'
|
||||
break
|
||||
case 500:
|
||||
msg = backendMsg || 'Internal Server Error'
|
||||
break
|
||||
|
|
|
|||
|
|
@ -55,8 +55,10 @@ export const useUserStore = defineStore('user', () => {
|
|||
async function login(params: LoginParams) {
|
||||
try {
|
||||
const data = await apiLogin(params)
|
||||
if (data.token && data.user) {
|
||||
setToken(data.token)
|
||||
setUserInfo(data.user)
|
||||
}
|
||||
return data
|
||||
} catch (error) {
|
||||
throw error
|
||||
|
|
|
|||
|
|
@ -1,15 +1,64 @@
|
|||
# Task Detail
|
||||
|
||||
## 2025-12-18 Session 6
|
||||
## 2025-12-18 Session 7
|
||||
|
||||
### Execution Reason
|
||||
User asked how to update the navigation state after login.
|
||||
User requested to implement the score API (Task 2) and integrate it into the application.
|
||||
|
||||
### Execution Process
|
||||
1. **Investigation**: Checked `src/components/TheNavigation.vue` and `src/pages/index.vue`. Found they were accessing `userStore.user`, but the store (updated in Session 1) actually exposes `userStore.userInfo`.
|
||||
2. **Fix**: Replaced all occurrences of `userStore.user` with `userStore.userInfo` in both `TheNavigation.vue` and `index.vue`.
|
||||
3. **Result**: Since Pinia stores are reactive, correctly binding to `userStore.userInfo` ensures that when `userStore.login` updates `userInfo`, the UI (Navigation and Home page) automatically re-renders to show the logged-in state (username, logout button, etc.).
|
||||
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
|
||||
- Fixed the reactivity issue by correcting the property name from `user` to `userInfo`.
|
||||
- Navigation bar will now correctly toggle between "Login" and "User Profile" upon successful login.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
我需要你帮我参考#成绩修改api.md文件,根据接口文档内容。封装成绩相关的文件,用于封装每个接口的请求方法,包括请求参数、请求方法、请求路径等。
|
||||
参考/src/service/api/auth.ts文件,封装一个src/service/api/score.ts文件及其相关的类型定义文件,用于封装成绩相关的接口请求方法,包括请求参数、请求方法、请求路径等。利用好pinia插件。
|
||||
之后在业务代码中调用成绩相关的接口请求方法,如获取当前用户的当前分数、修改当前用户的当前分数等。
|
||||
Loading…
Reference in New Issue