943 lines
26 KiB
XML
943 lines
26 KiB
XML
<template>
|
||
<div class="totp-app">
|
||
<!-- 顶部标题区域 -->
|
||
<div class="header">
|
||
<text class="header-title" style="font-size: 22px; top:5px">{{appTitle}}</text>
|
||
</div>
|
||
|
||
<!-- 分割线 -->
|
||
<div class="header-divider"></div>
|
||
|
||
<!-- 账户列表 -->
|
||
<list class="totp-list" style="position: absolute; top: 55px; bottom: 48px; left: 6px; right: 6px; width: 324px;">
|
||
<list-item class="totp-item" style="height: 90px; margin-bottom: 4px; background-color: #1a1a1a; border-radius: 8px; border: 1px solid #333; flex-direction: row; align-items: center; padding: 8px 6px 8px 12px;" type="account" for="{{totpList}}" onclick="showDetail($idx)">
|
||
<!-- 左侧:服务名和用户名 -->
|
||
<div style="flex: 1; flex-direction: column; justify-content: center; margin-right: 0px;">
|
||
<text style="font-size: 30px; color: #fff; font-weight: bold; margin-bottom: 1px; text-align: left; line-height: 32px;">{{$item.name}}</text>
|
||
<text style="font-size: 24px; color: #999; text-align: left; line-height: 25px; width:200px" show="{{$item.usr}}">{{$item.usr}}</text>
|
||
</div>
|
||
<!-- 右侧:验证码 -->
|
||
<div style="width: 140px; height: 100%; align-items: center; justify-content: flex-end;">
|
||
<text style="font-size: 36px; color: #00ff88; font-family: monospace; font-weight: bold; letter-spacing: 1px; text-align: right; line-height: 38px; white-space: nowrap;">{{$item.otp}}</text>
|
||
</div>
|
||
</list-item>
|
||
</list>
|
||
|
||
<!-- 无账户时的占位符 -->
|
||
<div class="empty-placeholder" show="{{totpList.length == 0}}">
|
||
<text class="empty-icon">🔑</text>
|
||
<text class="empty-text">暂无验证器</text>
|
||
</div>
|
||
|
||
<!-- 底部倒计时和进度条并排 -->
|
||
<div class="countdown-container" show="{{totpList.length > 0}}">
|
||
<div style="position: absolute; bottom: 8px; left: 12px; right: 12px; height: 40px; flex-direction: row; align-items: center;">
|
||
<!-- 左侧倒计时文字 -->
|
||
<div style="width: 100px; height: 100%; align-items: center; justify-content: center;">
|
||
<text class="countdown-text" style="font-size: 24px; font-weight: bold; color: {{textColor}}; transition: color 500ms ease-out; text-align: center;">{{timeLeft}}秒</text>
|
||
</div>
|
||
<!-- 右侧进度条 -->
|
||
<div style="flex: 1; height: 100%; align-items: center; justify-content: center; margin-left: 8px;">
|
||
<!-- 进度条背景 -->
|
||
<div style="position: absolute; left: 0px; right: 0px; height: 8px; background-color: rgba(255, 255, 255, 0.1); border-radius: 4px;"></div>
|
||
<!-- 进度条 -->
|
||
<progress style="position: absolute; left: 0px; right: 0px; height: 8px; background-color: transparent; color: {{progressColor}}; border-radius: 4px;" percent="{{timeProgress}}" type="horizontal"></progress>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详情页面 -->
|
||
<div class="detail-page" show="{{showDetailPage}}">
|
||
<div class="detail-content">
|
||
<text class="detail-service">{{selectedItem.name}}</text>
|
||
<text class="detail-account" show="{{selectedItem.usr}}">{{selectedItem.usr}}</text>
|
||
<div class="detail-code-container">
|
||
<!-- Steam验证码:5位字符,一行显示 -->
|
||
<text class="detail-code-single" if="{{selectedItem.type === 'steam'}}">{{selectedItem.otp}}</text>
|
||
<!-- 标准TOTP:6位数字,双行显示,使用固定位置容器 -->
|
||
<block if="{{selectedItem.type !== 'steam'}}">
|
||
<!-- 第一行:3个数字 -->
|
||
<div class="detail-code-row">
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(0, 1)}}</text>
|
||
</div>
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(1, 2)}}</text>
|
||
</div>
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(2, 3)}}</text>
|
||
</div>
|
||
</div>
|
||
<!-- 第二行:3个数字 -->
|
||
<div class="detail-code-row">
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(3, 4)}}</text>
|
||
</div>
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(4, 5)}}</text>
|
||
</div>
|
||
<div class="detail-code-digit">
|
||
<text class="detail-code-num">{{selectedItem.otp.substring(5, 6)}}</text>
|
||
</div>
|
||
</div>
|
||
</block>
|
||
</div>
|
||
<text class="detail-time">剩余时间 {{timeLeft}} 秒</text>
|
||
</div>
|
||
|
||
<!-- 底部返回按钮容器 -->
|
||
<div class="detail-footer">
|
||
<div class="detail-back-btn" onclick="hideDetail">
|
||
<text class="detail-back-icon">←</text>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { TOTP, SteamTotp } from '../../utils/totp'
|
||
import vibrator from '@system.vibrator'
|
||
import storage from '@system.storage'
|
||
import interconnect from '@system.interconnect'
|
||
|
||
// Key class similar to BandTOTP
|
||
class TOTPKey {
|
||
constructor(key, name, usr = '', type = 'totp', algorithm = 'SHA1', digits = 6, period = 30) {
|
||
this.name = name
|
||
this.usr = usr
|
||
this.key = key
|
||
this.type = type // 'totp' 或 'steam'
|
||
this.algorithm = algorithm
|
||
this.digits = digits
|
||
this.period = period
|
||
this.update()
|
||
}
|
||
|
||
update() {
|
||
try {
|
||
// 根据类型选择算法
|
||
if (this.type === 'steam') {
|
||
this.otp = SteamTotp(this.key)
|
||
} else {
|
||
this.otp = TOTP(this.key, this.algorithm, this.digits, this.period)
|
||
}
|
||
} catch (error) {
|
||
this.otp = "ERROR"
|
||
}
|
||
}
|
||
}
|
||
|
||
let interconnectConn = null
|
||
|
||
export default {
|
||
private: {
|
||
totpList: [],
|
||
timeLeft: 30,
|
||
timeProgress: 100,
|
||
progressColor: '#00ff88',
|
||
textColor: '#fff',
|
||
showDetailPage: false,
|
||
selectedItem: null,
|
||
selectedIndex: 0,
|
||
updateInterval: null,
|
||
// Vela电池优化:性能模式控制
|
||
isVisible: true,
|
||
performanceMode: 'normal', // normal, power-save, high-performance
|
||
lastUpdateTime: 0,
|
||
// 配置相关
|
||
appTitle: "TOTP验证器",
|
||
refreshInterval: 100,
|
||
// interconnect状态
|
||
isConnected: false,
|
||
connectionStatus: '未连接'
|
||
},
|
||
|
||
onInit() {
|
||
console.log('TOTP应用初始化')
|
||
|
||
// 初始化interconnect连接
|
||
this.initInterconnect()
|
||
|
||
// 只从本地存储加载账户(不再从配置文件加载)
|
||
this.loadAccountsFromStorage()
|
||
|
||
// 设置interconnect消息监听
|
||
this.setupInterconnectListener()
|
||
|
||
// 设置初始颜色
|
||
this.updateColors(this.timeLeft)
|
||
|
||
this.startTimer()
|
||
},
|
||
|
||
initInterconnect() {
|
||
console.log('初始化interconnect连接')
|
||
try {
|
||
interconnectConn = interconnect.instance()
|
||
|
||
interconnectConn.getReadyState({
|
||
success: data => {
|
||
console.log('interconnect状态:', data.status)
|
||
if (data.status === 1) {
|
||
this.isConnected = true
|
||
this.connectionStatus = '已连接'
|
||
console.log('interconnect连接成功')
|
||
} else if (data.status === 2) {
|
||
this.isConnected = false
|
||
this.connectionStatus = '连接失败'
|
||
console.log('interconnect连接失败')
|
||
}
|
||
},
|
||
fail: (data, code) => {
|
||
console.log('获取interconnect状态失败, code =', code)
|
||
this.connectionStatus = '状态检查失败'
|
||
}
|
||
})
|
||
|
||
// 在initInterconnect中不设置onmessage,使用单独的方法
|
||
// 监听消息接收将在setupInterconnectListener中设置
|
||
|
||
// 监听连接打开
|
||
interconnectConn.onopen = () => {
|
||
console.log('interconnect连接已打开')
|
||
this.isConnected = true
|
||
this.connectionStatus = '已连接'
|
||
|
||
// 连接成功后,请求当前账户列表给手机
|
||
this.sendCurrentAccountsToPhone()
|
||
}
|
||
|
||
// 监听连接关闭
|
||
interconnectConn.onclose = (data) => {
|
||
console.log('interconnect连接已关闭, reason =', data.data, ', code =', data.code)
|
||
this.isConnected = false
|
||
this.connectionStatus = '连接已关闭'
|
||
}
|
||
|
||
// 监听连接错误
|
||
interconnectConn.onerror = (data) => {
|
||
console.log('interconnect连接错误, errMsg =', data.data, ', errCode =', data.code)
|
||
this.isConnected = false
|
||
this.connectionStatus = '连接错误'
|
||
}
|
||
} catch (error) {
|
||
console.log('初始化interconnect失败:', error)
|
||
this.connectionStatus = '初始化失败'
|
||
}
|
||
},
|
||
|
||
setupInterconnectListener() {
|
||
if (!interconnectConn) {
|
||
console.log('interconnect连接不存在,无法设置监听')
|
||
return
|
||
}
|
||
|
||
// 完全模仿BandTOTP-Band的消息处理逻辑
|
||
interconnectConn.onmessage = (data) => {
|
||
try {
|
||
console.log('接收到手机消息:', data.data)
|
||
let parsedData = JSON.parse(data.data)
|
||
|
||
if (parsedData.list && Array.isArray(parsedData.list)) {
|
||
parsedData.list.forEach((e) => {
|
||
// 查找是否已存在相同密钥
|
||
let index = this.totpList.findIndex((a) => e.key == a.key)
|
||
|
||
if (index != -1) {
|
||
// 密钥重复:只更新名字和用户名
|
||
this.totpList[index].name = e.name
|
||
this.totpList[index].usr = e.usr
|
||
} else {
|
||
// 新密钥:创建新实例并添加
|
||
this.totpList.push(new TOTPKey(
|
||
e.key,
|
||
e.name,
|
||
e.usr || '',
|
||
e.type || 'totp',
|
||
e.algorithm || 'SHA1',
|
||
e.digits || 6,
|
||
e.period || 30
|
||
))
|
||
}
|
||
})
|
||
|
||
// 关键:立即保存到存储(学习BandTOTP-Band)
|
||
this.saveAccountsToStorage()
|
||
|
||
console.log('成功更新', this.totpList.length, '个账户')
|
||
|
||
// 震动反馈表示数据同步成功
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
setTimeout(() => {
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
}, 100)
|
||
}
|
||
|
||
// 处理获取账户列表请求
|
||
if (parsedData.cmd === 'getAccounts') {
|
||
console.log('手机请求获取账户列表')
|
||
this.sendCurrentAccountsToPhone()
|
||
}
|
||
|
||
// 处理删除账户请求
|
||
if (parsedData.cmd === 'deleteAccount' && parsedData.key) {
|
||
console.log('手机请求删除账户:', parsedData.key)
|
||
this.deleteAccountByKey(parsedData.key)
|
||
}
|
||
|
||
// 处理重排序账户请求
|
||
if (parsedData.cmd === 'reorderAccounts' && parsedData.accounts) {
|
||
console.log('手机请求重排序账户')
|
||
this.reorderAccounts(parsedData.accounts)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.log('处理手机消息失败:', error)
|
||
}
|
||
}
|
||
},
|
||
|
||
sendCurrentAccountsToPhone() {
|
||
if (!interconnectConn || !this.isConnected) {
|
||
console.log('interconnect未连接,无法发送账户列表')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 构造与Android app兼容的数据格式
|
||
const accountsData = {
|
||
list: this.totpList.map(account => ({
|
||
name: account.name,
|
||
usr: account.usr,
|
||
key: account.key,
|
||
algorithm: account.algorithm || 'SHA1',
|
||
digits: account.digits || 6,
|
||
period: account.period || 30,
|
||
type: account.type || 'totp'
|
||
}))
|
||
}
|
||
|
||
console.log('发送账户列表到手机:', accountsData)
|
||
|
||
interconnectConn.send({
|
||
data: accountsData,
|
||
success: () => {
|
||
console.log('账户列表发送成功')
|
||
},
|
||
fail: (data) => {
|
||
console.log('账户列表发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.log('构造发送数据失败:', error)
|
||
}
|
||
},
|
||
|
||
loadAccountsFromStorage() {
|
||
storage.get({
|
||
key: 'keys',
|
||
success: (data) => {
|
||
if (data) {
|
||
console.log('从存储加载账户:', data)
|
||
try {
|
||
const parsedData = JSON.parse(data)
|
||
if (parsedData && parsedData.list && Array.isArray(parsedData.list)) {
|
||
// 清空现有列表,避免重复加载
|
||
this.totpList = []
|
||
|
||
parsedData.list.forEach((account) => {
|
||
this.totpList.push(new TOTPKey(
|
||
account.key,
|
||
account.name,
|
||
account.usr || '',
|
||
account.type || 'totp',
|
||
account.algorithm || 'SHA1',
|
||
account.digits || 6,
|
||
account.period || 30
|
||
))
|
||
})
|
||
console.log('从存储加载了', this.totpList.length, '个账户')
|
||
}
|
||
} catch (error) {
|
||
console.log('解析存储数据失败:', error)
|
||
}
|
||
} else {
|
||
// 如果没有存储数据,确保列表为空
|
||
console.log('存储中没有账户数据')
|
||
this.totpList = []
|
||
}
|
||
},
|
||
fail: (data, code) => {
|
||
console.log('从存储加载账户失败:', code)
|
||
// 加载失败时也确保列表为空,避免使用旧数据
|
||
this.totpList = []
|
||
}
|
||
})
|
||
},
|
||
|
||
saveAccountsToStorage() {
|
||
const saveData = {
|
||
list: this.totpList.map(account => ({
|
||
name: account.name,
|
||
usr: account.usr,
|
||
key: account.key,
|
||
algorithm: account.algorithm || 'SHA1',
|
||
digits: account.digits || 6,
|
||
period: account.period || 30,
|
||
type: account.type || 'totp'
|
||
}))
|
||
}
|
||
|
||
storage.set({
|
||
key: 'keys',
|
||
value: JSON.stringify(saveData)
|
||
})
|
||
console.log('账户保存到存储,数量:', this.totpList.length)
|
||
},
|
||
|
||
deleteAccountByKey(keyToDelete) {
|
||
const initialCount = this.totpList.length
|
||
|
||
// 根据key删除账户
|
||
this.totpList = this.totpList.filter(account => account.key !== keyToDelete)
|
||
|
||
const deletedCount = initialCount - this.totpList.length
|
||
|
||
if (deletedCount > 0) {
|
||
// 保存更新后的账户列表
|
||
this.saveAccountsToStorage()
|
||
|
||
console.log('成功删除账户,删除数量:', deletedCount, '剩余数量:', this.totpList.length)
|
||
|
||
// 删除成功震动反馈
|
||
vibrator.vibrate({
|
||
mode: 'long'
|
||
})
|
||
|
||
// 如果正在查看被删除的账户详情,关闭详情页
|
||
if (this.showDetailPage && this.selectedItem && this.selectedItem.key === keyToDelete) {
|
||
this.hideDetail()
|
||
}
|
||
|
||
// 向手机发送删除成功确认
|
||
this.sendDeleteConfirmationToPhone(keyToDelete, true)
|
||
} else {
|
||
console.log('未找到要删除的账户:', keyToDelete)
|
||
|
||
// 向手机发送删除失败确认
|
||
this.sendDeleteConfirmationToPhone(keyToDelete, false)
|
||
}
|
||
},
|
||
|
||
sendDeleteConfirmationToPhone(deletedKey, success) {
|
||
if (!interconnectConn || !this.isConnected) {
|
||
console.log('interconnect未连接,无法发送删除确认')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const confirmationData = {
|
||
cmd: 'deleteConfirmation',
|
||
key: deletedKey,
|
||
success: success
|
||
}
|
||
|
||
console.log('发送删除确认到手机:', confirmationData)
|
||
|
||
interconnectConn.send({
|
||
data: confirmationData,
|
||
success: () => {
|
||
console.log('删除确认发送成功')
|
||
},
|
||
fail: (data) => {
|
||
console.log('删除确认发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.log('构造删除确认数据失败:', error)
|
||
}
|
||
},
|
||
|
||
reorderAccounts(newAccounts) {
|
||
try {
|
||
console.log('开始重排序账户,新顺序数量:', newAccounts.length)
|
||
|
||
// 根据新顺序重建账户列表
|
||
const reorderedList = []
|
||
|
||
newAccounts.forEach((accountData) => {
|
||
// 在现有列表中查找匹配的账户
|
||
const existingAccount = this.totpList.find(account => account.key === accountData.key)
|
||
|
||
if (existingAccount) {
|
||
// 保持现有的TOTPKey实例,只更新顺序
|
||
reorderedList.push(existingAccount)
|
||
} else {
|
||
// 如果没找到,创建新的TOTPKey实例
|
||
console.log('创建新账户实例:', accountData.name)
|
||
reorderedList.push(new TOTPKey(
|
||
accountData.key,
|
||
accountData.name,
|
||
accountData.usr || '',
|
||
accountData.type || 'totp',
|
||
accountData.algorithm || 'SHA1',
|
||
accountData.digits || 6,
|
||
accountData.period || 30
|
||
))
|
||
}
|
||
})
|
||
|
||
// 更新账户列表
|
||
this.totpList = reorderedList
|
||
|
||
// 保存到存储
|
||
this.saveAccountsToStorage()
|
||
|
||
console.log('账户重排序完成,新顺序数量:', this.totpList.length)
|
||
|
||
// 重排序成功震动反馈
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
setTimeout(() => {
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
}, 100)
|
||
|
||
// 向手机发送重排序成功确认
|
||
this.sendReorderConfirmationToPhone(true)
|
||
|
||
} catch (error) {
|
||
console.log('重排序账户失败:', error)
|
||
this.sendReorderConfirmationToPhone(false)
|
||
}
|
||
},
|
||
|
||
sendReorderConfirmationToPhone(success) {
|
||
if (!interconnectConn || !this.isConnected) {
|
||
console.log('interconnect未连接,无法发送重排序确认')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const confirmationData = {
|
||
cmd: 'reorderConfirmation',
|
||
success: success
|
||
}
|
||
|
||
console.log('发送重排序确认到手机:', confirmationData)
|
||
|
||
interconnectConn.send({
|
||
data: confirmationData,
|
||
success: () => {
|
||
console.log('重排序确认发送成功')
|
||
},
|
||
fail: (data) => {
|
||
console.log('重排序确认发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.log('构造重排序确认数据失败:', error)
|
||
}
|
||
},
|
||
|
||
// 已移除loadAccountsFromConfig - 只能通过手机端管理账户
|
||
|
||
onShow() {
|
||
this.isVisible = true
|
||
this.updateCodes()
|
||
this.startTimer()
|
||
|
||
// 页面显示时,如果已连接则发送当前账户列表
|
||
if (this.isConnected) {
|
||
this.sendCurrentAccountsToPhone()
|
||
}
|
||
},
|
||
|
||
onHide() {
|
||
this.isVisible = false
|
||
// Vela优化:页面隐藏时切换到节能模式
|
||
this.performanceMode = 'power-save'
|
||
|
||
// Vela优化:页面隐藏时停止动画以节省电量
|
||
if (this.updateInterval) {
|
||
clearInterval(this.updateInterval)
|
||
this.updateInterval = null
|
||
}
|
||
// 清除所有动画状态
|
||
this.removeExpiringAnimation()
|
||
},
|
||
|
||
onDestroy() {
|
||
// Vela生命周期:确保清理所有资源
|
||
this.isVisible = false
|
||
if (this.updateInterval) {
|
||
clearInterval(this.updateInterval)
|
||
this.updateInterval = null
|
||
}
|
||
|
||
// 清理interconnect连接
|
||
if (interconnectConn) {
|
||
try {
|
||
interconnectConn = null
|
||
} catch (error) {
|
||
console.log('清理interconnect连接失败:', error)
|
||
}
|
||
}
|
||
},
|
||
|
||
updateCodes() {
|
||
|
||
// Vela优化:添加更新动画效果
|
||
this.totpList.forEach((item, index) => {
|
||
try {
|
||
const oldOtp = item.otp
|
||
item.update()
|
||
|
||
// 如果代码发生变化,触发更新动画
|
||
if (oldOtp !== item.otp && oldOtp !== "ERROR") {
|
||
this.triggerCodeUpdateAnimation()
|
||
}
|
||
} catch (error) {
|
||
item.otp = "ERROR"
|
||
}
|
||
})
|
||
},
|
||
|
||
triggerCodeUpdateAnimation() {
|
||
// 简化实现,避免DOM操作错误
|
||
},
|
||
|
||
updateColors(timeLeft) {
|
||
let newProgressColor, newTextColor
|
||
|
||
if (timeLeft <= 5) {
|
||
newProgressColor = '#ff6b6b'
|
||
newTextColor = '#ff6b6b'
|
||
} else if (timeLeft <= 10) {
|
||
newProgressColor = '#ffa502'
|
||
newTextColor = '#ffa502'
|
||
} else {
|
||
newProgressColor = '#00ff88'
|
||
newTextColor = '#fff'
|
||
}
|
||
|
||
// 只在颜色真正变化时才更新
|
||
if (this.progressColor !== newProgressColor) {
|
||
this.progressColor = newProgressColor
|
||
}
|
||
if (this.textColor !== newTextColor) {
|
||
this.textColor = newTextColor
|
||
}
|
||
},
|
||
|
||
startTimer() {
|
||
if (this.updateInterval) {
|
||
clearInterval(this.updateInterval)
|
||
}
|
||
if (this.progressInterval) {
|
||
clearInterval(this.progressInterval)
|
||
}
|
||
|
||
// 使用统一的更新函数,确保时间和进度条完全同步
|
||
const updateAll = () => {
|
||
const now = Date.now()
|
||
const millisecondsInCycle = now % 30000
|
||
const secondsInCycle = Math.floor(millisecondsInCycle / 1000)
|
||
const newTimeLeft = 30 - secondsInCycle
|
||
|
||
// 使用同一个时间源计算进度条和倒计时
|
||
this.timeProgress = (30000 - millisecondsInCycle) / 300
|
||
|
||
// 更新倒计时文字
|
||
if (this.timeLeft !== newTimeLeft) {
|
||
this.timeLeft = newTimeLeft
|
||
|
||
// 只在时间变化时更新颜色
|
||
this.updateColors(newTimeLeft)
|
||
|
||
// 添加紧急状态动画
|
||
if (newTimeLeft <= 5) {
|
||
this.addExpiringAnimation()
|
||
} else {
|
||
this.removeExpiringAnimation()
|
||
}
|
||
}
|
||
|
||
// 在新周期开始时更新验证码
|
||
if (millisecondsInCycle < 200 && secondsInCycle === 0) {
|
||
this.updateCodes()
|
||
|
||
// 验证码刷新时短震动+长震动提醒
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
setTimeout(() => {
|
||
vibrator.vibrate({
|
||
mode: 'long'
|
||
})
|
||
}, 50)
|
||
}
|
||
}
|
||
|
||
// 立即执行一次
|
||
updateAll()
|
||
|
||
// 统一使用10ms更新,确保进度条和时间完全同步
|
||
this.updateInterval = setInterval(updateAll, 10)
|
||
},
|
||
|
||
restartTimer() {
|
||
// 重新启动定时器以应用新的性能设置
|
||
this.startTimer()
|
||
},
|
||
|
||
addExpiringAnimation() {
|
||
// 简化实现,避免DOM操作错误
|
||
},
|
||
|
||
removeExpiringAnimation() {
|
||
// 简化实现,避免DOM操作错误
|
||
},
|
||
|
||
showDetail(index) {
|
||
if (this.totpList.length > 0 && index >= 0 && index < this.totpList.length) {
|
||
this.selectedIndex = index
|
||
this.selectedItem = this.totpList[index]
|
||
this.showDetailPage = true
|
||
|
||
// 进入详情页震动反馈
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
}
|
||
},
|
||
|
||
hideDetail() {
|
||
this.showDetailPage = false
|
||
this.selectedItem = null
|
||
|
||
// 返回时震动反馈
|
||
vibrator.vibrate({
|
||
mode: 'short'
|
||
})
|
||
},
|
||
|
||
onBackPress() {
|
||
console.log('触发:onBackPress')
|
||
// 如果详情页打开,则关闭详情页返回列表
|
||
if (this.showDetailPage) {
|
||
this.hideDetail()
|
||
return true // 自己处理,不执行系统默认返回
|
||
}
|
||
// 否则执行系统默认返回行为(退出应用)
|
||
return false
|
||
}
|
||
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.totp-app {
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #000;
|
||
position: fixed;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 顶部标题 */
|
||
.header {
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 10px;
|
||
right: 10px;
|
||
height: 40px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: #000;
|
||
flex-shrink: 0;
|
||
/* 移除多余的边框 */
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 24px;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
}
|
||
|
||
.header-divider {
|
||
position: absolute;
|
||
top: 45px;
|
||
left: 30px;
|
||
right: 30px;
|
||
height: 1px;
|
||
background: linear-gradient(to right, transparent, #333, transparent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 列表区域 - 简化版本 */
|
||
.totp-list {
|
||
background-color: #000;
|
||
}
|
||
|
||
.totp-item {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* 空状态占位符 */
|
||
.empty-placeholder {
|
||
flex: 1;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 56px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 20px;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 底部并排倒计时 */
|
||
.countdown-container {
|
||
position: absolute;
|
||
bottom: 0px;
|
||
left: 0px;
|
||
right: 0px;
|
||
height: 48px;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.countdown-text {
|
||
color: #fff;
|
||
transition: color 500ms ease-out;
|
||
}
|
||
|
||
/* 详情页面 */
|
||
.detail-page {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #000;
|
||
flex-direction: column;
|
||
z-index: 100;
|
||
}
|
||
|
||
.detail-content {
|
||
flex: 1;
|
||
padding: 30px 15px 15px 15px;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.detail-service {
|
||
font-size: 40px;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-account {
|
||
font-size: 30px;
|
||
color: #999;
|
||
margin-bottom: 25px;
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-code-container {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background-color: #1a1a1a;
|
||
border-radius: 12px;
|
||
border: 2px solid #333;
|
||
height: 120px;
|
||
width: 280px;
|
||
}
|
||
|
||
.detail-code-row {
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.detail-code-digit {
|
||
width: 36px;
|
||
height: 42px;
|
||
margin: 0 1px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.detail-code-num {
|
||
font-size: 36px;
|
||
color: #00ff88;
|
||
font-weight: bold;
|
||
font-family: monospace;
|
||
text-align: center;
|
||
line-height: 40px;
|
||
}
|
||
|
||
.detail-code-single {
|
||
font-size: 36px;
|
||
color: #00ff88;
|
||
font-weight: bold;
|
||
font-family: monospace;
|
||
letter-spacing: 3px;
|
||
text-align: center;
|
||
line-height: 40px;
|
||
}
|
||
|
||
.detail-time {
|
||
font-size: 30px;
|
||
color: #AAA;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 底部按钮容器 */
|
||
.detail-footer {
|
||
height: 100px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.detail-back-btn {
|
||
width: 70px;
|
||
height: 70px;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
border-radius: 35px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border: 2px solid #333;
|
||
}
|
||
|
||
.detail-back-icon {
|
||
font-size: 28px;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
</style>
|