beta
This commit is contained in:
parent
a45ddf13d5
commit
bcd986e3f7
46 changed files with 6166 additions and 454 deletions
213
miniprogram-example/pages/otp-list/index.js
Normal file
213
miniprogram-example/pages/otp-list/index.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
// otp-list/index.js
|
||||
import { getOTPList, getOTPCode, deleteOTP } from '../../services/otp';
|
||||
import { checkLoginStatus } from '../../services/auth';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
otpList: [],
|
||||
loading: true,
|
||||
refreshing: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.checkLogin();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 每次页面显示时刷新OTP列表
|
||||
if (!this.data.loading) {
|
||||
this.fetchOTPList();
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.setData({ refreshing: true });
|
||||
this.fetchOTPList().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
this.setData({ refreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLogin() {
|
||||
checkLoginStatus().then(isLoggedIn => {
|
||||
if (isLoggedIn) {
|
||||
this.fetchOTPList();
|
||||
} else {
|
||||
wx.redirectTo({
|
||||
url: '/pages/login/login'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取OTP列表
|
||||
fetchOTPList() {
|
||||
this.setData({ loading: true });
|
||||
return getOTPList()
|
||||
.then(res => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
this.setData({
|
||||
otpList: res.data,
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 获取每个OTP的当前验证码
|
||||
this.refreshOTPCodes();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
wx.showToast({
|
||||
title: '获取OTP列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 刷新所有OTP的验证码
|
||||
refreshOTPCodes() {
|
||||
const { otpList } = this.data;
|
||||
|
||||
// 为每个OTP获取当前验证码
|
||||
const promises = otpList.map(otp => {
|
||||
return getOTPCode(otp.id)
|
||||
.then(res => {
|
||||
if (res.data && res.data.code) {
|
||||
return {
|
||||
id: otp.id,
|
||||
code: res.data.code,
|
||||
expiresIn: res.data.expires_in || 30
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch(() => null);
|
||||
});
|
||||
|
||||
Promise.all(promises).then(results => {
|
||||
const updatedList = [...this.data.otpList];
|
||||
|
||||
results.forEach(result => {
|
||||
if (result) {
|
||||
const index = updatedList.findIndex(otp => otp.id === result.id);
|
||||
if (index !== -1) {
|
||||
updatedList[index] = {
|
||||
...updatedList[index],
|
||||
currentCode: result.code,
|
||||
expiresIn: result.expiresIn
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setData({ otpList: updatedList });
|
||||
|
||||
// 设置定时器,每秒更新倒计时
|
||||
this.startCountdown();
|
||||
});
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
startCountdown() {
|
||||
// 清除之前的定时器
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer);
|
||||
}
|
||||
|
||||
// 创建新的定时器,每秒更新一次
|
||||
this.countdownTimer = setInterval(() => {
|
||||
const { otpList } = this.data;
|
||||
let needRefresh = false;
|
||||
|
||||
const updatedList = otpList.map(otp => {
|
||||
if (!otp.countdown) {
|
||||
otp.countdown = otp.expiresIn || 30;
|
||||
}
|
||||
|
||||
otp.countdown -= 1;
|
||||
|
||||
// 如果倒计时结束,标记需要刷新
|
||||
if (otp.countdown <= 0) {
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
return otp;
|
||||
});
|
||||
|
||||
this.setData({ otpList: updatedList });
|
||||
|
||||
// 如果有OTP需要刷新,重新获取验证码
|
||||
if (needRefresh) {
|
||||
this.refreshOTPCodes();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 添加新的OTP
|
||||
handleAddOTP() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/otp-add/index'
|
||||
});
|
||||
},
|
||||
|
||||
// 编辑OTP
|
||||
handleEditOTP(e) {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: `/pages/otp-edit/index?id=${id}`
|
||||
});
|
||||
},
|
||||
|
||||
// 删除OTP
|
||||
handleDeleteOTP(e) {
|
||||
const { id, name } = e.currentTarget.dataset;
|
||||
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除 ${name} 吗?`,
|
||||
confirmColor: '#ff4d4f',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
deleteOTP(id)
|
||||
.then(() => {
|
||||
wx.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
this.fetchOTPList();
|
||||
})
|
||||
.catch(err => {
|
||||
wx.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 复制验证码
|
||||
handleCopyCode(e) {
|
||||
const { code } = e.currentTarget.dataset;
|
||||
|
||||
wx.setClipboardData({
|
||||
data: code,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '验证码已复制',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 页面卸载时清除定时器
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer);
|
||||
}
|
||||
}
|
||||
});
|
3
miniprogram-example/pages/otp-list/index.json
Normal file
3
miniprogram-example/pages/otp-list/index.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
59
miniprogram-example/pages/otp-list/index.wxml
Normal file
59
miniprogram-example/pages/otp-list/index.wxml
Normal file
|
@ -0,0 +1,59 @@
|
|||
<!-- otp-list/index.wxml -->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">我的OTP列表</text>
|
||||
<view class="add-button" bindtap="handleAddOTP">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view class="loading-container" wx:if="{{loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- OTP列表 -->
|
||||
<view class="otp-list" wx:else>
|
||||
<block wx:if="{{otpList.length > 0}}">
|
||||
<view class="otp-item" wx:for="{{otpList}}" wx:key="id">
|
||||
<view class="otp-info">
|
||||
<view class="otp-name-row">
|
||||
<text class="otp-name">{{item.name}}</text>
|
||||
<text class="otp-issuer">{{item.issuer}}</text>
|
||||
</view>
|
||||
|
||||
<view class="otp-code-row" bindtap="handleCopyCode" data-code="{{item.currentCode}}">
|
||||
<text class="otp-code">{{item.currentCode || '******'}}</text>
|
||||
<text class="copy-hint">点击复制</text>
|
||||
</view>
|
||||
|
||||
<view class="otp-countdown">
|
||||
<progress
|
||||
percent="{{(item.countdown / item.expiresIn) * 100}}"
|
||||
stroke-width="3"
|
||||
activeColor="{{item.countdown < 10 ? '#ff4d4f' : '#1890ff'}}"
|
||||
backgroundColor="#e9e9e9"
|
||||
/>
|
||||
<text class="countdown-text">{{item.countdown || 0}}s</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="otp-actions">
|
||||
<view class="action-button edit" bindtap="handleEditOTP" data-id="{{item.id}}">
|
||||
<text class="action-icon">✎</text>
|
||||
</view>
|
||||
<view class="action-button delete" bindtap="handleDeleteOTP" data-id="{{item.id}}" data-name="{{item.name}}">
|
||||
<text class="action-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:else>
|
||||
<image class="empty-image" src="/assets/images/empty.png" mode="aspectFit"></image>
|
||||
<text class="empty-text">暂无OTP,点击右上角添加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
201
miniprogram-example/pages/otp-list/index.wxss
Normal file
201
miniprogram-example/pages/otp-list/index.wxss
Normal file
|
@ -0,0 +1,201 @@
|
|||
/* otp-list/index.wxss */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f8f8;
|
||||
padding: 0 0 40rpx 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx 32rpx;
|
||||
background-color: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
background-color: #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid #f3f3f3;
|
||||
border-top: 6rpx solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* OTP列表 */
|
||||
.otp-list {
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
.otp-item {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.otp-info {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.otp-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.otp-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.otp-issuer {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
background-color: #f5f5f5;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.otp-code-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.otp-code {
|
||||
font-size: 44rpx;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
letter-spacing: 4rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.copy-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.otp-countdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.countdown-text {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -30rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* OTP操作按钮 */
|
||||
.otp-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.action-button.edit {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.action-button.delete {
|
||||
background-color: #fff1f0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.edit .action-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.delete .action-icon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue