1157 lines
30 KiB
Vue
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>
|