534 lines
17 KiB
Vue
534 lines
17 KiB
Vue
<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>
|