// index.js const { generateCode, getRemainingSeconds, formatTime, syncTokens } = require('../../utils/util'); const storage = require('../../utils/storage'); const eventManager = require('../../utils/eventManager'); Page({ data: { tokens: [], remainingSeconds: {}, // 存储每个令牌的剩余时间 loading: true, syncing: false, error: null }, onLoad: function() { // 初始化页面数据 this.loadTokens(); // 监听令牌更新事件 this.tokensUpdatedListener = (tokens) => { this.loadTokens(); }; eventManager.on('tokensUpdated', this.tokensUpdatedListener); // 初始化页面路由 this.pageRoutes = { form: '/pages/form/form', edit: '/pages/edit/edit' }; }, onShow: function() { // 先加载令牌列表 this.loadTokens(); // 然后延迟100ms刷新验证码,确保tokens已经加载 setTimeout(() => { this.refreshTokens(); }, 100); }, onPullDownRefresh: function() { this.refreshTokens().then(() => { wx.stopPullDownRefresh(); }); }, async loadTokens() { try { this.setData({ loading: true, error: null }); // 从本地存储加载令牌 const tokens = await wx.getStorageSync('tokens') || []; // console.log('[DEBUG] Loaded tokens count:', tokens.length); // 统计令牌类型 const typeCounts = tokens.reduce((acc, token) => { acc[token.type] = (acc[token.type] || 0) + 1; return acc; }, {}); // console.log('[DEBUG] Token types:', typeCounts); // 初始化剩余时间 const remainingSeconds = {}; tokens.forEach(token => { if (token.type === 'totp') { const period = token.period || 30; const remaining = getRemainingSeconds(period); // console.log(`[DEBUG] Token ${token.id} period=${period}, remaining=${remaining}`); remainingSeconds[token.id] = remaining; } }); // 获取当前时间戳 const timestamp = Math.floor(Date.now() / 1000); // 先更新验证码 let tokensToSet = [...tokens]; // 创建tokens的副本 if (tokensToSet.length > 0) { const updatedTokens = await this.updateTokenCodes(tokensToSet); if (Array.isArray(updatedTokens)) { tokensToSet = updatedTokens; } } // 设置状态 this.setData({ tokens: tokensToSet, remainingSeconds, loading: false }); // 设置定时器,定期更新TOTP令牌 this.setupRefreshTimer(); } catch (error) { console.error('Failed to load tokens:', error); this.setData({ error: '加载令牌失败: ' + error.message, loading: false }); } }, async refreshTokens() { try { const currentTokens = [...this.data.tokens]; // 创建tokens的副本 const updatedTokens = await this.updateTokenCodes(currentTokens); // 更新剩余时间 const remainingSeconds = {}; updatedTokens.forEach(token => { if (token.type === 'totp') { remainingSeconds[token.id] = getRemainingSeconds(token.period || 30); } }); this.setData({ tokens: updatedTokens, remainingSeconds }); } catch (error) { console.error('Failed to refresh tokens:', error); this.setData({ error: '刷新令牌失败: ' + error.message }); } }, setupRefreshTimer() { // 清除现有的定时器 if (this.refreshTimer) { clearInterval(this.refreshTimer); } // console.log('[DEBUG] Setting up refresh timer...'); // 设置新的定时器,每秒检查一次 this.refreshTimer = setInterval(() => { // console.log('[DEBUG] Timer tick - checking tokens...'); const currentTokens = [...this.data.tokens]; // 创建tokens的副本 const currentRemainingSeconds = this.data.remainingSeconds || {}; const remainingSeconds = {}; const tokensToUpdate = []; currentTokens.forEach(token => { // 处理所有支持的OTP类型 const supportedTypes = ['totp', 'hotp', 'otpauth']; if (!supportedTypes.includes(token.type)) { // console.log(`[DEBUG] Skipping unsupported token type: ${token.type}`); return; } // console.log(`[DEBUG] Processing ${token.type.toUpperCase()} token ${token.id || token.name}`); // 针对不同类型做特殊处理 switch(token.type) { case 'totp': // 标准TOTP处理逻辑 break; case 'hotp': // HOTP特定处理 // console.log(`[DEBUG] HOTP token detected, counter: ${token.counter}`); break; case 'otpauth': // 扫描二维码生成的令牌 // console.log(`[DEBUG] OTPAUTH URI token detected`); break; } const period = token.period || 30; const remaining = getRemainingSeconds(period); const prevRemaining = currentRemainingSeconds[token.id]; // console.log(`[DEBUG] Token ${token.id}: prev=${prevRemaining}, curr=${remaining}`); // 更新剩余时间 remainingSeconds[token.id] = remaining; // 在以下情况下更新token: // 1. 剩余时间为0(新的时间窗口开始) // 2. 之前没有剩余时间记录(首次加载) // 3. 时间窗口发生变化(prevRemaining > remaining,说明跨越了时间窗口) if (remaining === 0 || prevRemaining === undefined || (prevRemaining > 0 && remaining < prevRemaining)) { // console.log(`[DEBUG] Token ${token.id} needs update`); tokensToUpdate.push({...token}); // 创建token的副本 } }); // 只在有变化时更新剩余时间 if (Object.keys(remainingSeconds).length > 0) { // console.log('[DEBUG] Updating remainingSeconds:', remainingSeconds); this.setData({ 'remainingSeconds': { ...currentRemainingSeconds, ...remainingSeconds } }, () => { // console.log('[DEBUG] remainingSeconds updated'); }); } // 只更新需要更新的token,并传入当前时间戳 if (tokensToUpdate.length > 0) { // console.log('[DEBUG] Updating tokens:', tokensToUpdate.length); const timestamp = Math.floor(Date.now() / 1000); this.updateSelectedTokens(tokensToUpdate, timestamp); } }, 1000); }, // 只更新选定的token async updateSelectedTokens(tokensToUpdate, timestamp) { try { const currentTokens = this.data.tokens; // 获取当前时间戳,如果没有传入 const currentTimestamp = timestamp || Math.floor(Date.now() / 1000); // 为每个token计算正确的时间戳 const updatePromises = tokensToUpdate.map(token => { // 优先使用令牌中的时间戳(如果有的话) const tokenTimestamp = token.timestamp || currentTimestamp; if (token.type === 'totp') { // 计算时间窗口的开始时间 const period = token.period || 30; const windowStart = Math.floor(tokenTimestamp / period) * period; return this.updateTokenCode(token, windowStart); } else { // 对于HOTP类型,直接使用时间戳 return this.updateTokenCode(token, tokenTimestamp); } }); const updatedTokens = await Promise.all(updatePromises); // 创建token ID到更新后token的映射 const updatedTokenMap = {}; updatedTokens.forEach(token => { updatedTokenMap[token.id] = token; }); // 更新tokens数组中的特定token const newTokens = currentTokens.map(token => updatedTokenMap[token.id] ? updatedTokenMap[token.id] : token ); this.setData({ tokens: newTokens }); } catch (error) { console.error('更新选定token失败:', error); } }, async updateTokenCodes(tokens) { if (!Array.isArray(tokens) || tokens.length === 0) { return []; } try { // 获取当前时间戳 const currentTimestamp = Math.floor(Date.now() / 1000); // 并行更新所有令牌的验证码,为每个令牌计算其时间窗口的开始时间 const updatePromises = tokens.map(token => { // 优先使用令牌中的时间戳(如果有的话) const tokenTimestamp = token.timestamp || currentTimestamp; if (token.type === 'totp') { // 计算时间窗口的开始时间 const period = token.period || 30; const windowStart = Math.floor(tokenTimestamp / period) * period; return this.updateTokenCode(token, windowStart); } else { // 对于HOTP类型,直接使用时间戳 return this.updateTokenCode(token, tokenTimestamp); } }); const updatedTokens = await Promise.all(updatePromises); return updatedTokens; } catch (error) { console.error('更新验证码失败:', error); // 出错时返回原始tokens,避免undefined return tokens; } }, async updateTokenCode(token, timestamp) { try { // 生成新的验证码,使用传入的统一时间戳 const code = await generateCode({ type: token.type, secret: token.secret, algorithm: token.algorithm || 'SHA1', digits: token.digits || 6, period: token.period || 30, counter: token.counter, timestamp: timestamp // 使用传入的统一时间戳 }); // 更新令牌数据 token.code = code; token.lastUpdate = formatTime(new Date()); return token; } catch (error) { console.error(`更新令牌 ${token.id} 失败:`, error); token.error = error.message; return token; } }, async refreshHotpToken(event) { const tokenId = event.currentTarget.dataset.tokenId; const token = this.data.tokens.find(t => t.id === tokenId); if (!token) return; try { // 增加计数器值 const newCounter = (token.counter || 0) + 1; // 生成新的验证码 const code = await generateCode({ type: 'hotp', secret: token.secret, algorithm: token.algorithm || 'SHA1', counter: newCounter, digits: token.digits || 6 }); // 更新令牌数据 token.code = code; token.counter = newCounter; token.lastUpdate = formatTime(new Date()); // 更新存储和状态 const tokens = this.data.tokens.map(t => t.id === token.id ? token : t ); await wx.setStorageSync('tokens', tokens); this.setData({ tokens }); // 同步到云端 if (this.data.syncing) { await syncTokens(tokens); } } catch (error) { console.error(`刷新HOTP令牌 ${token.id} 失败:`, error); this.setData({ error: '刷新HOTP令牌失败: ' + error.message }); } }, async syncWithCloud() { try { this.setData({ syncing: true, error: null }); // 获取本地令牌 const localTokens = this.data.tokens; // 同步令牌 const syncedTokens = await syncTokens(localTokens); // 更新所有令牌的验证码 await this.updateTokenCodes(syncedTokens); // 更新存储和状态 await wx.setStorageSync('tokens', syncedTokens); this.setData({ tokens: syncedTokens, syncing: false }); } catch (error) { console.error('同步失败:', error); this.setData({ error: '同步失败: ' + error.message, syncing: false }); } }, onUnload: function() { // 清除定时器 if (this.refreshTimer) { clearInterval(this.refreshTimer); } // 移除令牌更新事件监听 if (this.tokensUpdatedListener) { eventManager.off('tokensUpdated', this.tokensUpdatedListener); } }, // 处理令牌点击事件 handleTokenTap(event) { const tokenId = event.currentTarget.dataset.tokenId; const token = this.data.tokens.find(t => t.id === tokenId); if (!token) return; // 复制验证码到剪贴板 wx.setClipboardData({ data: token.code, success: () => { wx.showToast({ title: '验证码已复制', icon: 'success', duration: 1500 }); } }); }, // 显示添加令牌菜单 showAddTokenMenu() { wx.showActionSheet({ itemList: ['扫描二维码', '手动添加'], success: (res) => { switch (res.tapIndex) { case 0: // 扫描二维码 wx.scanCode({ onlyFromCamera: false, scanType: ['qrCode'], success: (res) => { try { // 解析二维码内容 const qrContent = res.result; // 导入util.js中的parseURL函数 const { parseURL } = require('../../utils/util'); // 使用parseURL函数解析二维码内容 const parsedToken = parseURL(qrContent); if (parsedToken) { // 构建表单数据 const formData = { type: parsedToken.type, issuer: parsedToken.issuer || '', account: parsedToken.account || '', secret: parsedToken.secret || '', algorithm: parsedToken.algorithm || 'SHA1', digits: parsedToken.digits || 6, period: parsedToken.type === 'totp' ? (parsedToken.period || 30) : undefined, counter: parsedToken.type === 'hotp' ? (parsedToken.counter || 0) : undefined }; // 验证必要参数 if (formData.digits < 6 || formData.digits > 8) { formData.digits = 6; console.warn('验证码位数无效,已设置为默认值6'); } if (formData.type === 'totp' && (formData.period < 15 || formData.period > 60)) { formData.period = 30; console.warn('TOTP周期无效,已设置为默认值30秒'); } // 检查必要参数是否完整 if (formData.secret && formData.type) { // 直接添加令牌 this.addTokenDirectly(formData).then(() => { wx.showToast({ title: '令牌已添加', icon: 'success' }); }).catch(error => { console.error('添加令牌失败:', error); // 失败后跳转到表单页面 const queryString = Object.entries(formData) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) .join('&'); wx.navigateTo({ url: `${this.pageRoutes.form}?${queryString}` }); }); } else { // 参数不完整,跳转到表单页面 const queryString = Object.entries(formData) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) .join('&'); wx.navigateTo({ url: `${this.pageRoutes.form}?${queryString}` }); } } else { wx.showToast({ title: '无效的二维码格式', icon: 'error' }); } } catch (error) { console.error('解析二维码失败:', error); wx.showToast({ title: '解析二维码失败', icon: 'error' }); } }, fail: (error) => { console.error('扫描二维码失败:', error); if (error.errMsg.includes('cancel')) { // 用户取消扫码,不显示错误提示 return; } wx.showToast({ title: '扫描失败', icon: 'error' }); } }); break; case 1: // 手动添加 wx.navigateTo({ url: '/pages/form/form', events: { // 监听form页面的事件 tokenAdded: () => { this.loadTokens(); } }, success: () => { // 跳转成功后的回调 wx.showToast({ title: '请填写令牌信息', icon: 'none', duration: 1000 }); } }); break; } } }); }, // 编辑令牌 editToken(event) { const id = event.currentTarget.dataset.tokenId; wx.navigateTo({ url: `${this.pageRoutes.edit}?token_id=${id}` }); }, // 删除令牌 deleteToken(event) { const id = event.currentTarget.dataset.id; const that = this; wx.showModal({ title: '确认删除', content: '确定要删除这个令牌吗?', success(res) { if (res.confirm) { // 从本地存储中删除 wx.getStorage({ key: 'tokens', success(res) { const tokens = res.data.filter(token => token.id !== id); wx.setStorage({ key: 'tokens', data: tokens, success() { // 更新UI that.setData({ tokens }); wx.showToast({ title: '删除成功', icon: 'success' }); // 如果正在同步,更新云端 if (that.data.syncing) { syncTokens(tokens).catch(err => { console.error('同步删除失败:', err); }); } }, fail() { wx.showToast({ title: '删除失败', icon: 'none' }); } }); } }); } } }); }, // 直接添加令牌 async addTokenDirectly(tokenData) { try { // 获取当前令牌列表 const tokens = await wx.getStorageSync('tokens') || []; // 生成唯一ID和时间戳 // 生成唯一ID和时间戳 const newToken = { ...tokenData, id: Date.now().toString(), createTime: formatTime(new Date()), lastUpdate: formatTime(new Date()), code: '' // 初始化为空字符串 }; // 对于HOTP类型,添加counter字段 if (tokenData.type && tokenData.type.toUpperCase() === 'HOTP') { newToken.counter = 0; // HOTP类型需要counter >= 0 } // 对于TOTP类型,不设置counter字段,让它在JSON序列化时被忽略 // 如果是TOTP类型,先初始化剩余时间 if ((newToken.type || 'totp').toLowerCase() === 'totp') { const period = parseInt(newToken.period || '30', 10); const currentRemainingSeconds = this.data.remainingSeconds || {}; // 先更新剩余时间,避免闪烁 this.setData({ remainingSeconds: { ...currentRemainingSeconds, [newToken.id]: getRemainingSeconds(period) } }); } // 生成验证码 try { // 确保类型是字符串且为小写 const type = (newToken.type || 'totp').toLowerCase(); // 确保digits是数字 const digits = parseInt(newToken.digits || '6', 10); // 确保period是数字 const period = parseInt(newToken.period || '30', 10); // 确保counter是数字(如果存在) const counter = newToken.counter ? parseInt(newToken.counter, 10) : undefined; const code = await generateCode({ type, secret: newToken.secret, algorithm: newToken.algorithm || 'SHA1', digits, period, counter }); newToken.code = code; } catch (error) { console.error('生成验证码失败:', error); wx.showToast({ title: '生成验证码失败: ' + error.message, icon: 'none', duration: 2000 }); throw error; } // 添加到列表并保存 const updatedTokens = [...tokens, newToken]; await wx.setStorageSync('tokens', updatedTokens); // 更新UI,使用setData的回调确保UI更新完成 this.setData({ tokens: updatedTokens }, () => { // 触发更新事件 eventManager.emit('tokensUpdated', updatedTokens); // 如果正在同步,更新云端 if (this.data.syncing) { syncTokens(updatedTokens).catch(err => { console.error('同步新令牌失败:', err); }); } }); } catch (error) { console.error('直接添加令牌失败:', error); throw error; } }, // 阻止触摸移动 catchTouchMove() { return false; } });