init
This commit is contained in:
commit
2b8870a40e
51 changed files with 5845 additions and 0 deletions
721
pages/index/index.js
Normal file
721
pages/index/index.js
Normal file
|
@ -0,0 +1,721 @@
|
|||
// 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 => {
|
||||
if (token.type === 'totp') {
|
||||
// 计算时间窗口的开始时间
|
||||
const period = token.period || 30;
|
||||
const windowStart = Math.floor(currentTimestamp / period) * period;
|
||||
return this.updateTokenCode(token, windowStart);
|
||||
} else {
|
||||
// 对于HOTP类型,直接使用当前时间戳
|
||||
return this.updateTokenCode(token, currentTimestamp);
|
||||
}
|
||||
});
|
||||
|
||||
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 => {
|
||||
if (token.type === 'totp') {
|
||||
// 计算时间窗口的开始时间
|
||||
const period = token.period || 30;
|
||||
const windowStart = Math.floor(currentTimestamp / period) * period;
|
||||
return this.updateTokenCode(token, windowStart);
|
||||
} else {
|
||||
// 对于HOTP类型,直接使用当前时间戳
|
||||
return this.updateTokenCode(token, currentTimestamp);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
// 如果是otpauth://格式的URL
|
||||
if (qrContent.startsWith('otpauth://')) {
|
||||
// 小程序兼容的URL解析
|
||||
const [protocolAndPath, search] = qrContent.split('?');
|
||||
const [protocol, path] = protocolAndPath.split('://');
|
||||
const type = protocol.replace('otpauth:', '');
|
||||
|
||||
// 解析路径部分
|
||||
const decodedPath = decodeURIComponent(path.substring(1)); // 移除开头的/
|
||||
let [issuer, remark] = decodedPath.split(':');
|
||||
if (!remark) {
|
||||
remark = issuer;
|
||||
issuer = '';
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
const params = {};
|
||||
if (search) {
|
||||
search.split('&').forEach(pair => {
|
||||
const [key, value] = pair.split('=');
|
||||
if (key && value) {
|
||||
params[key] = decodeURIComponent(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从参数中获取issuer(如果存在)
|
||||
if (params.issuer) {
|
||||
issuer = params.issuer;
|
||||
}
|
||||
|
||||
// 验证secret参数
|
||||
if (!params.secret) {
|
||||
wx.showToast({
|
||||
title: '无效的二维码:缺少secret参数',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 将otpauth类型转换为实际类型
|
||||
let validType = type.toLowerCase();
|
||||
if (validType === 'otpauth') {
|
||||
// 从URI路径中提取实际类型
|
||||
validType = path.split('/')[0].toLowerCase();
|
||||
}
|
||||
|
||||
// 构建表单数据,确保数字类型参数正确转换
|
||||
const formData = {
|
||||
type: validType,
|
||||
issuer,
|
||||
remark,
|
||||
secret: params.secret,
|
||||
algorithm: params.algorithm || 'SHA1',
|
||||
digits: params.digits ? parseInt(params.digits, 10) : 6,
|
||||
period: validType === 'totp' ? (params.period ? parseInt(params.period, 10) : 30) : undefined,
|
||||
counter: validType === 'hotp' ? (params.counter ? parseInt(params.counter, 10) : 0) : undefined
|
||||
};
|
||||
|
||||
// 验证必要参数
|
||||
if (formData.digits < 6 || formData.digits > 8) {
|
||||
formData.digits = 6;
|
||||
console.warn('验证码位数无效,已设置为默认值6');
|
||||
}
|
||||
|
||||
if (validType === '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和时间戳
|
||||
const newToken = {
|
||||
...tokenData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdate: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 如果是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;
|
||||
}
|
||||
});
|
1
pages/index/index.json
Normal file
1
pages/index/index.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
108
pages/index/index.wxml
Normal file
108
pages/index/index.wxml
Normal file
|
@ -0,0 +1,108 @@
|
|||
<view class="container {{loading ? 'is-loading' : ''}}">
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading"></view>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="error-message" wx:if="{{error}}">{{error}}</view>
|
||||
|
||||
<!-- 令牌列表 -->
|
||||
<scroll-view
|
||||
class="scroll-view"
|
||||
scroll-y="true"
|
||||
enable-flex="true"
|
||||
catch:touchmove="{{loading ? 'catchTouchMove' : ''}}"
|
||||
>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && tokens.length === 0}}">
|
||||
<text class="empty-icon">🔐</text>
|
||||
<text class="empty-title">暂无验证器</text>
|
||||
<text class="empty-desc">点击下方按钮添加新的验证器</text>
|
||||
</view>
|
||||
|
||||
<!-- 令牌列表 -->
|
||||
<view class="token-list" wx:else>
|
||||
<view
|
||||
class="token-item {{token.type === 'totp' && remainingSeconds[token.id] <= 5 ? 'warn' : ''}}"
|
||||
wx:for="{{tokens}}"
|
||||
wx:key="id"
|
||||
wx:for-item="token"
|
||||
bindtap="handleTokenTap"
|
||||
data-token-id="{{token.id}}"
|
||||
>
|
||||
<view class="token-content">
|
||||
<!-- 令牌头部 -->
|
||||
<view class="token-header">
|
||||
<text class="token-type {{token.type}}">{{token.type === 'totp' ? 'TOTP' : 'HOTP'}}</text>
|
||||
<text class="token-issuer">{{token.issuer || '未知服务'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 令牌主体 -->
|
||||
<view class="token-body">
|
||||
<view class="code-container">
|
||||
<text class="code">{{token.code || '------'}}</text>
|
||||
<view class="code-actions">
|
||||
<!-- HOTP刷新按钮 -->
|
||||
<view
|
||||
wx:if="{{token.type === 'hotp'}}"
|
||||
class="refresh-btn {{token.refreshing ? 'refreshing' : ''}}"
|
||||
catch:tap="refreshHotpToken"
|
||||
data-token-id="{{token.id}}"
|
||||
hover-class="button-hover"
|
||||
>
|
||||
<text class="refresh-icon {{token.refreshing ? 'spin' : ''}}">🔄</text>
|
||||
</view>
|
||||
<!-- 编辑按钮 -->
|
||||
<view
|
||||
class="edit-btn"
|
||||
catch:tap="editToken"
|
||||
data-token-id="{{token.id}}"
|
||||
hover-class="button-hover"
|
||||
>
|
||||
<text class="edit-icon">✏️</text>
|
||||
</view>
|
||||
<!-- 删除按钮 -->
|
||||
<view
|
||||
class="delete-btn"
|
||||
catch:tap="deleteToken"
|
||||
data-id="{{token.id}}"
|
||||
hover-class="button-hover"
|
||||
>
|
||||
<text class="delete-icon">🗑️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 令牌信息 -->
|
||||
<view class="token-info">
|
||||
<!-- TOTP信息 -->
|
||||
<view class="totp-info {{remainingSeconds[token.id] <= 5 ? 'warn' : ''}}" wx:if="{{token.type === 'totp'}}">
|
||||
<text class="remaining-time">剩余 {{remainingSeconds[token.id]}} 秒</text>
|
||||
<view class="progress-bar {{remainingSeconds[token.id] <= 5 ? 'warn' : ''}}">
|
||||
<view class="progress" style="width: {{(remainingSeconds[token.id] / (token.period || 30)) * 100}}%"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- HOTP信息 -->
|
||||
<view class="counter-info" wx:if="{{token.type === 'hotp'}}">
|
||||
<text>计数器: {{token.counter || 0}}</text>
|
||||
<text class="hint">点击刷新按钮生成新的验证码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 广告容器 -->
|
||||
<view class="ad-container" wx:if="{{!loading && tokens.length > 0}}">
|
||||
<!-- 这里可以放广告组件 -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="add-button" bindtap="showAddTokenMenu" hover-class="button-hover">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加验证器</text>
|
||||
</view>
|
||||
</view>
|
301
pages/index/index.wxss
Normal file
301
pages/index/index.wxss
Normal file
|
@ -0,0 +1,301 @@
|
|||
/**index.wxss**/
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.container.is-loading {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error-message {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 滚动视图 */
|
||||
.scroll-view {
|
||||
width: 100%;
|
||||
height: calc(100vh - 40rpx);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 令牌列表 */
|
||||
.token-list {
|
||||
width: 100%;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.token-item.warn {
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.token-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 令牌头部 */
|
||||
.token-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.token-type {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.token-type.totp {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.token-type.hotp {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.token-issuer {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 令牌主体 */
|
||||
.token-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 48rpx;
|
||||
font-family: monospace;
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.code-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* HOTP刷新按钮 */
|
||||
.refresh-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12rpx;
|
||||
opacity: 0.6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn.refreshing {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.refresh-icon.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 编辑按钮 */
|
||||
.edit-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 删除按钮 */
|
||||
.delete-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
font-size: 32rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.delete-btn.button-hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* TOTP进度条 */
|
||||
.totp-info {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.remaining-time {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4rpx;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar .progress {
|
||||
height: 100%;
|
||||
background: #4caf50;
|
||||
transition: width 1s linear;
|
||||
}
|
||||
|
||||
.progress-bar.warn .progress {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
/* HOTP计数器信息 */
|
||||
.counter-info {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.counter-info .hint {
|
||||
margin-left: 12rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 添加按钮 */
|
||||
.add-button {
|
||||
position: fixed;
|
||||
bottom: 40rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #007AFF;
|
||||
border-radius: 40rpx;
|
||||
padding: 20rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: white;
|
||||
font-size: 40rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
color: white;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 广告容器 */
|
||||
.ad-container {
|
||||
width: 100%;
|
||||
margin-top: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 按钮点击效果 */
|
||||
.button-hover {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.98);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue