|
@@ -0,0 +1,880 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="fingerprint-wrapper">
|
|
|
|
|
+ <PageHeader :show-back="true" :is-go-home="false" :is-custom-back="true" @back="handleBack" />
|
|
|
|
|
+ <div class="fingerprint-page">
|
|
|
|
|
+ <div class="fingerprint-content">
|
|
|
|
|
+ <!-- 步骤内容 -->
|
|
|
|
|
+ <div class="step-container">
|
|
|
|
|
+ <!-- 第一步输入账号 -->
|
|
|
|
|
+ <div v-if="currentStep === 1" class="step-card step-one">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="step-icon">
|
|
|
|
|
+ <i class="icon-user">👤</i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h2 class="step-title">身份验证</h2>
|
|
|
|
|
+ <p class="step-desc">请输入登录账号进行身份验证</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="input-section">
|
|
|
|
|
+ <div class="input-wrapper">
|
|
|
|
|
+ <van-cell-group inset style="padding: 10px 0 20px;">
|
|
|
|
|
+ <van-field v-model="jobNo" size="large" :border="true" label="登录账号:" placeholder="请输入登录账号" />
|
|
|
|
|
+ </van-cell-group>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <a-button
|
|
|
|
|
+ type="primary" size="large" class="next-button" :loading="checking" block
|
|
|
|
|
+ @click="handleNextStep"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span v-if="!checking">验证并继续</span>
|
|
|
|
|
+ <span v-else>验证中...</span>
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 第二部 指纹录入 -->
|
|
|
|
|
+ <div v-else class="step-card step-two">
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="step-icon">
|
|
|
|
|
+ <i class="icon-fingerprint">👆</i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="header-content">
|
|
|
|
|
+ <h2 class="step-title">指纹录入</h2>
|
|
|
|
|
+ <p class="step-desc">
|
|
|
|
|
+ 登录账号:<span class="job-highlight">{{ jobNo }}</span>
|
|
|
|
|
+ <!-- <a-button type="link" size="small" class="edit-link" @click="backToStepOne">
|
|
|
|
|
+ 返回
|
|
|
|
|
+ </a-button> -->
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="fingerprint-section">
|
|
|
|
|
+ <!-- 设备状态 -->
|
|
|
|
|
+ <div class="device-status" @click="handleDeviceClick">
|
|
|
|
|
+ <div class="status-indicator" :class="{ connected: isConnected }">
|
|
|
|
|
+ <div class="status-dot" />
|
|
|
|
|
+ <span class="status-text">
|
|
|
|
|
+ {{ isConnected ? '设备已连接' : '设备未连接' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 指纹 -->
|
|
|
|
|
+ <div class="fingerprint-display">
|
|
|
|
|
+ <div class="fingerprint-scanner" :class="{ active: isConnected, scanning: isScanning }">
|
|
|
|
|
+ <div class="scanner-glow" />
|
|
|
|
|
+ <svg class="fingerprint-icon" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
+ <!-- 外侧指纹脊线 -->
|
|
|
|
|
+ <ellipse
|
|
|
|
|
+ cx="100" cy="100" rx="85" ry="90" fill="none" stroke="currentColor" stroke-width="2"
|
|
|
|
|
+ opacity="0.3"
|
|
|
|
|
+ />
|
|
|
|
|
+ <ellipse
|
|
|
|
|
+ cx="100" cy="100" rx="75" ry="80" fill="none" stroke="currentColor" stroke-width="2"
|
|
|
|
|
+ opacity="0.4"
|
|
|
|
|
+ />
|
|
|
|
|
+ <ellipse
|
|
|
|
|
+ cx="100" cy="100" rx="65" ry="70" fill="none" stroke="currentColor" stroke-width="2"
|
|
|
|
|
+ opacity="0.5"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 内侧指纹脊线 -->
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M100 30 C130 30, 150 50, 150 80 C150 110, 130 130, 100 130 C70 130, 50 110, 50 80 C50 50, 70 30, 100 30"
|
|
|
|
|
+ fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M100 40 C120 40, 135 55, 135 75 C135 95, 120 110, 100 110 C80 110, 65 95, 65 75 C65 55, 80 40, 100 40"
|
|
|
|
|
+ fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.7"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M100 50 C110 50, 120 60, 120 70 C120 80, 110 90, 100 90 C90 90, 80 80, 80 70 C80 60, 90 50, 100 50"
|
|
|
|
|
+ fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.8"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 指纹脊线 -->
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M60 60 Q100 40, 140 60" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
+ opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M65 70 Q100 50, 135 70" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
+ opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M70 80 Q100 60, 130 80" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
+ opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M75 90 Q100 70, 125 90" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
+ opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M80 100 Q100 80, 120 100" fill="none" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
+ opacity="0.6"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中央螺旋纹 -->
|
|
|
|
|
+ <circle cx="100" cy="75" r="8" fill="none" stroke="currentColor" stroke-width="2" opacity="0.8" />
|
|
|
|
|
+ <circle cx="100" cy="75" r="4" fill="currentColor" opacity="0.6" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 扫描动画覆盖层 -->
|
|
|
|
|
+ <div v-if="isScanning" class="scan-line" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="instruction-text">
|
|
|
|
|
+ {{ isConnected ? statusText : '请先连接指纹设备' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Action Buttons -->
|
|
|
|
|
+ <div class="action-section">
|
|
|
|
|
+ <a-button
|
|
|
|
|
+ type="primary" size="large" class="enroll-button btn" :disabled="!isConnected"
|
|
|
|
|
+ :style="isConnected ? 'color: #fff' : 'color: #534a93'" :loading="isScanning" block
|
|
|
|
|
+ @click="handleEnroll"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template v-if="!isScanning">
|
|
|
|
|
+ <!-- <span class="button-icon"></span> -->
|
|
|
|
|
+ 指纹录入
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ 正在录入中...
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+
|
|
|
|
|
+ <a-button size="large" class="reset-button btn" :disabled="!isConnected" block @click="handleReEnroll">
|
|
|
|
|
+ 重新录入
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ <!-- <a-button size="large" class="reset-button" block @click="handleDisableConnect">
|
|
|
|
|
+ 断开连接
|
|
|
|
|
+ </a-button> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
|
|
+import { showToast, showFailToast } from 'vant';
|
|
|
|
|
+import PageHeader from '../common/PageHeader.vue';
|
|
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue';
|
|
|
|
|
+import { checkEmployeeByJobNo } from '../api/fingerprint.js';
|
|
|
|
|
+
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+
|
|
|
|
|
+const currentStep = ref(1);
|
|
|
|
|
+const jobNo = ref('');
|
|
|
|
|
+const checking = ref(false);
|
|
|
|
|
+const isConnected = ref(false);
|
|
|
|
|
+const isScanning = ref(false);
|
|
|
|
|
+const statusText = ref('');
|
|
|
|
|
+
|
|
|
|
|
+const handleBack = () => {
|
|
|
|
|
+ if (currentStep.value === 2) {
|
|
|
|
|
+ currentStep.value = 1;
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ plugin.fingerprintConfig.disableConnect();
|
|
|
|
|
+ }
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ statusText.value = '请点击指纹录入按钮进行录入';
|
|
|
|
|
+ } else if (currentStep.value === 1) {
|
|
|
|
|
+ router.push('/home');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 验证账号是否为系统成员
|
|
|
|
|
+const handleNextStep = async () => {
|
|
|
|
|
+ if (!jobNo.value) {
|
|
|
|
|
+ showToast('请输入账号');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ checking.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await checkEmployeeByJobNo(jobNo.value);
|
|
|
|
|
+ if (res.errorCode === 0) {
|
|
|
|
|
+ if (!res.data) {
|
|
|
|
|
+ showToast('该账号不是系统成员');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentStep.value = 2;
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ plugin.fingerprintConfig.connect();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+ statusText.value = '指纹设备插件未就绪,请检查设备';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast(res.errorMessage);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.log(error, '账号验证失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ checking.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 返回输入账号
|
|
|
|
|
+const backToStepOne = () => {
|
|
|
|
|
+ currentStep.value = 1;
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ plugin.fingerprintConfig.disableConnect();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 录入指纹
|
|
|
|
|
+const handleEnroll = () => {
|
|
|
|
|
+ if (!isConnected.value) {
|
|
|
|
|
+ showToast('设备未连接或插件异常,请检查指纹设备');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!jobNo.value) {
|
|
|
|
|
+ showToast('账号为空,将返回上一步重新输入');
|
|
|
|
|
+ currentStep.value = 1;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ isScanning.value = true;
|
|
|
|
|
+ statusText.value = '请将手指放在指纹采集器上多次按压';
|
|
|
|
|
+ plugin.fingerprintConfig.enroll(jobNo.value);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 重新录入指纹
|
|
|
|
|
+const handleReEnroll = async () => {
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 先断开连接
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ plugin.fingerprintConfig.disableConnect();
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+ statusText.value = '正在准备重新录入指纹,请稍等...';
|
|
|
|
|
+
|
|
|
|
|
+ // 等待一秒后重新连接
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ plugin.fingerprintConfig.connect();
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ handleEnroll();
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ showFailToast('重新连接设备失败');
|
|
|
|
|
+ statusText.value = '设备连接失败,请检查设备';
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理指纹响应
|
|
|
|
|
+const handleFingerprintResponse = data => {
|
|
|
|
|
+ console.log('接收到指纹设备的响应信息', data);
|
|
|
|
|
+ if (!data) return;
|
|
|
|
|
+ const code = data.code;
|
|
|
|
|
+ const msg = data.message || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (code === 0) {
|
|
|
|
|
+ statusText.value = msg || '指纹录入成功!';
|
|
|
|
|
+ if (msg === '上传成功' || msg === '当前用户已经录入过指纹,更新成功。') {
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (msg === '连接成功') {
|
|
|
|
|
+ isConnected.value = true;
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ statusText.value = '设备已连接,请点击指纹录入按钮进行录入';
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (msg === '连接失败') isConnected.value = false;
|
|
|
|
|
+ statusText.value = msg || '指纹录入失败,请重新录入';
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 设置指纹设备的响应处理函数
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ plugin.fingerprintConfig.receiveFingerprintResponse = handleFingerprintResponse;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 断开连接
|
|
|
|
|
+const handleDisableConnect = () => {
|
|
|
|
|
+ isScanning.value = false;
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+ statusText.value = '设备已断开连接,请点击连接状态重新连接';
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ plugin.fingerprintConfig.disableConnect();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 重连or断开
|
|
|
|
|
+const handleDeviceClick = () => {
|
|
|
|
|
+ if (isConnected.value) {
|
|
|
|
|
+ handleDisableConnect();
|
|
|
|
|
+ isConnected.value = false;
|
|
|
|
|
+ statusText.value = '设备已断开连接,请点击连接状态重新连接';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (plugin.fingerprintConfig) {
|
|
|
|
|
+ plugin.fingerprintConfig.connect();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 断开连接
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ handleDisableConnect();
|
|
|
|
|
+ plugin.fingerprintConfig.receiveFingerprintResponse = () => { };
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* 全局重置 - 防止出现滚动条 */
|
|
|
|
|
+* {
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+html,
|
|
|
|
|
+body {
|
|
|
|
|
+ overflow: hidden !important;
|
|
|
|
|
+ height: 100vh !important;
|
|
|
|
|
+ margin: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 基础布局 */
|
|
|
|
|
+.fingerprint-wrapper {
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-wrapper::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background:
|
|
|
|
|
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
|
|
|
|
+ radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-page {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 0.5rem;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-content {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 步骤卡片 */
|
|
|
|
|
+.step-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-card {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: clamp(1.5rem, 3vh, 2.5rem) clamp(1.5rem, 3vw, 2.5rem);
|
|
|
|
|
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ border: 1px solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 第一步 - 使卡片更大更居中 */
|
|
|
|
|
+.step-one {
|
|
|
|
|
+ min-height: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 第二步 - 防止过度填充,添加适当的间距 */
|
|
|
|
|
+.step-two {
|
|
|
|
|
+ min-height: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: clamp(0.5rem, 2vw, 1rem);
|
|
|
|
|
+ margin-bottom: clamp(0.5rem, 2vh, 1rem);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-icon {
|
|
|
|
|
+ width: clamp(2.5rem, 6vw, 4rem);
|
|
|
|
|
+ height: clamp(2.5rem, 6vw, 4rem);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea, #764ba2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: clamp(1rem, 3vw, 1.8rem);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-title {
|
|
|
|
|
+ font-size: clamp(1.2rem, 4vw, 2rem);
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: clamp(0.25rem, 1vh, 0.5rem);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-desc {
|
|
|
|
|
+ font-size: clamp(0.9rem, 2.5vw, 1.2rem);
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 第一步样式 */
|
|
|
|
|
+.input-section {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: clamp(1.2rem, 3.5vh, 2.5rem);
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ padding-top: clamp(2rem, 4vh, 3rem);
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input-wrapper {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.job-input {
|
|
|
|
|
+ height: clamp(3.5rem, 8vh, 5rem);
|
|
|
|
|
+ font-size: clamp(1rem, 2.5vw, 1.3rem);
|
|
|
|
|
+ border-radius: clamp(8px, 1.5vw, 12px);
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.job-input:focus {
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 1rem;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
|
|
+ font-size: 1.2rem;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.next-button {
|
|
|
|
|
+ height: clamp(3.5rem, 8vh, 5rem);
|
|
|
|
|
+ font-size: clamp(1rem, 2.5vw, 1.3rem);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea, #764ba2);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: clamp(8px, 1.5vw, 12px);
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.next-button:hover {
|
|
|
|
|
+ background: linear-gradient(135deg, #5a67d8, #6b46c1);
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 第二步样式 */
|
|
|
|
|
+.header-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.job-highlight {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ background: rgba(102, 126, 234, 0.1);
|
|
|
|
|
+ padding: 0.25rem 0.5rem;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.edit-link {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-left: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-section {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: clamp(0.75rem, 2vh, 1.25rem);
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 设备状态 */
|
|
|
|
|
+.device-status {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-indicator {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 0.75rem;
|
|
|
|
|
+ padding: 0.75rem 1.5rem;
|
|
|
|
|
+ border-radius: 50px;
|
|
|
|
|
+ background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-indicator.connected {
|
|
|
|
|
+ background: rgba(16, 185, 129, 0.1);
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-dot {
|
|
|
|
|
+ width: 0.75rem;
|
|
|
|
|
+ height: 0.75rem;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: currentColor;
|
|
|
|
|
+ animation: pulse 2s infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes pulse {
|
|
|
|
|
+
|
|
|
|
|
+ 0%,
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 指纹显示 */
|
|
|
|
|
+.fingerprint-display {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-scanner {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: clamp(140px, 25vw, 200px);
|
|
|
|
|
+ height: clamp(140px, 25vw, 200px);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border: clamp(2px, 0.5vw, 4px) solid #e5e7eb;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-scanner.active {
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ background: linear-gradient(135deg, #f0f4ff, #e0e7ff);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-scanner.scanning {
|
|
|
|
|
+ animation: scannerPulse 2s infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes scannerPulse {
|
|
|
|
|
+
|
|
|
|
|
+ 0%,
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 0 0 20px rgba(102, 126, 234, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.scanner-glow {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: -10px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: radial-gradient(circle, rgba(102, 126, 234, 0.2) 0%, transparent 70%);
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-scanner.active .scanner-glow {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-icon {
|
|
|
|
|
+ width: clamp(100px, 18vw, 140px);
|
|
|
|
|
+ height: clamp(100px, 18vw, 140px);
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ transition: color 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.fingerprint-scanner.active .fingerprint-icon {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.scan-line {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ height: 3px;
|
|
|
|
|
+ background: linear-gradient(90deg, transparent, #667eea, transparent);
|
|
|
|
|
+ animation: scanning 2s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes scanning {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: translateY(var(--scanner-size, 220px));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 动态扫描动画 */
|
|
|
|
|
+.fingerprint-scanner {
|
|
|
|
|
+ --scanner-size: clamp(140px, 25vw, 200px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 横屏扫描动画 */
|
|
|
|
|
+@media (orientation: landscape) {
|
|
|
|
|
+ .fingerprint-scanner {
|
|
|
|
|
+ --scanner-size: clamp(160px, 28vw, 220px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 竖屏大屏幕扫描动画 */
|
|
|
|
|
+@media (orientation: portrait) and (min-width: 768px) {
|
|
|
|
|
+ .fingerprint-scanner {
|
|
|
|
|
+ --scanner-size: clamp(150px, 28vw, 220px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.instruction-text {
|
|
|
|
|
+ font-size: clamp(0.9rem, 2.5vw, 1.2rem);
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ max-width: clamp(250px, 50vw, 400px);
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 按钮 */
|
|
|
|
|
+.action-section {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: clamp(0.6rem, 1.5vh, 0.9rem);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.enroll-button {
|
|
|
|
|
+ height: clamp(3.5rem, 8vh, 5rem);
|
|
|
|
|
+ font-size: clamp(1rem, 2.5vw, 1.3rem);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea, #764ba2);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: clamp(8px, 1.5vw, 12px);
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.enroll-button:hover:not(:disabled) {
|
|
|
|
|
+ background: linear-gradient(135deg, #5a67d8, #6b46c1);
|
|
|
|
|
+ transform: translateY(-1px);
|
|
|
|
|
+ box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.reset-button {
|
|
|
|
|
+ height: clamp(2.75rem, 5vh, 3.5rem);
|
|
|
|
|
+ font-size: clamp(0.9rem, 2vw, 1.1rem);
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 2px solid #e5e7eb;
|
|
|
|
|
+ border-radius: clamp(8px, 1.5vw, 12px);
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.reset-button:hover:not(:disabled) {
|
|
|
|
|
+ border-color: #667eea;
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ background: rgba(102, 126, 234, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.button-icon {
|
|
|
|
|
+ font-size: 1.2rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 横屏优化 */
|
|
|
|
|
+@media (orientation: landscape) {
|
|
|
|
|
+ .input-section {
|
|
|
|
|
+ gap: clamp(1.5rem, 4vh, 2.5rem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .job-input {
|
|
|
|
|
+ height: clamp(3.5rem, 7vh, 4.5rem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .next-button {
|
|
|
|
|
+ height: clamp(3.5rem, 7vh, 4.5rem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-scanner {
|
|
|
|
|
+ width: clamp(160px, 28vw, 220px);
|
|
|
|
|
+ height: clamp(160px, 28vw, 220px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-icon {
|
|
|
|
|
+ width: clamp(120px, 20vw, 160px);
|
|
|
|
|
+ height: clamp(120px, 20vw, 160px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 竖屏优化 - 内容略微升高 */
|
|
|
|
|
+@media (orientation: portrait) {
|
|
|
|
|
+ .input-section {
|
|
|
|
|
+ gap: clamp(1.5rem, 4vh, 3rem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-scanner {
|
|
|
|
|
+ width: clamp(150px, 28vw, 220px);
|
|
|
|
|
+ height: clamp(150px, 28vw, 220px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-icon {
|
|
|
|
|
+ width: clamp(110px, 20vw, 160px);
|
|
|
|
|
+ height: clamp(110px, 20vw, 160px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 竖屏大屏幕优化 */
|
|
|
|
|
+@media (orientation: portrait) and (min-width: 768px) and (min-height: 1000px) {
|
|
|
|
|
+ .input-section {
|
|
|
|
|
+ gap: clamp(2rem, 5vh, 3.5rem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-scanner {
|
|
|
|
|
+ width: clamp(170px, 30vw, 240px);
|
|
|
|
|
+ height: clamp(170px, 30vw, 240px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-icon {
|
|
|
|
|
+ width: clamp(120px, 22vw, 170px);
|
|
|
|
|
+ height: clamp(120px, 22vw, 170px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 针对1080*1920分辨率优化 */
|
|
|
|
|
+@media (min-width: 1080px) and (max-width: 1080px) and (min-height: 1920px) and (max-height: 1920px) {
|
|
|
|
|
+ .fingerprint-section {
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ padding-top: 2rem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-header {
|
|
|
|
|
+ margin-bottom: 1rem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-status {
|
|
|
|
|
+ margin-bottom: 1rem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .fingerprint-display {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 可访问性 */
|
|
|
|
|
+@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
+ * {
|
|
|
|
|
+ animation-duration: 0.01ms !important;
|
|
|
|
|
+ animation-iteration-count: 1 !important;
|
|
|
|
|
+ transition-duration: 0.01ms !important;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 深色模式支持 */
|
|
|
|
|
+@media (prefers-color-scheme: dark) {
|
|
|
|
|
+ .step-card {
|
|
|
|
|
+ background: #374151;
|
|
|
|
|
+ border-color: #4b5563;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .step-title {
|
|
|
|
|
+ color: #f9fafb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .step-desc {
|
|
|
|
|
+ color: #d1d5db;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .instruction-text {
|
|
|
|
|
+ color: #d1d5db;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn {
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.van-field) {
|
|
|
|
|
+ border: 1px solid #ccc;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|