Grade_Inquiry/src/views/HomePage.vue
2026-02-01 21:13:23 +08:00

534 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
/**
* 首页组件
* 成绩查询入口页面,包含身份证输入和查询功能
*/
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { queryScoreByIdCard, getExamBatchList } from '@/api/score'
import Toast from '@/components/common/Toast.vue'
import PuzzleCaptcha from '@/components/common/PuzzleCaptcha.vue'
import type { ExamBatch } from '@/types'
// 路由实例
const router = useRouter()
// 响应式状态
/** 考试批次列表 */
const examBatchList = ref<ExamBatch[]>([])
/** 选中的考试批次ID */
const selectedBatchId = ref('')
/** 是否正在加载批次列表 */
const isBatchLoading = ref(false)
/** 下拉框是否展开 */
const isDropdownOpen = ref(false)
/** 身份证号输入值 */
const idCard = ref('')
/** 学生姓名输入值 */
const studentName = ref('')
/** 是否正在加载 */
const isLoading = ref(false)
/** 输入框是否获得焦点 */
const isFocused = ref(false)
const isNameFocused = ref(false)
/** 验证错误信息 */
const errorMessage = ref('')
/** Toast 显示状态 */
const showToast = ref(false)
/** Toast 消息 */
const toastMessage = ref('')
/** Toast 类型 */
const toastType = ref<'info' | 'success' | 'warning' | 'error'>('error')
/** 是否显示验证码 */
const showCaptcha = ref(false)
/**
* 获取选中的批次名称
*/
const selectedBatchName = computed(() => {
const batch = examBatchList.value.find(b => b.id === selectedBatchId.value)
return batch?.name || '请选择考试批次'
})
/**
* 页面加载时获取考试批次列表
*/
onMounted(async () => {
await loadExamBatchList()
})
/**
* 加载考试批次列表
*/
const loadExamBatchList = async () => {
isBatchLoading.value = true
try {
const response = await getExamBatchList()
if (response.code === 200 && response.data?.rows) {
examBatchList.value = response.data.rows
// 默认选中第一个批次
if (response.data.rows.length > 0) {
selectedBatchId.value = response.data.rows[0].id
}
}
} catch (error) {
console.error('Failed to load exam batch list:', error)
showMessage('加载考试批次失败', 'error')
} finally {
isBatchLoading.value = false
}
}
/**
* 选择考试批次
*/
const selectBatch = (batch: ExamBatch) => {
selectedBatchId.value = batch.id
isDropdownOpen.value = false
}
/**
* 切换下拉框展开状态
*/
const toggleDropdown = () => {
if (!isBatchLoading.value && examBatchList.value.length > 0) {
isDropdownOpen.value = !isDropdownOpen.value
}
}
/**
* 点击外部关闭下拉框
*/
const closeDropdown = () => {
isDropdownOpen.value = false
}
/**
* 按钮是否可点击
*/
const isButtonDisabled = computed(() => {
return isLoading.value ||
idCard.value.trim().length === 0 ||
studentName.value.trim().length === 0 ||
!selectedBatchId.value
})
/**
* 身份证输入框样式类
*/
const inputClasses = computed(() => {
const base = 'input-field'
if (errorMessage.value && !idCard.value.trim()) {
return `${base} border-red-400 focus:border-red-400 focus:ring-red-100`
}
if (isFocused.value) {
return `${base} border-primary-400`
}
return base
})
/**
* 姓名输入框样式类
*/
const nameInputClasses = computed(() => {
const base = 'input-field'
if (errorMessage.value && !studentName.value.trim()) {
return `${base} border-red-400 focus:border-red-400 focus:ring-red-100`
}
if (isNameFocused.value) {
return `${base} border-primary-400`
}
return base
})
/**
* 处理输入变化
* 清除之前的错误提示,限制输入长度
*/
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
// 只允许数字和X
const value = target.value.replace(/[^0-9xX]/g, '').toUpperCase()
// 限制长度为18位
idCard.value = value.slice(0, 18)
// 清除错误信息
if (errorMessage.value) {
errorMessage.value = ''
}
}
/**
* 显示 Toast 提示
*/
const showMessage = (message: string, type: 'info' | 'success' | 'warning' | 'error' = 'error') => {
toastMessage.value = message
toastType.value = type
showToast.value = true
}
/**
* 处理查询点击
*/
const handleQueryClick = () => {
// 检查考试批次
if (!selectedBatchId.value) {
errorMessage.value = '请选择考试批次'
showMessage(errorMessage.value, 'warning')
return
}
// 简单非空检查
if (!studentName.value.trim()) {
errorMessage.value = '请输入学生姓名'
showMessage(errorMessage.value, 'warning')
return
}
if (!idCard.value.trim()) {
errorMessage.value = '请输入身份证号码'
showMessage(errorMessage.value, 'warning')
return
}
// 显示验证码
showCaptcha.value = true
}
/**
* 验证成功后执行查询
*/
const handleCaptchaSuccess = async () => {
// 开始加载
isLoading.value = true
errorMessage.value = ''
try {
// 使用选中的考试批次ID进行查询
const response = await queryScoreByIdCard(idCard.value, studentName.value, selectedBatchId.value)
if (response.code === 200 && response.data) {
// 查询成功,存储数据并跳转
sessionStorage.setItem('scoreResult', JSON.stringify(response.data))
router.push({
name: 'Result',
query: { id: idCard.value.slice(-4) },
})
} else {
// 查询失败
errorMessage.value = response.message || '查询失败,请稍后重试'
showMessage(errorMessage.value, 'error')
}
} catch (error) {
// 网络或其他错误
errorMessage.value = '网络错误,请检查网络连接后重试'
showMessage(errorMessage.value, 'error')
console.error('Query error:', error)
} finally {
isLoading.value = false
}
}
/**
* 处理键盘回车事件
*/
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !isButtonDisabled.value) {
handleQueryClick()
}
}
</script>
<template>
<div class="page-container safe-area-inset-bottom">
<!-- 验证码弹窗 -->
<PuzzleCaptcha
v-model:visible="showCaptcha"
@success="handleCaptchaSuccess"
/>
<!-- 页面头部 -->
<header class="text-center mb-8 pt-8">
<!-- 学校标识 -->
<div class="w-20 h-20 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center shadow-lg shadow-primary-200">
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
</div>
<!-- 标题 -->
<h1 class="text-2xl font-bold text-slate-800 mb-2">
德艺中学
</h1>
<p class="text-slate-500">
期末考试成绩查询
</p>
</header>
<!-- 查询表单卡片 -->
<div class="card">
<!-- 考试批次选择 -->
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 mb-2">
考试批次
</label>
<div class="relative" @click.stop>
<!-- 下拉按钮 -->
<button
type="button"
class="input-field w-full flex items-center justify-between cursor-pointer"
:class="{ 'border-primary-400 ring-2 ring-primary-100': isDropdownOpen }"
@click="toggleDropdown"
>
<span :class="selectedBatchId ? 'text-slate-800' : 'text-slate-400'">
<template v-if="isBatchLoading">
<span class="flex items-center gap-2">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
加载中...
</span>
</template>
<template v-else-if="examBatchList.length === 0">
暂无考试批次
</template>
<template v-else>
{{ selectedBatchName }}
</template>
</span>
<!-- 下拉箭头 -->
<svg
class="w-5 h-5 text-slate-400 transition-transform duration-200"
:class="{ 'rotate-180': isDropdownOpen }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- 下拉列表 -->
<Transition name="dropdown">
<div
v-if="isDropdownOpen"
class="absolute z-20 mt-1 w-full bg-white border border-slate-200 rounded-xl shadow-lg overflow-hidden"
>
<div class="max-h-60 overflow-y-auto scrollbar-thin">
<button
v-for="batch in examBatchList"
:key="batch.id"
type="button"
class="w-full px-4 py-3 text-left text-sm transition-colors cursor-pointer
hover:bg-primary-50 active:bg-primary-100"
:class="batch.id === selectedBatchId
? 'bg-primary-50 text-primary-600 font-medium'
: 'text-slate-700'"
@click="selectBatch(batch)"
>
<div class="flex items-center justify-between">
<span class="line-clamp-2">{{ batch.name }}</span>
<!-- 选中标记 -->
<svg
v-if="batch.id === selectedBatchId"
class="w-5 h-5 text-primary-500 flex-shrink-0 ml-2"
fill="currentColor"
viewBox="0 0 20 20"
>
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</button>
</div>
</div>
</Transition>
<!-- 点击外部关闭遮罩 -->
<div
v-if="isDropdownOpen"
class="fixed inset-0 z-10"
@click="closeDropdown"
/>
</div>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 mb-2">
学生姓名
</label>
<div class="relative">
<input
v-model="studentName"
:class="nameInputClasses"
type="text"
placeholder="请输入学生姓名"
maxlength="20"
autocomplete="off"
@focus="isNameFocused = true"
@blur="isNameFocused = false"
@keypress="handleKeyPress"
/>
<button
v-if="studentName.length > 0 && !isLoading"
class="absolute right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center transition-colors active:bg-slate-300"
@click="studentName = ''; errorMessage = ''"
>
<svg class="w-3.5 h-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 mb-2">
身份证号码
</label>
<!-- 输入框 -->
<div class="relative">
<input
:value="idCard"
:class="inputClasses"
type="text"
inputmode="numeric"
placeholder="请输入学生身份证号码"
maxlength="18"
autocomplete="off"
@input="handleInput"
@focus="isFocused = true"
@blur="isFocused = false"
@keypress="handleKeyPress"
/>
<!-- 清除按钮 -->
<button
v-if="idCard.length > 0 && !isLoading"
class="absolute right-3 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center transition-colors active:bg-slate-300"
@click="idCard = ''; errorMessage = ''"
>
<svg class="w-3.5 h-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- 错误提示 -->
<p v-if="errorMessage" class="mt-2 text-sm text-red-500 flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ errorMessage }}
</p>
<!-- 输入提示 -->
<p v-else class="mt-2 text-xs text-slate-400">
请输入18位身份证号码进行查询
</p>
</div>
<!-- 查询按钮 -->
<button
:class="['btn-primary', { 'opacity-60': isButtonDisabled }]"
:disabled="isButtonDisabled"
@click="handleQueryClick"
>
<span v-if="isLoading" class="flex items-center justify-center gap-2">
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
查询中...
</span>
<span v-else>查询成绩</span>
</button>
</div>
<!-- 使用说明 -->
<div class="mt-6 px-2">
<h3 class="text-sm font-medium text-slate-700 mb-3">
温馨提示
</h3>
<ul class="space-y-2 text-sm text-slate-500">
<li class="flex items-start gap-2">
<svg class="w-4 h-4 mt-0.5 text-primary-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>请使用学生本人身份证号进行查询</span>
</li>
<li class="flex items-start gap-2">
<svg class="w-4 h-4 mt-0.5 text-primary-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>成绩仅供参考如有疑问请联系班主任</span>
</li>
<li class="flex items-start gap-2">
<svg class="w-4 h-4 mt-0.5 text-primary-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>请妥善保管个人信息避免泄露</span>
</li>
</ul>
</div>
<!-- 底部版权信息 -->
<footer class="mt-auto pt-8 pb-4 text-center">
<p class="text-xs text-slate-400">
© 2025 德艺中学 版权所有
</p>
</footer>
<!-- Toast 提示 -->
<Toast
v-model:visible="showToast"
:message="toastMessage"
:type="toastType"
/>
</div>
</template>
<style scoped>
/* 下拉动画 */
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px);
}
/* 多行文本截断 */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 自定义滚动条样式 */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: #94a3b8;
}
</style>