Initial commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view class="content" direction="vertical">
|
||||
<view class="intro">
|
||||
<text class="title">验证孩子的学生信息</text>
|
||||
<text class="description">扫码或输入学生账号和姓名进行验证。验证不等于绑定,新增绑定仍需接入真实接口。</text>
|
||||
</view>
|
||||
<view class="card">
|
||||
<text class="section-title">已绑定学生</text>
|
||||
<text v-if="loading" class="hint">正在加载已绑定列表...</text>
|
||||
<view v-else-if="loadFailed" class="retry" @tap="loadStudents"><text class="retry-text">列表加载失败,点击重试</text></view>
|
||||
<text v-else class="hint">当前 {{ students.length }} / 3 名{{ students.length >= 3 ? ',已达到上限' : '' }}</text>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="mode === 'scan' ? 'tab-active' : ''" @tap="changeMode('scan')"><text class="tab-text" :class="mode === 'scan' ? 'tab-text-active' : ''">扫码验证</text></view>
|
||||
<view class="tab" :class="mode === 'manual' ? 'tab-active' : ''" @tap="changeMode('manual')"><text class="tab-text" :class="mode === 'manual' ? 'tab-text-active' : ''">手动输入</text></view>
|
||||
</view>
|
||||
<view v-if="mode === 'scan'" class="form">
|
||||
<text class="section-title">对准学生二维码</text>
|
||||
<view v-if="visible && !loading && !loadFailed && !verified" class="scanner"><QrScanner @scan="handleScan" @error="handleCameraError"></QrScanner></view>
|
||||
<text class="hint">二维码格式为学生账号,学生姓名。请允许使用摄像头。</text>
|
||||
</view>
|
||||
<view v-else class="form">
|
||||
<text class="label">学生账号</text>
|
||||
<input class="input" v-model="account" placeholder="请输入学生账号" />
|
||||
<text class="label">学生姓名</text>
|
||||
<input class="input" v-model="name" placeholder="请输入学生姓名" />
|
||||
<view class="submit" @tap="handleManual"><text class="submit-text">{{ verifying ? '验证中...' : '验证学生' }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="verified" class="result">
|
||||
<text class="result-title">已验证,尚未绑定</text>
|
||||
<text class="result-detail">{{ verified.name }} · {{ verified.account }}</text>
|
||||
<text class="result-detail">当前没有登录后新增绑定接口,需接入真实接口才能完成绑定。验证结果可能来自项目配置的 mock 服务。</text>
|
||||
<view class="retry" @tap="verified = null"><text class="retry-text">验证其他学生</text></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import QrScanner from '../../components/qr-scanner.uvue'
|
||||
import { apiGetBoundStudents, apiVerifyStudentQRCode, apiVerifyStudentAccount, type BoundStudentSummary, type StudentInfo } from '../../api/student'
|
||||
|
||||
const students = ref<BoundStudentSummary[]>([])
|
||||
const loading = ref(true)
|
||||
const loadFailed = ref(false)
|
||||
const verifying = ref(false)
|
||||
const visible = ref(false)
|
||||
const mode = ref('scan')
|
||||
const account = ref('')
|
||||
const name = ref('')
|
||||
const verified = ref<StudentInfo | null>(null)
|
||||
|
||||
onLoad((options) => { if (options?.mode === 'manual') mode.value = 'manual'; loadStudents() })
|
||||
onShow(() => { visible.value = true })
|
||||
onHide(() => { visible.value = false })
|
||||
|
||||
async function loadStudents() {
|
||||
loading.value = true
|
||||
loadFailed.value = false
|
||||
try {
|
||||
const res = await apiGetBoundStudents()
|
||||
if (res && res.code === 0 && res.data) students.value = res.data.students
|
||||
else loadFailed.value = true
|
||||
} catch (_error) { loadFailed.value = true }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function changeMode(value: string) { if (!verifying.value) { mode.value = value; verified.value = null } }
|
||||
function handleCameraError() { uni.showToast({ title: '无法使用摄像头,请检查权限', icon: 'none' }) }
|
||||
function handleScan(qrCode: string) {
|
||||
const separator = qrCode.indexOf(',')
|
||||
const studentAccount = separator > 0 ? qrCode.slice(0, separator).trim() : ''
|
||||
const studentName = separator > 0 ? qrCode.slice(separator + 1).trim() : ''
|
||||
if (!studentAccount || !studentName) { uni.showToast({ title: '二维码格式无效', icon: 'none' }); return }
|
||||
verify(studentAccount, studentName, qrCode)
|
||||
}
|
||||
function handleManual() {
|
||||
const studentAccount = account.value.trim()
|
||||
const studentName = name.value.trim()
|
||||
if (!studentAccount || !studentName) { uni.showToast({ title: '请输入学生账号和姓名', icon: 'none' }); return }
|
||||
if (!/^[\u4e00-\u9fa5]{2,10}$/.test(studentName)) { uni.showToast({ title: '姓名需为2至10个汉字', icon: 'none' }); return }
|
||||
verify(studentAccount, studentName, '')
|
||||
}
|
||||
async function verify(studentAccount: string, studentName: string, qrCode: string) {
|
||||
if (verifying.value) return
|
||||
verified.value = null
|
||||
if (loading.value || loadFailed.value) { uni.showToast({ title: '请先加载已绑定学生列表', icon: 'none' }); return }
|
||||
if (students.value.some(item => item.account === studentAccount)) { uni.showToast({ title: '该学生已绑定', icon: 'none' }); return }
|
||||
if (students.value.length >= 3) { uni.showToast({ title: '最多绑定3名学生', icon: 'none' }); return }
|
||||
verifying.value = true
|
||||
try {
|
||||
const res = qrCode ? await apiVerifyStudentQRCode({ qrCode, account: studentAccount, name: studentName }) : await apiVerifyStudentAccount({ account: studentAccount, name: studentName })
|
||||
if (res && res.code === 0 && res.data) {
|
||||
if (students.value.some(item => item.id === res.data!.id || item.account === res.data!.account)) uni.showToast({ title: '该学生已绑定', icon: 'none' })
|
||||
else verified.value = res.data
|
||||
}
|
||||
} catch (_error) { uni.showToast({ title: '学生信息验证失败', icon: 'none' }) }
|
||||
finally { verifying.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
|
||||
.content { flex: 1; }
|
||||
.intro { display: flex; flex-direction: column; margin: 20px 14px 0; padding: 20px 16px; border-radius: 16px; background-color: #6542eb; }
|
||||
.title { font-size: 18px; font-weight: 700; color: #ffffff; }
|
||||
.description { margin-top: 8px; font-size: 12px; line-height: 20px; color: #eee9ff; }
|
||||
.card { display: flex; flex-direction: column; margin: 14px 14px 0; padding: 16px; border-radius: 16px; background-color: #ffffff; }
|
||||
.section-title { font-size: 15px; font-weight: 700; color: #242b3d; }
|
||||
.hint { margin-top: 10px; font-size: 12px; line-height: 19px; color: #8a99ae; }
|
||||
.retry { margin-top: 12px; }
|
||||
.retry-text { font-size: 12px; color: #554be9; }
|
||||
.tabs { display: flex; flex-direction: row; padding: 4px; border-radius: 12px; background-color: #f1f4f8; }
|
||||
.tab { display: flex; flex-direction: column; flex: 1; align-items: center; justify-content: center; height: 38px; border-radius: 9px; }
|
||||
.tab-active { background-color: #554be9; }
|
||||
.tab-text { font-size: 13px; color: #65758b; }
|
||||
.tab-text-active { color: #ffffff; }
|
||||
.form { display: flex; flex-direction: column; margin-top: 18px; }
|
||||
.scanner { margin-top: 12px; }
|
||||
.label { margin-top: 12px; font-size: 13px; color: #242b3d; }
|
||||
.input { height: 44px; margin-top: 8px; padding: 0 12px; border-radius: 10px; font-size: 14px; background-color: #f5f7fb; }
|
||||
.submit { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 44px; margin-top: 20px; border-radius: 11px; background-color: #554be9; }
|
||||
.submit-text { font-size: 14px; color: #ffffff; }
|
||||
.result { display: flex; flex-direction: column; margin: 14px 14px 24px; padding: 16px; border-radius: 16px; background-color: #ffffff; }
|
||||
.result-title { font-size: 15px; font-weight: 700; color: #554be9; }
|
||||
.result-detail { margin-top: 8px; font-size: 12px; line-height: 20px; color: #65758b; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user