Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490611335c | ||
|
|
2786542ffd | ||
|
|
a8329f1179 | ||
|
|
031e4823b9 |
No files matched your search
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "hello",
|
||||
"name": "totp",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hello",
|
||||
"name": "totp",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"name": "Microsoft",
|
||||
"username": "example@outlook.com",
|
||||
"secret": "JBSWY3DPEHPK3PXP",
|
||||
"enabled": true,
|
||||
"type": "totp"
|
||||
},
|
||||
{
|
||||
"name": "GitHub",
|
||||
"username": "myusername",
|
||||
"secret": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ",
|
||||
"enabled": true,
|
||||
"type": "totp"
|
||||
},
|
||||
{
|
||||
"name": "Steam",
|
||||
"username": "steamuser",
|
||||
"secret": "NBSWY3DPEHPK3PXPNBSWY3DPEHPK3PXP",
|
||||
"enabled": true,
|
||||
"type": "steam"
|
||||
},
|
||||
{
|
||||
"name": "Work Server",
|
||||
"username": "employee",
|
||||
"secret": "MFRGG2LTEBYGCZLSMUFA====",
|
||||
"enabled": true,
|
||||
"type": "totp"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft",
|
||||
"username": "user@example.com",
|
||||
"secret": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
||||
"enabled": false,
|
||||
"type": "totp"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"title": "TOTP",
|
||||
"refreshInterval": 20,
|
||||
"maxDisplayAccounts": 10
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"package": "com.deepfal.totp.authenticator",
|
||||
"package": "cn.deepfal.band.TOTPauthenticator",
|
||||
"name": "TOTP",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": 1,
|
||||
"versionName": "1.1.0",
|
||||
"versionCode": 2,
|
||||
"minPlatformVersion": 1000,
|
||||
"icon": "/common/logo.png",
|
||||
"deviceTypeList": [
|
||||
@@ -14,6 +14,12 @@
|
||||
},
|
||||
{
|
||||
"name": "system.vibrator"
|
||||
},
|
||||
{
|
||||
"name": "system.interconnect"
|
||||
},
|
||||
{
|
||||
"name": "system.storage"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
|
||||
+554
-48
@@ -9,14 +9,16 @@
|
||||
<div class="header-divider"></div>
|
||||
|
||||
<!-- 账户列表 -->
|
||||
<list class="totp-list" style="position: absolute; top: 70px; bottom: 70px; left: 2px; right: 2px; width: 208px;">
|
||||
<list-item class="totp-item" style="height: 115px; margin-bottom: 10px; background-color: #1a1a1a; border-radius: 15px; border: 1px solid #333; flex-direction: column; justify-content: space-between; padding: 8px;" type="account" for="{{totpList}}" onclick="showDetail($idx)">
|
||||
<div style="flex-direction: column; width: 100%; margin-bottom: 8px;">
|
||||
<text style="font-size: 24px; color: #fff; font-weight: bold; margin-bottom: 3px; text-align: left; width: 100%; line-height: 26px;">{{$item.name}}</text>
|
||||
<text style="font-size: 18px; color: #999; text-align: left; width: 100%; line-height: 22px;" show="{{$item.usr}}">{{$item.usr}}</text>
|
||||
<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="align-items: center; justify-content: center; width: 100%; height: 45px;">
|
||||
<text style="font-size: 32px; color: #00ff88; font-family: monospace; font-weight: bold; letter-spacing: 4px; text-align: center; width: 100%; line-height: 36px;">{{$item.otp}}</text>
|
||||
<!-- 右侧:验证码 -->
|
||||
<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>
|
||||
@@ -27,12 +29,21 @@
|
||||
<text class="empty-text">暂无验证器</text>
|
||||
</div>
|
||||
|
||||
<!-- 底部弧形倒计时进度条 -->
|
||||
<!-- 底部倒计时和进度条并排 -->
|
||||
<div class="countdown-container" show="{{totpList.length > 0}}">
|
||||
<!-- 弧形进度条 -->
|
||||
<progress style="position: absolute; left: 0px; bottom: 10px; center-x: 106px; center-y: 0px; width: 212px; height: 100px; radius: 100px; start-angle: 240deg; total-angle: -120deg; stroke-width: 8px; background-color: rgba(255, 255, 255, 0.2); color: {{progressColor}};" percent="{{timeProgress}}" type="arc"></progress>
|
||||
<!-- 倒计时文字 -->
|
||||
<text class="countdown-text" style="position: absolute; bottom: 35px; left: 0px; right: 0px; text-align: center; font-size: 20px; font-weight: bold; color: {{textColor}}; transition: color 500ms ease-out;">{{timeLeft}}秒</text>
|
||||
<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>
|
||||
|
||||
<!-- 详情页面 -->
|
||||
@@ -86,16 +97,20 @@
|
||||
|
||||
<script>
|
||||
import { TOTP, SteamTotp } from '../../utils/totp'
|
||||
import accountsConfig from '../../common/accounts.json'
|
||||
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') {
|
||||
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.type = type // 'totp' 或 'steam'
|
||||
this.algorithm = algorithm
|
||||
this.digits = digits
|
||||
this.period = period
|
||||
this.update()
|
||||
}
|
||||
|
||||
@@ -105,7 +120,7 @@ class TOTPKey {
|
||||
if (this.type === 'steam') {
|
||||
this.otp = SteamTotp(this.key)
|
||||
} else {
|
||||
this.otp = TOTP(this.key)
|
||||
this.otp = TOTP(this.key, this.algorithm, this.digits, this.period)
|
||||
}
|
||||
} catch (error) {
|
||||
this.otp = "ERROR"
|
||||
@@ -113,6 +128,8 @@ class TOTPKey {
|
||||
}
|
||||
}
|
||||
|
||||
let interconnectConn = null
|
||||
|
||||
export default {
|
||||
private: {
|
||||
totpList: [],
|
||||
@@ -130,13 +147,23 @@ export default {
|
||||
lastUpdateTime: 0,
|
||||
// 配置相关
|
||||
appTitle: "TOTP验证器",
|
||||
refreshInterval: 100
|
||||
refreshInterval: 100,
|
||||
// interconnect状态
|
||||
isConnected: false,
|
||||
connectionStatus: '未连接'
|
||||
},
|
||||
|
||||
onInit() {
|
||||
console.log('TOTP应用初始化')
|
||||
|
||||
// 加载配置文件
|
||||
this.loadAccountsFromConfig()
|
||||
// 初始化interconnect连接
|
||||
this.initInterconnect()
|
||||
|
||||
// 只从本地存储加载账户(不再从配置文件加载)
|
||||
this.loadAccountsFromStorage()
|
||||
|
||||
// 设置interconnect消息监听
|
||||
this.setupInterconnectListener()
|
||||
|
||||
// 设置初始颜色
|
||||
this.updateColors(this.timeLeft)
|
||||
@@ -144,45 +171,393 @@ export default {
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
loadAccountsFromConfig() {
|
||||
initInterconnect() {
|
||||
console.log('初始化interconnect连接')
|
||||
try {
|
||||
interconnectConn = interconnect.instance()
|
||||
|
||||
// 加载设置
|
||||
if (accountsConfig.settings) {
|
||||
this.appTitle = accountsConfig.settings.title || "TOTP验证器"
|
||||
this.refreshInterval = accountsConfig.settings.refreshInterval || 100
|
||||
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()
|
||||
}
|
||||
|
||||
// 加载启用的账户
|
||||
const enabledAccounts = accountsConfig.accounts.filter(account => account.enabled)
|
||||
// 监听连接关闭
|
||||
interconnectConn.onclose = (data) => {
|
||||
console.log('interconnect连接已关闭, reason =', data.data, ', code =', data.code)
|
||||
this.isConnected = false
|
||||
this.connectionStatus = '连接已关闭'
|
||||
}
|
||||
|
||||
// 转换为TOTPKey实例
|
||||
this.totpList = enabledAccounts.map(account => {
|
||||
return new TOTPKey(
|
||||
account.secret,
|
||||
account.name,
|
||||
account.username,
|
||||
account.type || 'totp' // 如果没有指定type,默认为totp
|
||||
)
|
||||
// 监听连接错误
|
||||
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
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
this.totpList.forEach(() => {
|
||||
// 关键:立即保存到存储(学习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.error('Failed to load accounts config:', error)
|
||||
// 如果加载配置失败,使用默认账户
|
||||
this.loadDefaultAccounts()
|
||||
console.log('处理手机消息失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadDefaultAccounts() {
|
||||
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() {
|
||||
@@ -206,6 +581,15 @@ export default {
|
||||
clearInterval(this.updateInterval)
|
||||
this.updateInterval = null
|
||||
}
|
||||
|
||||
// 清理interconnect连接
|
||||
if (interconnectConn) {
|
||||
try {
|
||||
interconnectConn = null
|
||||
} catch (error) {
|
||||
console.log('清理interconnect连接失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateCodes() {
|
||||
@@ -343,6 +727,17 @@ export default {
|
||||
vibrator.vibrate({
|
||||
mode: 'short'
|
||||
})
|
||||
},
|
||||
|
||||
onBackPress() {
|
||||
console.log('触发:onBackPress')
|
||||
// 如果详情页打开,则关闭详情页返回列表
|
||||
if (this.showDetailPage) {
|
||||
this.hideDetail()
|
||||
return true // 自己处理,不执行系统默认返回
|
||||
}
|
||||
// 否则执行系统默认返回行为(退出应用)
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -375,7 +770,7 @@ export default {
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
@@ -383,9 +778,9 @@ export default {
|
||||
|
||||
.header-divider {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
top: 45px;
|
||||
left: 30px;
|
||||
right: 30px;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #333, transparent);
|
||||
flex-shrink: 0;
|
||||
@@ -409,23 +804,23 @@ export default {
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 56px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 底部弧形倒计时 */
|
||||
/* 底部并排倒计时 */
|
||||
.countdown-container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 100px;
|
||||
height: 48px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -433,4 +828,115 @@ export default {
|
||||
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>
|
||||
+20
-7
@@ -3,9 +3,9 @@ let timeOffset=0
|
||||
function time(){
|
||||
return Math.floor(Date.now() / 1000) + (timeOffset || 0);
|
||||
}
|
||||
function TOTP(key) {
|
||||
function TOTP(key, algorithm = 'SHA1', digits = 6, period = 30) {
|
||||
|
||||
const timeStep = 30; // 默认时间步长为 30 秒
|
||||
const timeStep = period; // 使用传入的时间周期
|
||||
const currentTime = time()
|
||||
const timeCounter = Math.floor(currentTime / timeStep).toString(16).padStart(16, '0'); // 时间计数器,16位
|
||||
|
||||
@@ -13,8 +13,20 @@ function TOTP(key) {
|
||||
// 使用 Base32 解码并返回 WordArray
|
||||
const decodedKey = base32Decode(key);
|
||||
|
||||
// 使用 HMAC-SHA1 计算哈希值
|
||||
const hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter),decodedKey);
|
||||
// 根据算法选择不同的HMAC函数
|
||||
let hmacHash;
|
||||
switch (algorithm.toUpperCase()) {
|
||||
case 'SHA256':
|
||||
hmacHash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||
break;
|
||||
case 'SHA512':
|
||||
hmacHash = CryptoJS.HmacSHA512(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||
break;
|
||||
case 'SHA1':
|
||||
default:
|
||||
hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||
break;
|
||||
}
|
||||
|
||||
// 动态截取
|
||||
const hmacBytes = hexToBytes(hmacHash.toString());
|
||||
@@ -25,8 +37,9 @@ function TOTP(key) {
|
||||
(hmacBytes[offset + 2] << 8) |
|
||||
hmacBytes[offset + 3];
|
||||
|
||||
// 生成 6 位 OTP
|
||||
const result = (otp % 1000000).toString().padStart(6, '0');
|
||||
// 生成指定位数的OTP
|
||||
const modulo = Math.pow(10, digits);
|
||||
const result = (otp % modulo).toString().padStart(digits, '0');
|
||||
return result
|
||||
} catch (error) {
|
||||
throw error
|
||||
@@ -77,7 +90,7 @@ function bufferizeSecret(secret) {
|
||||
if (secret.match(/^[0-9a-f]{40}$/i)) {
|
||||
return CryptoJS.enc.Hex.parse(secret); // 解析为 WordArray
|
||||
} else {
|
||||
// 假设是 base32 编码
|
||||
// 假设是 base32 编码(包括Steam的标准secret)
|
||||
return base32Decode(secret); // 解析为 WordArray
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user