|
@@ -1,527 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div class="container">
|
|
|
|
|
- <div v-if="step === 1">
|
|
|
|
|
- <div class="faceWindowWrapper">
|
|
|
|
|
- <div class="faceWindow" :style="{ borderColor: tagColor }">
|
|
|
|
|
- <video id="page_draw-video" muted playsinline></video>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="changeButton">
|
|
|
|
|
- <a-button type="primary" shape="circle"
|
|
|
|
|
- @click="state.constraints.video.facingMode === 'user' ? state.constraints.video.facingMode = 'environment' : state.constraints.video.facingMode = 'user'">
|
|
|
|
|
- <icon-sync />
|
|
|
|
|
- </a-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="faceInfo">
|
|
|
|
|
- <a-tag size="large" :color="tagColor">
|
|
|
|
|
- {{ tagInfo }}
|
|
|
|
|
- </a-tag>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="countTime" v-if="state.isRecording">
|
|
|
|
|
- <span>还需等待:</span>
|
|
|
|
|
- <span class="time">{{ recTime }}</span>
|
|
|
|
|
- <span>s</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="step === 2" class="spin">
|
|
|
|
|
- <a-spin dot tip="信息上传中,请勿离开此界面..." />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="step === 3">
|
|
|
|
|
- <a-result status="success" title="采集完成">
|
|
|
|
|
- <template #extra>
|
|
|
|
|
- <a-space>
|
|
|
|
|
- <a-button type='primary' @click="retry()">返回</a-button>
|
|
|
|
|
- </a-space>
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-result>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-if="step === 4">
|
|
|
|
|
- <a-result status="error" title="采集失败">
|
|
|
|
|
- <template #extra>
|
|
|
|
|
- <a-space>
|
|
|
|
|
- <a-button type='primary' @click="retry()">返回重试</a-button>
|
|
|
|
|
- </a-space>
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-result>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="userInfo">
|
|
|
|
|
- <div class="left">
|
|
|
|
|
- <a-avatar>
|
|
|
|
|
- <img :alt="props.userInfo.name ?? props.userInfo.student_num"
|
|
|
|
|
- :src="props.userInfo.user_avatar ?? 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
|
|
|
|
|
- </a-avatar>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="right">
|
|
|
|
|
- <div class="name">{{ props.userInfo.name }}</div>
|
|
|
|
|
- <div class="sub">{{ props.userInfo.academy_name }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div style="display: none">
|
|
|
|
|
- <div v-show="!state.netsLoadModel">
|
|
|
|
|
- <img id="page_draw-img-target" :src="props.userInfo.face_img" />
|
|
|
|
|
- <canvas id="page_draw-canvas-target"></canvas>
|
|
|
|
|
- <canvas id="page_draw-video-canvas"></canvas>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="loading" v-if="state.netsLoadModel">
|
|
|
|
|
- <div class="loadingTip">
|
|
|
|
|
- <icon-loading :size="32" />
|
|
|
|
|
- <div class="text">努力加载中</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup>
|
|
|
|
|
-import { Message, Modal } from "@arco-design/web-vue"
|
|
|
|
|
-import * as faceapi from "@vladmandic/face-api"
|
|
|
|
|
-import axios from 'axios'
|
|
|
|
|
-import storage from 'store'
|
|
|
|
|
-import { ref, onMounted, onUnmounted, reactive, watch } from "vue"
|
|
|
|
|
-
|
|
|
|
|
-const tagInfo = ref('请将面部完全放置在取景框中央')
|
|
|
|
|
-const tagColor = ref('blue')
|
|
|
|
|
-const step = ref(1)
|
|
|
|
|
-const recTime = ref(10)
|
|
|
|
|
-let isSuccessRecording = false
|
|
|
|
|
-let countdownTimer = null
|
|
|
|
|
-
|
|
|
|
|
-const props = defineProps({
|
|
|
|
|
- userInfo: {
|
|
|
|
|
- type: Object,
|
|
|
|
|
- required: true
|
|
|
|
|
- }
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-/**属性状态 */
|
|
|
|
|
-const state = reactive({
|
|
|
|
|
- netsLoadModel: true,
|
|
|
|
|
- netsType: "tinyFaceDetector",
|
|
|
|
|
- netsOptions: {
|
|
|
|
|
- ssdMobilenetv1: undefined,
|
|
|
|
|
- tinyFaceDetector: undefined,
|
|
|
|
|
- },
|
|
|
|
|
- faceMatcher: null,
|
|
|
|
|
- targetImgEl: null,
|
|
|
|
|
- targetCanvasEl: null,
|
|
|
|
|
- discernVideoEl: null,
|
|
|
|
|
- discernCanvasEl: null,
|
|
|
|
|
- timer: 0,
|
|
|
|
|
- constraints: {
|
|
|
|
|
- audio: false,
|
|
|
|
|
- video: {
|
|
|
|
|
- width: { min: 320, ideal: 720, max: 1280 },
|
|
|
|
|
- height: { min: 200, ideal: 480, max: 720 },
|
|
|
|
|
- frameRate: { min: 7, ideal: 15, max: 30 },
|
|
|
|
|
- facingMode: "user",
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- stream: null,
|
|
|
|
|
-
|
|
|
|
|
- /** 录制相关 */
|
|
|
|
|
- recorder: null,
|
|
|
|
|
- recordingChunks: [],
|
|
|
|
|
- recordingTimer: null,
|
|
|
|
|
- isRecording: false,
|
|
|
|
|
- lastBlob: null,
|
|
|
|
|
- failCount: 0,
|
|
|
|
|
- uploading: false, // 上传状态
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-/**启动录制 */
|
|
|
|
|
-function startRecording() {
|
|
|
|
|
- if (state.isRecording || !state.stream) return
|
|
|
|
|
- state.recordingChunks = []
|
|
|
|
|
- state.recorder = new MediaRecorder(state.stream, { mimeType: "video/webm" })
|
|
|
|
|
-
|
|
|
|
|
- state.recorder.ondataavailable = (e) => {
|
|
|
|
|
- if (e.data.size > 0) {
|
|
|
|
|
- state.recordingChunks.push(e.data)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 停止录制时
|
|
|
|
|
- state.recorder.onstop = () => {
|
|
|
|
|
- const blob = new Blob(state.recordingChunks, { type: "video/webm" })
|
|
|
|
|
-
|
|
|
|
|
- if (isSuccessRecording && blob) {
|
|
|
|
|
- uploadVideo(blob)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- state.recorder.start()
|
|
|
|
|
- state.isRecording = true
|
|
|
|
|
- recTime.value = 10 // ✅ 开始时重置倒计时
|
|
|
|
|
-
|
|
|
|
|
- // 倒计时逻辑
|
|
|
|
|
- countdownTimer = setInterval(() => {
|
|
|
|
|
- if (recTime.value > 0) {
|
|
|
|
|
- recTime.value -= 1
|
|
|
|
|
- }
|
|
|
|
|
- }, 1000)
|
|
|
|
|
-
|
|
|
|
|
- // 10秒后自动停止并标记为成功
|
|
|
|
|
- state.recordingTimer = setTimeout(() => {
|
|
|
|
|
- isSuccessRecording = true
|
|
|
|
|
- stopRecording() // ✅ 成功录制
|
|
|
|
|
- fnClose()
|
|
|
|
|
-
|
|
|
|
|
- step.value = 3
|
|
|
|
|
-
|
|
|
|
|
- }, recTime.value * 1000)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**结束录制 */
|
|
|
|
|
-function stopRecording() {
|
|
|
|
|
- if (state.recorder && state.isRecording) {
|
|
|
|
|
- state.recorder.stop()
|
|
|
|
|
- state.isRecording = false
|
|
|
|
|
-
|
|
|
|
|
- clearTimeout(state.recordingTimer)
|
|
|
|
|
- state.recordingTimer = null
|
|
|
|
|
-
|
|
|
|
|
- clearInterval(countdownTimer)
|
|
|
|
|
- countdownTimer = null
|
|
|
|
|
- recTime.value = 10
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**上传视频到服务器 */
|
|
|
|
|
-async function uploadVideo(blob) {
|
|
|
|
|
- try {
|
|
|
|
|
- step.value = 2
|
|
|
|
|
- const formData = new FormData()
|
|
|
|
|
- formData.append("student_num", props.userInfo.student_num)
|
|
|
|
|
- formData.append("key", props.userInfo.key)
|
|
|
|
|
- formData.append("upload", blob, "face_record.webm")
|
|
|
|
|
-
|
|
|
|
|
- const url = import.meta.env.VITE_APP_API_BASE_URL + '/UploadFaceVideo'
|
|
|
|
|
-
|
|
|
|
|
- const res = await axios.post(url, formData, {
|
|
|
|
|
- headers: { "Content-Type": "multipart/form-data" }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- if (!res || !res.data || res.data.code !== 0) {
|
|
|
|
|
- step.value = 4
|
|
|
|
|
- throw new Error(res?.data?.msg ?? '上传失败,请稍后再试')
|
|
|
|
|
- }
|
|
|
|
|
- Message.success("人脸数据上传成功")
|
|
|
|
|
- step.value = 3
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- step.value = 4
|
|
|
|
|
- Message.error(err.message ?? "人脸数据上传失败")
|
|
|
|
|
- console.error(err)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**重新开始录制 */
|
|
|
|
|
-function restartRecording() {
|
|
|
|
|
- stopRecording()
|
|
|
|
|
- // 小延迟确保上一次 recorder 彻底结束
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- startRecording()
|
|
|
|
|
- }, 200)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**初始化模型加载 */
|
|
|
|
|
-async function fnLoadModel() {
|
|
|
|
|
- const modelsPath = `https://lepao-api.xxoo365.top/models`
|
|
|
|
|
- await faceapi.nets.faceLandmark68Net.load(modelsPath)
|
|
|
|
|
- await faceapi.nets.faceRecognitionNet.load(modelsPath)
|
|
|
|
|
- await faceapi.nets.tinyFaceDetector.load(modelsPath)
|
|
|
|
|
- state.netsOptions.tinyFaceDetector = new faceapi.TinyFaceDetectorOptions({
|
|
|
|
|
- inputSize: 416,
|
|
|
|
|
- scoreThreshold: 0.5,
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- state.targetImgEl = document.getElementById("page_draw-img-target")
|
|
|
|
|
- state.discernVideoEl = document.getElementById("page_draw-video")
|
|
|
|
|
- state.discernCanvasEl = document.getElementById("page_draw-video-canvas")
|
|
|
|
|
-
|
|
|
|
|
- state.netsLoadModel = false
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**根据模型参数识别绘制--目标图 */
|
|
|
|
|
-async function fnRedrawTarget() {
|
|
|
|
|
- const detect = await faceapi
|
|
|
|
|
- .detectAllFaces(state.targetImgEl, state.netsOptions[state.netsType])
|
|
|
|
|
- .withFaceLandmarks()
|
|
|
|
|
- .withFaceDescriptors()
|
|
|
|
|
- if (!detect || detect.length === 0) {
|
|
|
|
|
- state.faceMatcher = null
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- state.faceMatcher = new faceapi.FaceMatcher(detect)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**根据模型参数识别绘制 */
|
|
|
|
|
-async function fnRedrawDiscern() {
|
|
|
|
|
- if (!state.faceMatcher) return
|
|
|
|
|
-
|
|
|
|
|
- if (state.discernVideoEl.paused) {
|
|
|
|
|
- clearTimeout(state.timer)
|
|
|
|
|
- state.timer = 0
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const detect = await faceapi
|
|
|
|
|
- .detectAllFaces(state.discernVideoEl, state.netsOptions[state.netsType])
|
|
|
|
|
- .withFaceLandmarks()
|
|
|
|
|
- .withFaceDescriptors()
|
|
|
|
|
-
|
|
|
|
|
- if (!detect || detect.length === 0) {
|
|
|
|
|
- state.failCount++
|
|
|
|
|
- if (state.failCount >= 3) {
|
|
|
|
|
- tagColor.value = 'blue'
|
|
|
|
|
- tagInfo.value = '请将面部完全放置在取景框中央'
|
|
|
|
|
- if (state.isRecording) stopRecording()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 延迟下一次识别,避免直接递归造成栈/CPU 问题
|
|
|
|
|
- clearTimeout(state.timer)
|
|
|
|
|
- state.timer = setTimeout(() => fnRedrawDiscern(), 300)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const dims = faceapi.matchDimensions(
|
|
|
|
|
- state.discernCanvasEl,
|
|
|
|
|
- state.discernVideoEl,
|
|
|
|
|
- true
|
|
|
|
|
- )
|
|
|
|
|
- const result = faceapi.resizeResults(detect, dims)
|
|
|
|
|
- // 若检测到多张脸,逐一判断(若任一匹配成功则开始录制)
|
|
|
|
|
- for (const item of result) {
|
|
|
|
|
- const descriptor = item.descriptor
|
|
|
|
|
- const best = state.faceMatcher.findBestMatch(descriptor)
|
|
|
|
|
-
|
|
|
|
|
- if (best) {
|
|
|
|
|
- // 临时加大匹配范围
|
|
|
|
|
- if (best._distance <= 0.9) {
|
|
|
|
|
- state.failCount = 0
|
|
|
|
|
- tagColor.value = 'green'
|
|
|
|
|
- tagInfo.value = '录制中,请确保面部不要离开取景框'
|
|
|
|
|
- if (!state.isRecording) startRecording()
|
|
|
|
|
- // 找到一个合格的人脸就可以停止遍历
|
|
|
|
|
- break
|
|
|
|
|
- } else {
|
|
|
|
|
- state.failCount = 3
|
|
|
|
|
- tagColor.value = 'red'
|
|
|
|
|
- tagInfo.value = `人脸匹配失败,请确保为 ${props?.userInfo?.name} 本人操作`
|
|
|
|
|
- if (state.isRecording) restartRecording()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 继续调度下一次识别(0ms 相当于下一事件循环)
|
|
|
|
|
- clearTimeout(state.timer)
|
|
|
|
|
- state.timer = setTimeout(() => fnRedrawDiscern(), 0)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**启动摄像头视频媒体 */
|
|
|
|
|
-async function fnOpen() {
|
|
|
|
|
- if (state.stream !== null) return
|
|
|
|
|
- try {
|
|
|
|
|
- state.stream = {}
|
|
|
|
|
- const stream = await navigator.mediaDevices.getUserMedia(state.constraints)
|
|
|
|
|
- state.stream = stream
|
|
|
|
|
- state.discernVideoEl.srcObject = stream
|
|
|
|
|
- await state.discernVideoEl.play()
|
|
|
|
|
- state.discernCanvasEl.width = state.discernVideoEl.videoWidth
|
|
|
|
|
- state.discernCanvasEl.height = state.discernVideoEl.videoHeight
|
|
|
|
|
- // 稍作延迟再开始识别
|
|
|
|
|
- setTimeout(() => fnRedrawDiscern(), 300)
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- fnClose()
|
|
|
|
|
- step.value = 4
|
|
|
|
|
- Message.error("视频媒体流获取错误: " + error)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**结束摄像头视频媒体 */
|
|
|
|
|
-function fnClose() {
|
|
|
|
|
- if (state.stream === null) return
|
|
|
|
|
- try {
|
|
|
|
|
- state.discernVideoEl.pause()
|
|
|
|
|
- state.discernVideoEl.srcObject = null
|
|
|
|
|
- state.stream.getTracks().forEach((track) => track.stop())
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.warn("关闭流时出错:", err)
|
|
|
|
|
- } finally {
|
|
|
|
|
- state.stream = null
|
|
|
|
|
- clearTimeout(state.timer)
|
|
|
|
|
- state.timer = 0
|
|
|
|
|
- stopRecording()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-watch(
|
|
|
|
|
- () => state.constraints.video.facingMode,
|
|
|
|
|
- () => {
|
|
|
|
|
- if (state.stream !== null) {
|
|
|
|
|
- fnClose()
|
|
|
|
|
- fnOpen()
|
|
|
|
|
- const videoEl = document.getElementById('page_draw-video')
|
|
|
|
|
- if (state.constraints.video.facingMode === 'user')
|
|
|
|
|
- videoEl.style.transform = 'scaleX(-1)'
|
|
|
|
|
- else
|
|
|
|
|
- videoEl.style.transform = 'none'
|
|
|
|
|
- } else {
|
|
|
|
|
- fnClose()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
-
|
|
|
|
|
-onMounted(() => {
|
|
|
|
|
- fnLoadModel().then(() => {
|
|
|
|
|
- fnRedrawTarget()
|
|
|
|
|
- fnOpen()
|
|
|
|
|
- isIosSafari()
|
|
|
|
|
- })
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-onUnmounted(() => {
|
|
|
|
|
- fnClose()
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-// IOS加载慢
|
|
|
|
|
-function isIosSafari() {
|
|
|
|
|
- const ua = window.navigator.userAgent
|
|
|
|
|
-
|
|
|
|
|
- // 1. 判断是否是 iOS 设备
|
|
|
|
|
- const isIOS = /iP(hone|od|ad)/.test(ua)
|
|
|
|
|
-
|
|
|
|
|
- if (isIOS) {
|
|
|
|
|
- const faceRecoIosShow = storage.get('faceRecoIosShow')
|
|
|
|
|
- if (!faceRecoIosShow)
|
|
|
|
|
- Modal.info({
|
|
|
|
|
- title: 'IOS设备采集提醒',
|
|
|
|
|
- escToClose: false,
|
|
|
|
|
- maskClosable: false,
|
|
|
|
|
- width: '300px',
|
|
|
|
|
- content: `部分IOS设备在首次采集时可能会遇到加载时间长或采集失败等情况。出现此类情况时可刷新页面重新采集。`,
|
|
|
|
|
- onOk: () => {
|
|
|
|
|
- storage.set('faceRecoIosShow', true)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function retry() {
|
|
|
|
|
- window.location.reload()
|
|
|
|
|
-}
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style lang="less" scoped>
|
|
|
|
|
-.container {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.faceWindowWrapper {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- width: 270px;
|
|
|
|
|
- height: 270px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 圆形视频框 */
|
|
|
|
|
-.faceWindow {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- border: 5px solid;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- position: relative;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 按钮 */
|
|
|
|
|
-.changeButton {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: -10px;
|
|
|
|
|
- /* 超出圆形框右边 */
|
|
|
|
|
- bottom: -10px;
|
|
|
|
|
- /* 超出圆形框底边 */
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.faceInfo {
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.countTime {
|
|
|
|
|
- margin-top: 10px;
|
|
|
|
|
- font-size: 1.1em;
|
|
|
|
|
- font-family: AlibabaSans, -apple-system, BlinkMacSystemFont;
|
|
|
|
|
-
|
|
|
|
|
- .time {
|
|
|
|
|
- font-size: 1.3em;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.faceWindow video {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- object-fit: cover;
|
|
|
|
|
- /* 保持视频比例并填满圆形 */
|
|
|
|
|
- transform: scaleX(-1);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.spin {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- min-height: 270px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.userInfo {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- text-align: left;
|
|
|
|
|
- height: 60px;
|
|
|
|
|
- min-width: 300px;
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- background-color: rgba(190, 218, 255, 0.5);
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
-
|
|
|
|
|
- .right {
|
|
|
|
|
- margin-left: 10px;
|
|
|
|
|
-
|
|
|
|
|
- .name {
|
|
|
|
|
- font-size: 1.2em;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.loading {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- background-color: rgba(0, 0, 0, 0.2);
|
|
|
|
|
- z-index: 9999;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
-
|
|
|
|
|
- .loadingTip {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- color: #3370FF;
|
|
|
|
|
-
|
|
|
|
|
- .text {
|
|
|
|
|
- font-family: 'Alimama ShuHeiTi';
|
|
|
|
|
- font-size: 1.2em;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|