This commit is contained in:
zhouwentao 2025-12-20 10:11:08 +08:00
parent 57f7937c2d
commit 256c34d5af
7 changed files with 174 additions and 18 deletions

View File

@ -1,5 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useScoreStore } from '~/stores/score'
import { type SaveScoreRequest } from '~/service/api/score'
import message from '~/utils/message'
const scoreStore = useScoreStore()
// //
export interface ScoreFormData { export interface ScoreFormData {
@ -25,6 +30,7 @@ const examType = ref('历史组')
const selectedElectives = ref<string[]>([]) const selectedElectives = ref<string[]>([])
const majorCategory = ref('') const majorCategory = ref('')
const selectedSubMajors = ref<string[]>([]) const selectedSubMajors = ref<string[]>([])
const isSubmitting = ref(false)
// Score inputs // Score inputs
const scores = ref({ const scores = ref({
@ -54,6 +60,8 @@ const electiveOptions = [
{ label: '政治', value: '政治' }, { label: '政治', value: '政治' },
{ label: '化学', value: '化学' }, { label: '化学', value: '化学' },
{ label: '生物', value: '生物' }, { label: '生物', value: '生物' },
{ label: '历史', value: '历史' },
{ label: '物理', value: '物理' },
] ]
const majorCategoryOptions = [ const majorCategoryOptions = [
@ -194,7 +202,7 @@ function validateForm() {
} }
// --- Submit --- // --- Submit ---
function handleSubmit() { async function handleSubmit() {
if (validateForm()) { if (validateForm()) {
// //
const formData: ScoreFormData = { const formData: ScoreFormData = {
@ -205,11 +213,60 @@ function handleSubmit() {
scores: { ...scores.value }, scores: { ...scores.value },
} }
// Transform to API request
const requestData: SaveScoreRequest = {
cognitioPolyclinic: examType.value === '历史组' ? '文科' : '理科',
subjectList: selectedElectives.value,
professionalCategory: majorCategory.value,
professionalCategoryChildren: selectedSubMajors.value,
professionalCategoryChildrenScore: {},
professionalScore: Number(scores.value.unified) || 0,
culturalScore: Number(scores.value.culture) || 0,
englishScore: Number(scores.value.english) || 0,
chineseScore: Number(scores.value.chinese) || 0,
province: '河南', // Default
}
try {
isSubmitting.value = true
await scoreStore.saveScore(requestData)
//
scoreStore.fetchScore()
message.success('保存成功')
// //
emit('confirm', formData) emit('confirm', formData)
} catch (e: any) {
message.error(e.message || '保存失败')
} finally {
isSubmitting.value = false
}
} }
} }
// --- Init ---
function initForm() {
console.warn('initForm', scoreStore.scoreInfo)
if (scoreStore.scoreInfo) {
const info = scoreStore.scoreInfo
examType.value = info.cognitioPolyclinic === '文科' ? '历史组' : '物理组'
selectedElectives.value = info.subjectList || []
majorCategory.value = info.professionalCategory || ''
selectedSubMajors.value = info.professionalCategoryChildren || []
scores.value.unified = info.professionalScore?.toString() || ''
scores.value.culture = info.culturalScore?.toString() || ''
scores.value.chinese = info.chineseScore?.toString() || ''
scores.value.english = info.englishScore?.toString() || ''
}
}
onMounted(() => {
initForm()
})
watch(() => scoreStore.scoreInfo, () => {
initForm()
})
// //
defineExpose({ defineExpose({
resetForm: () => { resetForm: () => {
@ -333,8 +390,8 @@ defineExpose({
v-model="scores.culture" v-model="scores.culture"
type="number" type="number"
min="0" min="0"
max="300" max="750"
placeholder="0-300" placeholder="0-750"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<div v-if="errors.scores.culture" class="mt-2 text-sm text-red-600"> <div v-if="errors.scores.culture" class="mt-2 text-sm text-red-600">
@ -377,7 +434,7 @@ defineExpose({
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<button <button :disabled="isSubmitting"
class="w-full rounded-full bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl" class="w-full rounded-full bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl"
@click="handleSubmit" @click="handleSubmit"
> >

View File

@ -2,11 +2,14 @@
import type { ScoreFormData } from './ScoreForm.vue' import type { ScoreFormData } from './ScoreForm.vue'
import { useWindowScroll } from '@vueuse/core' // 使 VueUse import { useWindowScroll } from '@vueuse/core' // 使 VueUse
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
import { useScoreStore } from '../stores/score'
import message from '~/utils/message' import message from '~/utils/message'
import { logout as apiLogout } from '~/service/api/auth'
const userStore = useUserStore() const userStore = useUserStore()
const scoreStore = useScoreStore()
const isLoginModalOpen = ref(false) const isLoginModalOpen = ref(false)
// //
const isMobileMenuOpen = ref(false) const isMobileMenuOpen = ref(false)
@ -259,10 +262,16 @@ function handleMobileLinkClick() {
</template> </template>
<template v-else> <template v-else>
<div class="mb-3 flex items-center px-3"> <div v-if="scoreStore.scoreInfo" class="mb-3 flex items-center px-3">
<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 pl-2 pr-2 text-sm text-gray-700 dark:text-gray-200">文化成绩</span> <span class="border-r-2 border-gray-3 pl-2 pr-2 text-sm text-gray-700 dark:text-gray-200">文化成绩</span>
<span class="pl-2 pr-2 text-sm text-gray-700 dark:text-gray-200">334</span> <span class="pl-2 pr-2 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>
</div>
<div v-else class="mb-3 flex items-center px-3">
<span class="text-sm text-gray-700 dark:text-gray-200">未设置分数</span>
<a target="_blank" class="cursor-pointer text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300" @click="openScoreFormModal"> <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" /> <div i-carbon:edit class="text-xs" />
</a> </a>

View File

@ -12,13 +12,13 @@ export interface LoginResult {
} }
export function login(params: LoginParams) { export function login(params: LoginParams) {
return request.post<LoginResult>('/auth/login', params) return request.post<LoginResult>('/user/auth/login', params)
} }
export function logout() { export function logout() {
return request.post<null>('/auth/logout') return request.post<null>('/user/auth/logout')
} }
export function getUserInfo() { export function getUserInfo() {
return request.get<UserInfo>('/auth/info') return request.get<UserInfo>('/user/auth/info')
} }

39
src/service/api/score.ts Normal file
View File

@ -0,0 +1,39 @@
import request from '../request'
export interface SaveScoreRequest {
cognitioPolyclinic: string
subjectList: string[]
professionalCategory: string
professionalCategoryChildren: string[]
professionalCategoryChildrenScore: Record<string, number>
professionalScore: number
culturalScore: number
englishScore: number
chineseScore: number
province: string
}
export interface ScoreInfo {
id: string
type: string
educationalLevel: string
cognitioPolyclinic: string
subjectList: string[]
professionalCategory: string
professionalCategoryChildren: string[]
professionalCategoryChildrenScore: Record<string, number>
professionalScore: number
culturalScore: number
englishScore: number
chineseScore: number
province: string
state: string
}
export function saveScore(data: SaveScoreRequest) {
return request.post<ScoreInfo>('/user/score/save-score', data)
}
export function getScore() {
return request.get<ScoreInfo>('/user/score')
}

View File

@ -4,6 +4,7 @@ import CryptoJS from 'crypto-js'
import { useUserStore } from '~/stores/user' import { useUserStore } from '~/stores/user'
import message from '~/utils/message' import message from '~/utils/message'
import { InternalAxiosRequestConfig } from 'axios' import { InternalAxiosRequestConfig } from 'axios'
interface CustomRequestConfig extends InternalAxiosRequestConfig { interface CustomRequestConfig extends InternalAxiosRequestConfig {
showLoading?: boolean, showLoading?: boolean,
showError?: boolean showError?: boolean
@ -81,7 +82,10 @@ class Request {
case 401: case 401:
msg = backendMsg || 'Unauthorized' msg = backendMsg || 'Unauthorized'
const userStore = useUserStore() const userStore = useUserStore()
userStore.logout() userStore.clearToken()
userStore.clearUserInfo()
window.location.href= '/'
// userStore.logout()
// router push login? // router push login?
break break
case 403: case 403:
@ -98,9 +102,9 @@ class Request {
} }
} }
if (config?.showError !== false) { // if (config?.showError !== false) {
message.error(msg) // message.error(msg)
} // }
// 返回包含具体错误信息的 Error 对象 // 返回包含具体错误信息的 Error 对象
return Promise.reject(new Error(msg)) return Promise.reject(new Error(msg))
} }

45
src/stores/score.ts Normal file
View File

@ -0,0 +1,45 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
import { getScore, saveScore as apiSaveScore, type SaveScoreRequest, type ScoreInfo } from '~/service/api/score'
export const useScoreStore = defineStore('score', () => {
const scoreInfo = ref<ScoreInfo | null>(null)
async function fetchScore() {
try {
const data = await getScore()
scoreInfo.value = data
return data
} catch (error) {
// If error occurs (e.g. 404 not found if user hasn't set score), we might want to handle it.
// For now, just throw or log.
console.error('Failed to fetch score', error)
throw error
}
}
async function saveScore(data: SaveScoreRequest) {
try {
const response = await apiSaveScore(data)
scoreInfo.value = response
return response
} catch (error) {
console.error('Failed to save score', error)
throw error
}
}
function clearScore() {
scoreInfo.value = null
}
return {
scoreInfo,
fetchScore,
saveScore,
clearScore,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useScoreStore as any, import.meta.hot))

View File

@ -81,6 +81,8 @@ export const useUserStore = defineStore('user', () => {
userInfo, userInfo,
setToken, setToken,
setUserInfo, setUserInfo,
clearToken,
clearUserInfo,
login, login,
logout, logout,
} }