首页、学情、任务绑定接口

This commit is contained in:
hexi
2026-11-06 16:43:41 +08:00
parent af73453035
commit 4a19192d73
16 changed files with 965 additions and 236 deletions
+43 -32
View File
@@ -16,11 +16,6 @@
</template>
<script setup lang="ts">
// #ifdef WEB
import { BrowserQRCodeReader } from '@zxing/browser'
import type { IScannerControls } from '@zxing/browser'
// #endif
type Props = {
cooldown?: number
active?: boolean
@@ -39,10 +34,10 @@
let lastScannedAt = 0
// #ifdef WEB
const videoContainerId = `qr-scanner-${Date.now()}`
let scannerControls: IScannerControls | null = null
let scanVideo: HTMLVideoElement | null = null
let scanStream: MediaStream | null = null
let isPageActive = false
let restartTimer: number | null = null
let scanTimer: number | null = null
// #endif
function emitScan(result: string) {
@@ -77,7 +72,7 @@
}
async function startWebScanner() {
if (!isPageActive || !props.active || scannerControls != null) return
if (!isPageActive || !props.active || scanStream != null) return
const video = getScanVideo()
if (video == null) {
handleCameraError()
@@ -85,39 +80,55 @@
}
try {
const reader = new BrowserQRCodeReader()
scannerControls = await reader.decodeFromConstraints(
{ video: { facingMode: 'environment' } },
video,
(result, _error, controls) => {
if (!result) return
controls.stop()
scannerControls = null
emitScan(result.getText())
scheduleRestart()
}
)
const BarcodeDetectorClass = (window as any).BarcodeDetector
if (BarcodeDetectorClass == null || navigator.mediaDevices == null) {
handleCameraError()
return
}
scanStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
video.srcObject = scanStream
await video.play()
const detector = new BarcodeDetectorClass({ formats: ['qr_code'] })
void scanWebFrame(detector)
} catch (_error) {
stopWebScanner()
handleCameraError()
}
}
function scheduleRestart() {
if (!isPageActive || !props.active || restartTimer != null) return
restartTimer = setTimeout(() => {
restartTimer = null
void startWebScanner()
}, props.cooldown)
async function scanWebFrame(detector: any) {
if (!isPageActive || !props.active || scanVideo == null || scanStream == null) return
try {
const results = await detector.detect(scanVideo)
if (results.length > 0) {
emitScan(results[0].rawValue as string)
}
} catch (_error) {
handleCameraError()
return
}
scanTimer = setTimeout(() => {
scanTimer = null
void scanWebFrame(detector)
}, 250)
}
function stopWebScanner() {
if (restartTimer != null) {
clearTimeout(restartTimer)
restartTimer = null
if (scanTimer != null) {
clearTimeout(scanTimer)
scanTimer = null
}
if (scannerControls != null) {
scannerControls.stop()
scannerControls = null
if (scanStream != null) {
scanStream.getTracks().forEach((track) => {
track.stop()
})
scanStream = null
}
if (scanVideo != null) {
scanVideo.srcObject = null
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<view class="activity-chart"><view class="activity-scale"><text class="activity-scale-text">时长(分)</text><text class="activity-scale-text">200</text><text class="activity-scale-text">100</text><text class="activity-scale-text">0</text></view><view class="activity-columns"><view v-for="point in points" :key="point.label" class="activity-column"><view class="activity-bar-wrap"><view class="activity-grid-line activity-grid-line-top"></view><view class="activity-grid-line activity-grid-line-middle"></view><view class="activity-grid-line activity-grid-line-bottom"></view><view class="activity-planned-bar" :style="{ height: point.plannedHeight + 'px' }"></view><view class="activity-study-bar" :style="{ height: point.studyHeight + 'px' }"></view><view v-if="point.newMasteredCount > 0" class="activity-mastery-point" :style="{ bottom: point.newMasteredHeight + 'px' }"><text class="activity-mastery-count">{{ point.newMasteredCount }}</text></view></view><text class="activity-day">{{ point.label }}</text></view></view><view class="activity-mastery-scale"><text class="activity-mastery-scale-text">新增掌握(个)</text><text class="activity-mastery-scale-text">3 个</text><text class="activity-mastery-scale-text">2 个</text><text class="activity-mastery-scale-text">1 个</text><text class="activity-mastery-scale-text">0 个</text></view></view>
<view class="activity-chart"><view class="activity-scale"><text class="activity-scale-text">时长(分)</text><text class="activity-scale-text">200</text><text class="activity-scale-text">100</text><text class="activity-scale-text">0</text></view><view class="activity-columns"><view v-for="point in points" :key="point.label" class="activity-column"><view class="activity-bar-wrap"><view class="activity-grid-line activity-grid-line-top"></view><view class="activity-grid-line activity-grid-line-middle"></view><view class="activity-grid-line activity-grid-line-bottom"></view><view class="activity-planned-bar" :style="{ height: point.plannedHeight + 'px' }"></view><view class="activity-study-bar" :style="{ height: point.studyHeight + 'px' }"></view><view v-if="point.newMasteredCount > 0" class="activity-mastery-point" :style="{ bottom: point.newMasteredHeight + 'px' }"><text class="activity-mastery-count">{{ point.newMasteredCount }}</text></view></view><text class="activity-day">{{ point.label }}</text></view></view><view class="activity-mastery-scale"><text class="activity-mastery-scale-text">未攻克(分)</text><text class="activity-mastery-scale-text">90</text><text class="activity-mastery-scale-text">60</text><text class="activity-mastery-scale-text">30</text><text class="activity-mastery-scale-text">0</text></view></view>
</template>
<script setup lang="ts">