Files
App.k12.Parents/pages/register/scan-student.uvue
T
2026-10-29 10:08:39 +08:00

516 lines
10 KiB
Plaintext

<template>
<view class="page-container">
<scroll-view class="content-scroll" direction="vertical">
<!-- 步骤进度 -->
<view class="step-indicator">
<view class="step-item step-active">
<view class="step-dot step-dot-active"></view>
<text class="step-text step-text-active">扫码绑定孩子</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-dot"></view>
<text class="step-text">家长账号与密码</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-dot"></view>
<text class="step-text">注册完成</text>
</view>
</view>
<!-- 提示信息 -->
<view class="info-banner">
<text class="info-text">先扫描孩子的学生二维码,或输入学生账号完成绑定。再设置家长账号,最多可绑定 3 个孩子。</text>
</view>
<!-- 切换按钮 -->
<view class="toggle-buttons">
<view class="toggle-btn toggle-btn-active toggle-btn-first" @tap="handleTabChange('scan')">
<text class="toggle-btn-text toggle-btn-text-active">扫码绑定</text>
</view>
<view class="toggle-btn toggle-btn-inactive" @tap="handleTabChange('input')">
<text class="toggle-btn-text toggle-btn-text-inactive">输入学生账号</text>
</view>
</view>
<!-- 扫码区域 -->
<view class="scan-section">
<view class="scan-status">
<text class="status-text">对准学生二维码</text>
</view>
<view class="scan-area">
<QrScanner :active="studentList.length < MAX_STUDENTS" @scan="handleScannedQRCode" @error="handleCameraError"></QrScanner>
</view>
<text class="scan-hint">将学生二维码放入框内,即可自动识别</text>
<text class="scan-note">学生账号/密码可以在学校指定平台或在班主任或家长群可获取</text>
</view>
<!-- 已绑定学生列表 -->
<view class="student-list" v-if="studentList.length > 0">
<view class="list-title-row">
<text class="list-title">已绑定学生</text>
<uni-badge-view :text="studentList.length + '/3'" badge-class="student-count-badge"></uni-badge-view>
</view>
<view class="student-item" v-for="(student, index) in studentList" :key="student.id">
<image class="student-avatar" :src="student.avatar || '/static/avatar-default.png'" mode="aspectFill"></image>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
<text class="student-detail">{{ student.grade }} {{ student.class }} | {{ student.account }}</text>
</view>
<button class="remove-btn" @tap="handleRemoveStudent(index)">移除</button>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="footer-button-container">
<view
class="footer-button"
:class="canNext ? '' : 'footer-button-disabled'"
@tap="handleNext"
>
<text class="footer-button-text" :class="canNext ? '' : 'footer-button-text-disabled'">下一步</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { apiVerifyStudentQRCode } from '../../api/student'
import { registrationStudents } from '../../store/registration'
import QrScanner from '../../components/qr-scanner.uvue'
const studentList = registrationStudents
const MAX_STUDENTS = 3
const isVerifying = ref(false)
const canNext = computed((): boolean => {
return studentList.value.length > 0
})
/**
* 切换标签页
*/
function handleTabChange(tab: string) {
if (tab === 'input') {
uni.redirectTo({
url: '/pages/register/input-student'
})
}
}
async function handleScannedQRCode(qrCode: string) {
if (!qrCode || isVerifying.value) return
const separatorIndex = qrCode.indexOf(',')
if (separatorIndex <= 0 || separatorIndex === qrCode.length - 1) {
uni.showToast({
title: '二维码格式无效',
icon: 'none'
})
return
}
const account = qrCode.slice(0, separatorIndex).trim()
const name = qrCode.slice(separatorIndex + 1).trim()
if (!account || !name) return
isVerifying.value = true
try {
await verifyStudentQRCode(qrCode, account, name)
} finally {
isVerifying.value = false
}
}
function handleCameraError() {
uni.showToast({
title: '无法使用摄像头,请检查权限',
icon: 'none'
})
}
/**
* 验证学生二维码
*/
async function verifyStudentQRCode(qrCode: string, account: string, name: string) {
// 检查是否达到上限
if (studentList.value.length >= MAX_STUDENTS) {
uni.showToast({
title: `最多只能绑定${MAX_STUDENTS}个孩子`,
icon: 'none'
})
return
}
if (studentList.value.some(student => student.account === account)) {
uni.showToast({
title: '该学生已添加',
icon: 'none'
})
return
}
uni.showLoading({ title: '验证中...' })
const res = await apiVerifyStudentQRCode({ qrCode, account, name })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// 检查是否重复绑定
const exists = studentList.value.find(s => s.account === res.data!.account)
if (exists) {
uni.showToast({
title: '该学生已添加',
icon: 'none'
})
return
}
// 添加到列表
studentList.value.push(res.data)
uni.showToast({
title: '绑定成功',
icon: 'success'
})
} else {
uni.showToast({
title: res?.message || '学生信息验证失败',
icon: 'none'
})
}
}
/**
* 移除学生
*/
function handleRemoveStudent(index: number) {
uni.showModal({
title: '提示',
content: '确定要移除该学生吗?',
success: (res) => {
if (res.confirm) {
studentList.value.splice(index, 1)
uni.showToast({
title: '已移除',
icon: 'success'
})
}
}
})
}
/**
* 下一步
*/
function handleNext() {
if (!canNext.value) {
uni.showToast({
title: '请至少绑定一个学生',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/register/parent-account'
})
}
</script>
<style>
.page-container {
display: flex;
flex-direction: column;
flex: 1;
background-color: #F5F5F5;
}
.content-scroll {
flex: 1;
padding: 16px 16px calc(96px + var(--uni-safe-area-inset-bottom));
}
/* 步骤指示器 */
.step-indicator {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px;
background-color: #FFFFFF;
border-radius: 8px;
margin-bottom: 16px;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: #E5E5E5;
margin-bottom: 8px;
}
.step-dot-active {
background-color: #6C63FF;
width: 12px;
height: 12px;
border-radius: 6px;
}
.step-text {
font-size: 11px;
color: #999999;
text-align: center;
}
.step-text-active {
color: #6C63FF;
font-weight: 600;
}
.step-line {
flex: 1;
height: 2px;
background-color: #E5E5E5;
margin: 0 4px;
margin-bottom: 20px;
}
/* 提示信息 */
.info-banner {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 12px;
background-color: #EEF2FF;
border-radius: 8px;
margin-bottom: 16px;
}
.info-icon {
font-size: 16px;
margin-right: 8px;
flex-shrink: 0;
}
.info-text {
flex: 1;
font-size: 13px;
color: #4F46E5;
line-height: 1.6;
}
/* 切换按钮 */
.toggle-buttons {
display: flex;
flex-direction: row;
margin-bottom: 24px;
}
.toggle-btn-first {
margin-right: 12px;
}
.toggle-btn {
flex: 1;
height: 40px;
border-radius: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.toggle-btn-active {
background-color: #6C63FF;
}
.toggle-btn-inactive {
background-color: #FFFFFF;
border: 1px solid #E5E5E5;
}
.toggle-btn-text {
font-size: 14px;
}
.toggle-btn-text-active {
color: #FFFFFF;
}
.toggle-btn-text-inactive {
color: #666666;
}
/* 扫码区域 */
.scan-section {
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
.scan-status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #1E293B;
border-radius: 8px 8px 0 0;
}
.status-text {
font-size: 14px;
color: #FFFFFF;
}
.status-icon {
font-size: 18px;
}
.scan-area {
background-color: #0F172A;
padding: 16px;
border-radius: 0 0 8px 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.scan-hint {
font-size: 14px;
color: #F8FAFC;
text-align: center;
margin-top: 16px;
}
.scan-note {
font-size: 12px;
color: #94A3B8;
text-align: center;
margin-top: 8px;
line-height: 1.5;
}
/* 学生列表 */
.student-list {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.list-title {
font-size: 14px;
font-weight: 600;
color: #333333;
margin-bottom: 12px;
}
.list-title-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.student-count-badge {
background-color: #6C63FF;
color: #FFFFFF;
}
.student-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px;
background-color: #FFFFFF;
border-radius: 8px;
margin-bottom: 8px;
}
.student-avatar {
width: 48px;
height: 48px;
border-radius: 24px;
margin-right: 12px;
background-color: #F5F5F5;
}
.student-info {
display: flex;
flex-direction: column;
flex: 1;
}
.student-name {
font-size: 16px;
font-weight: 600;
color: #333333;
margin-bottom: 4px;
}
.student-detail {
font-size: 12px;
color: #999999;
}
.remove-btn {
width: 56px;
height: 32px;
padding: 0;
font-size: 12px;
color: #EF4444;
background-color: #FEF2F2;
}
/* 底部按钮 */
.footer-button-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 16px calc(16px + var(--uni-safe-area-inset-bottom));
background-color: #F5F5F5;
}
.footer-button {
height: 48px;
background-color: #6C63FF;
border-radius: 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.footer-button-disabled {
background-color: #E5E5E5;
}
.footer-button-text {
font-size: 16px;
color: #FFFFFF;
font-weight: 500;
}
.footer-button-text-disabled {
color: #999999;
}
</style>