516 lines
10 KiB
Plaintext
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>
|