Files
QSY-lvjuxcx/pkgService/lock_control/lock_control.vue
liangjian 9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00

1157 lines
30 KiB
Vue

<template>
<view class="lock-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ $tr('门锁') }}</view>
</tn-nav-bar>
<view class="page-body tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view class="hero-card">
<view class="hero-kicker">{{ lockTargetTypeText }}</view>
<view class="hero-title tn-margin-top-sm">{{ displayRoomName }}</view>
<view class="hero-desc tn-margin-top-sm">{{ contextHintText }}</view>
<view v-if="showLockTargetSelector" class="target-panel tn-margin-top-lg">
<view class="target-panel__head">
<view class="target-panel__title">{{ $tr('可开门锁') }}</view>
<view v-if="isLoadingTargets" class="target-panel__meta">
<text class="tn-icon-loading target-panel__loading tn-margin-right-xs"></text>
<text>{{ $tr('同步中') }}</text>
</view>
<view v-else class="target-panel__meta">{{ targetCountText }}</view>
</view>
<view v-if="targetLoadError && !lockTargets.length" class="target-panel__error">
<text class="tn-icon-warning tn-margin-right-xs"></text>
<text>{{ targetLoadError }}</text>
</view>
<view v-else class="target-chip-list">
<view
v-for="item in lockTargets"
:key="item.targetKey"
class="target-chip"
:class="{
active: item.targetKey === activeLockTargetKey,
disabled: isAddingFinger
}"
@click="handleSelectLockTarget(item)"
>
<view class="target-chip__title">{{ item.displayName }}</view>
<view class="target-chip__desc">
{{ item.suiteCode ? $tr('套间 / 公区') : $tr('房间') }}
</view>
</view>
</view>
</view>
<view class="hero-action-row tn-margin-top-lg">
<view
class="action-btn action-btn--primary"
:class="{ disabled: !canAddFinger || isAddingFinger }"
@click="handleAddFingerKey"
>
<text
class="tn-margin-right-xs"
:class="isAddingFinger ? 'tn-icon-loading finger-btn-loading' : 'tn-icon-fingerprint'"
></text>
<text>{{ isAddingFinger ? $tr('下发中...') : $tr('录入指纹') }}</text>
</view>
</view>
</view>
<view class="info-card tn-margin-top-lg">
<view class="section-head">
<view class="section-title">{{ $tr('我的钥匙') }}</view>
<view class="section-action" @click="reloadAllLockData">
<text class="tn-icon-refresh tn-margin-right-xs"></text>
<text>{{ $tr('刷新') }}</text>
</view>
</view>
<view class="key-row key-row--compact">
<view class="key-label">{{ $tr('当前门锁') }}</view>
<view class="key-target-name">{{ displayRoomName }}</view>
</view>
<view v-if="isLoadingKeyInfo" class="info-empty">
<text class="tn-icon-loading"></text>
<text>{{ $tr('正在同步门锁钥匙...') }}</text>
</view>
<view v-else-if="keyLoadError" class="info-empty info-empty--error">
<text class="tn-icon-warning tn-margin-right-xs"></text>
<text>{{ keyLoadError }}</text>
</view>
<view v-else>
<view class="key-row">
<view class="key-label">{{ $tr('门锁密码') }}</view>
<view class="key-value-wrap">
<text class="key-value">{{ passwordText }}</text>
<view
class="mini-btn"
:class="{ disabled: !keyInfo.pwd }"
@click="copyPassword"
>
{{ $tr('复制') }}
</view>
</view>
</view>
<view class="key-row">
<view class="key-label">{{ $tr('指纹状态') }}</view>
<view class="finger-status" :class="{ active: hasFinger }">
{{ fingerStatusText }}
</view>
</view>
<view class="tips-box tn-margin-top-lg">
<view class="tips-title">{{ $tr('使用说明') }}</view>
<view class="tips-desc tn-margin-top-sm">{{ tipsDescText }}</view>
</view>
</view>
</view>
</view>
<view v-if="showFingerGuide" class="finger-guide-mask">
<view class="finger-guide-card">
<view class="finger-guide-animation">
<view class="finger-guide-ripple finger-guide-ripple--1"></view>
<view class="finger-guide-ripple finger-guide-ripple--2"></view>
<view class="finger-guide-ripple finger-guide-ripple--3"></view>
<view class="finger-guide-core">
<view class="finger-guide-spinner">
<view class="finger-guide-spinner__ring"></view>
<view class="finger-guide-spinner__ring finger-guide-spinner__ring--delay"></view>
<view class="finger-guide-spinner__dot"></view>
</view>
</view>
</view>
<view class="finger-guide-title">{{ fingerGuideTitle }}</view>
<view class="finger-guide-desc">{{ fingerGuideDesc }}</view>
<view class="finger-guide-tip">{{ fingerGuideTip }}</view>
</view>
</view>
</view>
</template>
<script>
import {
deviceGetDevicesByRoomAsync,
lockHxjAddFingerKey,
lockHxjGetKeysByUserCode
} from '../../api/index.js'
import { getSessionSnapshot } from '../../utils/app-state'
function pickFirstFilledValue(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (text) {
return text
}
}
return ''
}
function safeDecodeText(value) {
if (value === undefined || value === null || value === '') {
return ''
}
try {
return decodeURIComponent(String(value))
} catch (error) {
return String(value)
}
}
function unwrapApiData(response = {}) {
if (response && Object.prototype.hasOwnProperty.call(response, 'data')) {
return response.data
}
return response || {}
}
function normalizeKeyInfo(response = {}) {
const data = unwrapApiData(response)
return {
pwd: String((data && data.pwd) || '').trim(),
pwdId: data && data.pwdId,
fingerId: data && data.fingerId
}
}
function isLockTargetLike(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
return (
Object.prototype.hasOwnProperty.call(value, 'roomCode')
|| Object.prototype.hasOwnProperty.call(value, 'roomName')
|| Object.prototype.hasOwnProperty.call(value, 'suiteCode')
|| Object.prototype.hasOwnProperty.call(value, 'suiteName')
)
}
function collectLockTargetSources(value, result = [], depth = 0) {
if (depth > 6 || value === undefined || value === null) {
return result
}
if (Array.isArray(value)) {
value.forEach(item => collectLockTargetSources(item, result, depth + 1))
return result
}
if (typeof value !== 'object') {
return result
}
if (isLockTargetLike(value)) {
result.push(value)
}
;[
'data',
'result',
'payload',
'items',
'list',
'rows',
'records'
].forEach((key) => {
if (Object.prototype.hasOwnProperty.call(value, key)) {
collectLockTargetSources(value[key], result, depth + 1)
}
})
return result
}
function normalizeLockTarget(source = {}) {
const roomCode = pickFirstFilledValue(source.roomCode)
const suiteCode = pickFirstFilledValue(source.suiteCode)
const roomName = pickFirstFilledValue(source.roomName)
const suiteName = pickFirstFilledValue(source.suiteName)
if (!roomCode && !suiteCode) {
return null
}
return {
roomCode,
roomName,
suiteCode,
suiteName,
displayName: pickFirstFilledValue(suiteName, roomName, suiteCode, roomCode),
targetKey: `${suiteCode ? 'suite' : 'room'}:${suiteCode || roomCode}`
}
}
function mergeLockTargets(primaryList = [], fallbackList = []) {
const mergedMap = new Map()
;[...(primaryList || []), ...(fallbackList || [])].forEach((item) => {
const normalized = normalizeLockTarget(item)
if (!normalized || !normalized.targetKey) {
return
}
if (!mergedMap.has(normalized.targetKey)) {
mergedMap.set(normalized.targetKey, normalized)
}
})
return Array.from(mergedMap.values())
}
export default {
data() {
return {
hasCheckedIn: false,
roomCode: '',
suiteCode: '',
roomName: '',
suiteName: '',
userCode: '',
lockTargets: [],
activeLockTargetKey: '',
isLoadingTargets: false,
targetLoadError: '',
isLoadingKeyInfo: false,
isAddingFinger: false,
fingerGuidePhase: '',
fingerGuideTimer: null,
suppressSystemLoadingTimer: null,
fingerGuidePollCount: 0,
keyLoadError: '',
keyInfo: {
pwd: '',
pwdId: null,
fingerId: null
}
}
},
computed: {
activeLockTarget() {
const matched = this.lockTargets.find(item => item.targetKey === this.activeLockTargetKey)
if (matched) {
return matched
}
return normalizeLockTarget({
roomCode: this.roomCode,
roomName: this.roomName,
suiteCode: this.suiteCode,
suiteName: this.suiteName
})
},
displayRoomName() {
const target = this.activeLockTarget || {}
return target.displayName || this.$tr('当前房间')
},
lockTargetTypeText() {
const target = this.activeLockTarget || {}
return target.suiteCode ? this.$tr('套间 / 公区门锁') : this.$tr('房间门锁')
},
hasLockContext() {
const target = this.activeLockTarget || {}
return !!(target.suiteCode || target.roomCode)
},
canLoadTargets() {
return this.hasCheckedIn && !!this.roomCode
},
canLoadKeys() {
return this.hasCheckedIn && this.hasLockContext && !!this.userCode
},
canAddFinger() {
return this.canLoadKeys
},
showFingerGuide() {
return this.fingerGuidePhase === 'sending' || this.fingerGuidePhase === 'collecting'
},
showLockTargetSelector() {
return this.isLoadingTargets || !!this.targetLoadError || this.lockTargets.length > 0
},
targetCountText() {
return this.$tr('共 {count} 个', { count: this.lockTargets.length })
},
passwordText() {
return this.keyInfo.pwd || this.$tr('暂未分配')
},
hasFinger() {
return this.keyInfo.fingerId !== null && this.keyInfo.fingerId !== undefined && this.keyInfo.fingerId !== ''
},
fingerStatusText() {
return this.hasFinger ? this.$tr('已录入') : this.$tr('未录入')
},
fingerGuideTitle() {
if (this.fingerGuidePhase === 'sending') {
return this.$tr('正在准备录入')
}
return this.$tr('请重复按压指纹')
},
fingerGuideDesc() {
if (this.fingerGuidePhase === 'sending') {
return this.$tr('已向门锁下发指令,请稍候片刻。')
}
return this.$tr('请根据门锁提示,将手指自然贴合采集区并重复按压,直到门锁提示录入完成。')
},
fingerGuideTip() {
if (this.fingerGuidePhase === 'sending') {
return this.$tr('正在连接门锁...')
}
return this.$tr('系统会自动刷新录入结果')
},
tipsDescText() {
return this.$tr('如需录入指纹,请先点击“录入指纹”,再按门锁提示完成采集。')
},
contextHintText() {
if (!this.hasCheckedIn) {
return this.$tr('完成入住后即可使用门锁能力。')
}
if (!this.roomCode && !this.suiteCode) {
return this.$tr('当前入住订单缺少房间编码,请联系管家配置。')
}
if (this.isLoadingTargets) {
return this.$tr('正在同步当前房间下可操作的门锁。')
}
if (!this.hasLockContext) {
return this.$tr('当前房间暂未查询到可用门锁。')
}
if (!this.userCode) {
return this.$tr('当前账号缺少学工号,暂无法同步门锁钥匙。')
}
if (this.lockTargets.length > 1) {
return this.$tr('当前房间下共 {count} 个可操作门锁,当前已选“{name}”。', {
count: this.lockTargets.length,
name: this.displayRoomName
})
}
if ((this.activeLockTarget || {}).suiteCode) {
return this.$tr('当前将操作所选套间或公区门锁。')
}
return this.$tr('当前将操作当前房间门锁。')
}
},
onLoad(options) {
this.resolvePageContext(options || {})
},
onShow() {
this.reloadAllLockData()
},
onUnload() {
this.stopSuppressSystemLoading()
this.clearFingerGuideTimer()
},
beforeDestroy() {
this.stopSuppressSystemLoading()
this.clearFingerGuideTimer()
},
methods: {
resolvePageContext(options = {}) {
const session = getSessionSnapshot()
const roomData = (session && session.myRoomData) || {}
const resolvedUser = (session && (session.resolvedUser || session.currentUser || session.registeredUser)) || {}
this.hasCheckedIn = !!(session && session.hasCheckedIn)
this.roomCode = pickFirstFilledValue(
safeDecodeText(options.roomCode),
roomData.roomCode,
roomData.facilityCode,
roomData.gatewayRoomCode,
roomData.iotRoomCode
)
this.suiteCode = pickFirstFilledValue(
safeDecodeText(options.suiteCode),
roomData.suiteCode,
roomData.suiteNo,
roomData.setCode,
roomData.unitCode,
roomData.parentRoomCode
)
this.roomName = pickFirstFilledValue(
safeDecodeText(options.roomName),
roomData.roomName,
roomData.roomLabel,
roomData.roomNo
)
this.suiteName = pickFirstFilledValue(
safeDecodeText(options.suiteName),
roomData.suiteName,
roomData.unitName,
roomData.suiteNo
)
this.userCode = pickFirstFilledValue(
safeDecodeText(options.userCode),
resolvedUser.userCode,
resolvedUser.code
)
},
buildFallbackLockTargets() {
const fallbackTarget = normalizeLockTarget({
roomCode: this.roomCode,
roomName: this.roomName,
suiteCode: this.suiteCode,
suiteName: this.suiteName
})
return fallbackTarget ? [fallbackTarget] : []
},
resolvePreferredTargetKey(targetList = []) {
const matchedCurrent = targetList.find(item => item.targetKey === this.activeLockTargetKey)
if (matchedCurrent) {
return matchedCurrent.targetKey
}
if (this.suiteCode) {
const suiteTarget = targetList.find(item => item.suiteCode === this.suiteCode)
if (suiteTarget) {
return suiteTarget.targetKey
}
}
if (this.roomCode) {
const roomTarget = targetList.find(item => item.roomCode === this.roomCode && !item.suiteCode)
if (roomTarget) {
return roomTarget.targetKey
}
}
return targetList.length ? targetList[0].targetKey : ''
},
applyLockTargetList(targetList = []) {
this.lockTargets = targetList
this.activeLockTargetKey = this.resolvePreferredTargetKey(targetList)
},
resetKeyInfo() {
this.keyInfo = {
pwd: '',
pwdId: null,
fingerId: null
}
},
fetchLockTargets() {
const fallbackTargets = this.buildFallbackLockTargets()
if (!this.canLoadTargets) {
this.targetLoadError = fallbackTargets.length
? ''
: (this.hasCheckedIn ? this.$tr('当前入住订单缺少房间编码,请联系管家配置。') : '')
this.applyLockTargetList(fallbackTargets)
return Promise.resolve(fallbackTargets)
}
this.isLoadingTargets = true
this.targetLoadError = ''
return deviceGetDevicesByRoomAsync({ roomCode: this.roomCode }, this.$getLanguage())
.then((response) => {
const sourceList = collectLockTargetSources(unwrapApiData(response))
const nextTargets = mergeLockTargets(sourceList, fallbackTargets)
this.applyLockTargetList(nextTargets)
this.targetLoadError = nextTargets.length ? '' : this.$tr('当前房间暂未查询到可用门锁。')
return nextTargets
})
.catch((error) => {
console.error('load lock target list failed', error)
this.applyLockTargetList(fallbackTargets)
this.targetLoadError = fallbackTargets.length
? ''
: ((error && (error.message || error.msg)) || this.$tr('门锁列表获取失败'))
return fallbackTargets
})
.finally(() => {
this.isLoadingTargets = false
})
},
buildLockPayload(includeUser = false) {
const target = this.activeLockTarget || {}
const payload = {
roomCode: target.suiteCode ? null : (target.roomCode || null),
suiteCode: target.suiteCode || null
}
if (includeUser) {
payload.userCode = this.userCode || null
}
return payload
},
reloadAllLockData() {
return this.fetchLockTargets().then(() => this.reloadKeyInfo())
},
reloadKeyInfo() {
if (!this.canLoadKeys) {
this.resetKeyInfo()
this.keyLoadError = this.hasCheckedIn && this.hasLockContext && !this.userCode
? this.$tr('缺少学工号,无法获取钥匙')
: ''
return Promise.resolve()
}
this.isLoadingKeyInfo = true
this.keyLoadError = ''
return lockHxjGetKeysByUserCode(this.buildLockPayload(true), this.$getLanguage())
.then((response) => {
this.keyInfo = normalizeKeyInfo(response)
this.keyLoadError = ''
})
.catch((error) => {
console.error('load lock key info failed', error)
this.resetKeyInfo()
this.keyLoadError = (error && (error.message || error.msg)) || this.$tr('钥匙同步失败')
})
.finally(() => {
this.isLoadingKeyInfo = false
})
},
handleSelectLockTarget(target = {}) {
if (!target.targetKey || target.targetKey === this.activeLockTargetKey || this.isAddingFinger) {
return
}
this.stopFingerGuide()
this.activeLockTargetKey = target.targetKey
this.reloadKeyInfo()
},
clearFingerGuideTimer() {
if (this.fingerGuideTimer) {
clearTimeout(this.fingerGuideTimer)
this.fingerGuideTimer = null
}
},
startSuppressSystemLoading() {
this.stopSuppressSystemLoading()
uni.hideLoading()
this.suppressSystemLoadingTimer = setInterval(() => {
uni.hideLoading()
}, 120)
},
stopSuppressSystemLoading() {
if (this.suppressSystemLoadingTimer) {
clearInterval(this.suppressSystemLoadingTimer)
this.suppressSystemLoadingTimer = null
}
uni.hideLoading()
},
stopFingerGuide() {
this.clearFingerGuideTimer()
this.stopSuppressSystemLoading()
this.fingerGuidePhase = ''
this.fingerGuidePollCount = 0
},
startFingerCollectGuide() {
this.clearFingerGuideTimer()
this.startSuppressSystemLoading()
this.fingerGuidePhase = 'collecting'
this.fingerGuidePollCount = 0
this.scheduleFingerStatusCheck()
},
scheduleFingerStatusCheck() {
this.clearFingerGuideTimer()
this.fingerGuideTimer = setTimeout(() => {
this.fingerGuidePollCount += 1
this.reloadKeyInfo()
.then(() => {
if (this.hasFinger) {
this.stopFingerGuide()
uni.showToast({
title: this.$tr('指纹已录入'),
icon: 'success'
})
return
}
if (this.fingerGuidePollCount >= 8) {
this.stopFingerGuide()
uni.showToast({
title: this.$tr('如门锁仍在等待,请继续按压后点刷新查看'),
icon: 'none'
})
return
}
this.scheduleFingerStatusCheck()
})
.catch(() => {
if (this.fingerGuidePollCount >= 8) {
this.stopFingerGuide()
} else {
this.scheduleFingerStatusCheck()
}
})
}, 2500)
},
handleAddFingerKey() {
if (!this.canAddFinger || this.isAddingFinger) {
if (!this.userCode) {
uni.showToast({ title: this.$tr('缺少学工号,暂无法录入指纹'), icon: 'none' })
} else if (!this.hasLockContext) {
uni.showToast({ title: this.$tr('当前暂无可操作门锁'), icon: 'none' })
}
return
}
this.isAddingFinger = true
this.startSuppressSystemLoading()
this.fingerGuidePhase = 'sending'
lockHxjAddFingerKey(this.buildLockPayload(true), this.$getLanguage())
.then(() => {
this.startFingerCollectGuide()
})
.catch((error) => {
console.error('add finger key failed', error)
this.stopFingerGuide()
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('指纹模式下发失败'),
icon: 'none'
})
})
.finally(() => {
this.isAddingFinger = false
})
},
copyPassword() {
if (!this.keyInfo.pwd) {
uni.showToast({
title: this.$tr('暂无可复制密码'),
icon: 'none'
})
return
}
uni.setClipboardData({
data: this.keyInfo.pwd,
success: () => {
uni.showToast({
title: this.$tr('密码已复制'),
icon: 'success'
})
}
})
}
}
}
</script>
<style scoped lang="scss">
.lock-page {
min-height: 100vh;
background: linear-gradient(180deg, #eef5ff 0%, #f8faff 100%);
}
.hero-card,
.info-card {
background: #ffffff;
border-radius: 28rpx;
padding: 32rpx;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
border: 1rpx solid rgba($brand-primary, 0.06);
}
.hero-card {
background:
radial-gradient(circle at top right, rgba($brand-primary, 0.12) 0%, rgba($brand-primary, 0) 36%),
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
}
.hero-kicker {
display: inline-flex;
align-items: center;
height: 42rpx;
padding: 0 18rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
font-size: 22rpx;
font-weight: 600;
}
.hero-title {
font-size: 44rpx;
font-weight: 700;
color: #152033;
line-height: 1.3;
}
.hero-desc {
font-size: 24rpx;
color: #6b7a90;
line-height: 1.7;
}
.target-panel {
padding: 24rpx;
border-radius: 24rpx;
background: rgba($brand-primary, 0.045);
}
.target-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.target-panel__title {
font-size: 24rpx;
font-weight: 700;
color: #152033;
}
.target-panel__meta {
display: inline-flex;
align-items: center;
font-size: 22rpx;
color: #6b7a90;
}
.target-panel__loading {
display: inline-block;
animation: fingerBtnSpin 0.9s linear infinite;
}
.target-panel__error {
display: flex;
align-items: center;
margin-top: 18rpx;
color: #d94b4b;
font-size: 23rpx;
line-height: 1.6;
}
.target-chip-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 20rpx;
}
.target-chip {
min-width: 180rpx;
max-width: 100%;
padding: 18rpx 22rpx;
border-radius: 22rpx;
background: #ffffff;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
transition: all 0.2s ease;
}
.target-chip.active {
background: linear-gradient(180deg, rgba($brand-primary, 0.11) 0%, rgba($brand-primary, 0.06) 100%);
border-color: rgba($brand-primary, 0.22);
box-shadow: 0 10rpx 26rpx rgba($brand-primary, 0.08);
}
.target-chip.disabled {
opacity: 0.55;
}
.target-chip__title {
font-size: 25rpx;
font-weight: 600;
color: #152033;
line-height: 1.4;
}
.target-chip__desc {
margin-top: 6rpx;
font-size: 20rpx;
color: #7a8ba5;
line-height: 1.4;
}
.hero-action-row {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
transition: all 0.2s ease;
}
.action-btn:active {
transform: scale(0.98);
}
.action-btn.disabled {
opacity: 0.45;
}
.action-btn--primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
color: #ffffff;
box-shadow: 0 12rpx 28rpx rgba($brand-primary, 0.2);
}
.action-btn--light {
background: #f4f8ff;
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.16);
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #152033;
}
.section-action {
display: inline-flex;
align-items: center;
color: $brand-primary;
font-size: 24rpx;
}
.info-empty {
min-height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
color: #7a8ba5;
font-size: 24rpx;
}
.info-empty--error {
color: #d94b4b;
}
.key-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding-top: 28rpx;
}
.key-row--compact {
padding-top: 22rpx;
}
.key-label {
font-size: 26rpx;
color: #6b7a90;
}
.key-target-name {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #152033;
text-align: right;
line-height: 1.5;
}
.key-value-wrap {
display: flex;
align-items: center;
}
.key-value {
font-size: 34rpx;
font-weight: 700;
color: #152033;
letter-spacing: 2rpx;
}
.finger-status {
min-width: 120rpx;
height: 48rpx;
padding: 0 18rpx;
border-radius: 999rpx;
background: #f3f5f8;
color: #7a8ba5;
font-size: 22rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.finger-status.active {
background: rgba(16, 185, 129, 0.12);
color: #10b981;
}
.mini-btn {
height: 52rpx;
padding: 0 20rpx;
margin-left: 20rpx;
border-radius: 999rpx;
background: #f4f8ff;
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.14);
font-size: 22rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.mini-btn.disabled {
opacity: 0.4;
}
.tips-box {
padding: 24rpx;
border-radius: 22rpx;
background: rgba($brand-primary, 0.04);
}
.tips-title {
font-size: 24rpx;
font-weight: 700;
color: #152033;
}
.tips-desc {
font-size: 23rpx;
color: #6b7a90;
line-height: 1.8;
}
.finger-guide-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
background: rgba(15, 23, 42, 0.34);
}
.finger-guide-card {
width: 100%;
max-width: 520rpx;
padding: 44rpx 40rpx;
border-radius: 32rpx;
background: rgba(41, 52, 69, 0.92);
color: #ffffff;
text-align: center;
box-shadow: 0 24rpx 56rpx rgba(15, 23, 42, 0.24);
}
.finger-guide-animation {
position: relative;
width: 220rpx;
height: 220rpx;
margin: 0 auto 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.finger-guide-ripple {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 2rpx solid rgba(101, 216, 255, 0.3);
animation: fingerRipple 2.4s ease-out infinite;
}
.finger-guide-ripple--2 {
animation-delay: 0.8s;
}
.finger-guide-ripple--3 {
animation-delay: 1.6s;
}
.finger-guide-core {
position: relative;
z-index: 2;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #5ed7ff 0%, #35bdf2 100%);
box-shadow: 0 0 0 16rpx rgba(94, 215, 255, 0.1);
animation: fingerPulse 1.6s ease-in-out infinite;
font-size: 56rpx;
color: #ffffff;
}
.finger-guide-spinner {
position: relative;
width: 56rpx;
height: 56rpx;
}
.finger-guide-spinner__ring {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 50%;
border: 6rpx solid rgba(255, 255, 255, 0.28);
border-top-color: #ffffff;
animation: fingerSpinner 0.9s linear infinite;
}
.finger-guide-spinner__ring--delay {
top: 10rpx;
right: 10rpx;
bottom: 10rpx;
left: 10rpx;
border-width: 4rpx;
border-color: rgba(255, 255, 255, 0.22);
border-bottom-color: #ffffff;
animation-duration: 1.2s;
animation-direction: reverse;
}
.finger-guide-spinner__dot {
position: absolute;
top: 50%;
left: 50%;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #ffffff;
transform: translate(-50%, -50%);
box-shadow: 0 0 20rpx rgba(255, 255, 255, 0.65);
}
.finger-guide-title {
font-size: 34rpx;
font-weight: 700;
line-height: 1.4;
}
.finger-guide-desc {
margin-top: 16rpx;
font-size: 24rpx;
line-height: 1.8;
color: rgba(255, 255, 255, 0.88);
}
.finger-guide-tip {
margin-top: 18rpx;
font-size: 22rpx;
color: #8ee7ff;
letter-spacing: 1rpx;
}
.finger-btn-loading {
display: inline-block;
animation: fingerBtnSpin 0.9s linear infinite;
}
@keyframes fingerRipple {
0% {
transform: scale(0.62);
opacity: 0;
}
30% {
opacity: 0.8;
}
100% {
transform: scale(1.06);
opacity: 0;
}
}
@keyframes fingerPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
@keyframes fingerBtnSpin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fingerSpinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>