beta
This commit is contained in:
parent
a45ddf13d5
commit
bcd986e3f7
46 changed files with 6166 additions and 454 deletions
50
miniprogram-example/app.js
Normal file
50
miniprogram-example/app.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
// app.js
|
||||
App({
|
||||
onLaunch() {
|
||||
// 检查更新
|
||||
if (wx.canIUse('getUpdateManager')) {
|
||||
const updateManager = wx.getUpdateManager();
|
||||
updateManager.onCheckForUpdate(function (res) {
|
||||
if (res.hasUpdate) {
|
||||
updateManager.onUpdateReady(function () {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateManager.onUpdateFailed(function () {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本下载失败,请检查网络后重试',
|
||||
showCancel: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
try {
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
this.globalData.systemInfo = systemInfo;
|
||||
|
||||
// 计算安全区域
|
||||
const { screenHeight, safeArea } = systemInfo;
|
||||
this.globalData.safeAreaBottom = screenHeight - safeArea.bottom;
|
||||
} catch (e) {
|
||||
console.error('获取系统信息失败', e);
|
||||
}
|
||||
},
|
||||
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
systemInfo: {},
|
||||
safeAreaBottom: 0
|
||||
}
|
||||
});
|
23
miniprogram-example/app.json
Normal file
23
miniprogram-example/app.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"pages": [
|
||||
"pages/login/login",
|
||||
"pages/otp-list/index",
|
||||
"pages/otp-add/index"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "OTPM",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"permission": {
|
||||
"scope.camera": {
|
||||
"desc": "需要使用相机扫描二维码"
|
||||
}
|
||||
},
|
||||
"usingComponents": {},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
238
miniprogram-example/app.wxss
Normal file
238
miniprogram-example/app.wxss
Normal file
|
@ -0,0 +1,238 @@
|
|||
/**app.wxss**/
|
||||
page {
|
||||
--primary-color: #1890ff;
|
||||
--danger-color: #ff4d4f;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--text-color: #333333;
|
||||
--text-color-secondary: #666666;
|
||||
--text-color-light: #999999;
|
||||
--border-color: #e8e8e8;
|
||||
--background-color: #f8f8f8;
|
||||
--border-radius: 8rpx;
|
||||
--safe-area-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
|
||||
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei',
|
||||
sans-serif;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
/* 清除默认样式 */
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
line-height: inherit;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 通用样式类 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: var(--safe-area-bottom);
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 动画类 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 间距类 */
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: 10rpx; }
|
||||
.m-2 { margin: 20rpx; }
|
||||
.m-3 { margin: 30rpx; }
|
||||
.m-4 { margin: 40rpx; }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: 10rpx; }
|
||||
.mt-2 { margin-top: 20rpx; }
|
||||
.mt-3 { margin-top: 30rpx; }
|
||||
.mt-4 { margin-top: 40rpx; }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 10rpx; }
|
||||
.mb-2 { margin-bottom: 20rpx; }
|
||||
.mb-3 { margin-bottom: 30rpx; }
|
||||
.mb-4 { margin-bottom: 40rpx; }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-1 { margin-left: 10rpx; }
|
||||
.ml-2 { margin-left: 20rpx; }
|
||||
.ml-3 { margin-left: 30rpx; }
|
||||
.ml-4 { margin-left: 40rpx; }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-1 { margin-right: 10rpx; }
|
||||
.mr-2 { margin-right: 20rpx; }
|
||||
.mr-3 { margin-right: 30rpx; }
|
||||
.mr-4 { margin-right: 40rpx; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: 10rpx; }
|
||||
.p-2 { padding: 20rpx; }
|
||||
.p-3 { padding: 30rpx; }
|
||||
.p-4 { padding: 40rpx; }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-1 { padding-top: 10rpx; }
|
||||
.pt-2 { padding-top: 20rpx; }
|
||||
.pt-3 { padding-top: 30rpx; }
|
||||
.pt-4 { padding-top: 40rpx; }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-1 { padding-bottom: 10rpx; }
|
||||
.pb-2 { padding-bottom: 20rpx; }
|
||||
.pb-3 { padding-bottom: 30rpx; }
|
||||
.pb-4 { padding-bottom: 40rpx; }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-1 { padding-left: 10rpx; }
|
||||
.pl-2 { padding-left: 20rpx; }
|
||||
.pl-3 { padding-left: 30rpx; }
|
||||
.pl-4 { padding-left: 40rpx; }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-1 { padding-right: 10rpx; }
|
||||
.pr-2 { padding-right: 20rpx; }
|
||||
.pr-3 { padding-right: 30rpx; }
|
||||
.pr-4 { padding-right: 40rpx; }
|
48
miniprogram-example/pages/login/login.js
Normal file
48
miniprogram-example/pages/login/login.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
// login.js
|
||||
import { wxLogin } from '../../services/auth';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 页面加载时检查是否已经登录
|
||||
const token = wx.getStorageSync('token');
|
||||
if (token) {
|
||||
this.redirectToHome();
|
||||
}
|
||||
},
|
||||
|
||||
// 处理登录按钮点击
|
||||
handleLogin() {
|
||||
if (this.data.loading) return;
|
||||
|
||||
this.setData({ loading: true });
|
||||
|
||||
wxLogin()
|
||||
.then(() => {
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
});
|
||||
this.redirectToHome();
|
||||
})
|
||||
.catch(err => {
|
||||
wx.showToast({
|
||||
title: err.message || '登录失败',
|
||||
icon: 'none'
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到首页
|
||||
redirectToHome() {
|
||||
wx.reLaunch({
|
||||
url: '/pages/otp-list/index'
|
||||
});
|
||||
}
|
||||
});
|
3
miniprogram-example/pages/login/login.json
Normal file
3
miniprogram-example/pages/login/login.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
30
miniprogram-example/pages/login/login.wxml
Normal file
30
miniprogram-example/pages/login/login.wxml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!-- login.wxml -->
|
||||
<view class="container">
|
||||
<view class="logo-container">
|
||||
<image class="logo" src="/assets/images/logo.png" mode="aspectFit"></image>
|
||||
<text class="app-name">OTPM 小程序</text>
|
||||
</view>
|
||||
|
||||
<view class="login-container">
|
||||
<text class="login-title">欢迎使用 OTPM</text>
|
||||
<text class="login-subtitle">一次性密码管理工具</text>
|
||||
|
||||
<button
|
||||
class="login-button {{loading ? 'loading' : ''}}"
|
||||
type="primary"
|
||||
bindtap="handleLogin"
|
||||
disabled="{{loading}}"
|
||||
>
|
||||
<text wx:if="{{!loading}}">微信一键登录</text>
|
||||
<view wx:else class="loading-container">
|
||||
<view class="loading-icon"></view>
|
||||
<text>登录中...</text>
|
||||
</view>
|
||||
</button>
|
||||
|
||||
<view class="privacy-policy">
|
||||
<text>登录即表示您同意</text>
|
||||
<navigator url="/pages/privacy/index" class="policy-link">《隐私政策》</navigator>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
97
miniprogram-example/pages/login/login.wxss
Normal file
97
miniprogram-example/pages/login/login.wxss
Normal file
|
@ -0,0 +1,97 @@
|
|||
/* login.wxss */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100vh;
|
||||
padding: 60rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 80rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 100rpx;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 80%;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-button.loading {
|
||||
background-color: #8cc4ff;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 10rpx;
|
||||
border: 4rpx solid #ffffff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.privacy-policy {
|
||||
margin-top: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.policy-link {
|
||||
color: #1890ff;
|
||||
display: inline;
|
||||
}
|
169
miniprogram-example/pages/otp-add/index.js
Normal file
169
miniprogram-example/pages/otp-add/index.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
// otp-add/index.js
|
||||
import { createOTP } from '../../services/otp';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
form: {
|
||||
name: '',
|
||||
issuer: '',
|
||||
secret: '',
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30
|
||||
},
|
||||
algorithms: ['SHA1', 'SHA256', 'SHA512'],
|
||||
digitOptions: [6, 8],
|
||||
periodOptions: [30, 60],
|
||||
submitting: false,
|
||||
scanMode: false
|
||||
},
|
||||
|
||||
// 处理输入变化
|
||||
handleInputChange(e) {
|
||||
const { field } = e.currentTarget.dataset;
|
||||
const { value } = e.detail;
|
||||
|
||||
this.setData({
|
||||
[`form.${field}`]: value
|
||||
});
|
||||
},
|
||||
|
||||
// 处理选择器变化
|
||||
handlePickerChange(e) {
|
||||
const { field } = e.currentTarget.dataset;
|
||||
const { value } = e.detail;
|
||||
|
||||
const options = this.data[`${field}Options`] || this.data[field];
|
||||
const selectedValue = options[value];
|
||||
|
||||
this.setData({
|
||||
[`form.${field}`]: selectedValue
|
||||
});
|
||||
},
|
||||
|
||||
// 扫描二维码
|
||||
handleScanQRCode() {
|
||||
this.setData({ scanMode: true });
|
||||
|
||||
wx.scanCode({
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
try {
|
||||
// 解析otpauth://协议的URL
|
||||
const url = res.result;
|
||||
if (url.startsWith('otpauth://totp/')) {
|
||||
const parsedUrl = new URL(url);
|
||||
const path = parsedUrl.pathname.substring(1); // 移除开头的斜杠
|
||||
|
||||
// 解析路径中的issuer和name
|
||||
let issuer = '';
|
||||
let name = path;
|
||||
|
||||
if (path.includes(':')) {
|
||||
const parts = path.split(':');
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
}
|
||||
|
||||
// 从查询参数中获取其他信息
|
||||
const secret = parsedUrl.searchParams.get('secret') || '';
|
||||
const algorithm = parsedUrl.searchParams.get('algorithm') || 'SHA1';
|
||||
const digits = parseInt(parsedUrl.searchParams.get('digits') || '6');
|
||||
const period = parseInt(parsedUrl.searchParams.get('period') || '30');
|
||||
|
||||
// 如果查询参数中有issuer,优先使用
|
||||
if (parsedUrl.searchParams.get('issuer')) {
|
||||
issuer = parsedUrl.searchParams.get('issuer');
|
||||
}
|
||||
|
||||
this.setData({
|
||||
form: {
|
||||
name,
|
||||
issuer,
|
||||
secret,
|
||||
algorithm,
|
||||
digits,
|
||||
period
|
||||
}
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '二维码解析成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '不支持的二维码格式',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
wx.showToast({
|
||||
title: '二维码解析失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '扫描取消',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({ scanMode: false });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
handleSubmit() {
|
||||
const { form } = this.data;
|
||||
|
||||
// 表单验证
|
||||
if (!form.name) {
|
||||
wx.showToast({
|
||||
title: '请输入名称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.secret) {
|
||||
wx.showToast({
|
||||
title: '请输入密钥',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ submitting: true });
|
||||
|
||||
createOTP(form)
|
||||
.then(() => {
|
||||
wx.showToast({
|
||||
title: '添加成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
wx.showToast({
|
||||
title: err.message || '添加失败',
|
||||
icon: 'none'
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.setData({ submitting: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 取消
|
||||
handleCancel() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
3
miniprogram-example/pages/otp-add/index.json
Normal file
3
miniprogram-example/pages/otp-add/index.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
119
miniprogram-example/pages/otp-add/index.wxml
Normal file
119
miniprogram-example/pages/otp-add/index.wxml
Normal file
|
@ -0,0 +1,119 @@
|
|||
<!-- otp-add/index.wxml -->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">添加OTP</text>
|
||||
</view>
|
||||
|
||||
<view class="form-container">
|
||||
<view class="form-group">
|
||||
<text class="form-label">名称 <text class="required">*</text></text>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入OTP名称"
|
||||
value="{{form.name}}"
|
||||
bindinput="handleInputChange"
|
||||
data-field="name"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">发行方</text>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入发行方名称"
|
||||
value="{{form.issuer}}"
|
||||
bindinput="handleInputChange"
|
||||
data-field="issuer"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">密钥 <text class="required">*</text></text>
|
||||
<view class="secret-input-container">
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入密钥或扫描二维码"
|
||||
value="{{form.secret}}"
|
||||
bindinput="handleInputChange"
|
||||
data-field="secret"
|
||||
/>
|
||||
<view class="scan-button" bindtap="handleScanQRCode" wx:if="{{!scanMode}}">
|
||||
<text class="scan-icon">🔍</text>
|
||||
</view>
|
||||
<view class="scanning-indicator" wx:else>
|
||||
<view class="scanning-spinner"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-label">算法</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{algorithms}}"
|
||||
value="{{algorithms.indexOf(form.algorithm)}}"
|
||||
bindchange="handlePickerChange"
|
||||
data-field="algorithm"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text>{{form.algorithm}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-group half">
|
||||
<text class="form-label">位数</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{digitOptions}}"
|
||||
value="{{digitOptions.indexOf(form.digits)}}"
|
||||
bindchange="handlePickerChange"
|
||||
data-field="digits"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text>{{form.digits}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group half">
|
||||
<text class="form-label">周期(秒)</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{periodOptions}}"
|
||||
value="{{periodOptions.indexOf(form.period)}}"
|
||||
bindchange="handlePickerChange"
|
||||
data-field="period"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text>{{form.period}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="button-group">
|
||||
<button
|
||||
class="cancel-button"
|
||||
bindtap="handleCancel"
|
||||
disabled="{{submitting}}"
|
||||
>取消</button>
|
||||
|
||||
<button
|
||||
class="submit-button {{submitting ? 'loading' : ''}}"
|
||||
bindtap="handleSubmit"
|
||||
disabled="{{submitting}}"
|
||||
>
|
||||
<text wx:if="{{!submitting}}">保存</text>
|
||||
<view wx:else class="loading-container">
|
||||
<view class="loading-icon"></view>
|
||||
<text>保存中...</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
176
miniprogram-example/pages/otp-add/index.wxss
Normal file
176
miniprogram-example/pages/otp-add/index.wxss
Normal file
|
@ -0,0 +1,176 @@
|
|||
/* otp-add/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;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx;
|
||||
margin: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form-group.half {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.secret-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scan-icon {
|
||||
font-size: 40rpx;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.scanning-indicator {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.scanning-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid #f3f3f3;
|
||||
border-top: 4rpx solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.cancel-button, .submit-button {
|
||||
width: 48%;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #1890ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.submit-button.loading {
|
||||
background-color: #8cc4ff;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 10rpx;
|
||||
border: 4rpx solid #ffffff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
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;
|
||||
}
|
47
miniprogram-example/project.config.json
Normal file
47
miniprogram-example/project.config.json
Normal file
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"description": "项目配置文件",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"miniprogramRoot": "",
|
||||
"compileType": "miniprogram",
|
||||
"projectname": "OTPM",
|
||||
"setting": {
|
||||
"useCompilerPlugins": [
|
||||
"sass"
|
||||
],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"minified": true,
|
||||
"postcss": true,
|
||||
"minifyWXSS": true,
|
||||
"minifyWXML": true,
|
||||
"uglifyFileName": true,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"ignoreUploadUnusedFiles": true,
|
||||
"compileWorklet": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"localPlugins": false,
|
||||
"disableUseStrict": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {},
|
||||
"srcMiniprogramRoot": "",
|
||||
"appid": "wxb6599459668b6b55",
|
||||
"libVersion": "2.30.2",
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
23
miniprogram-example/project.private.config.json
Normal file
23
miniprogram-example/project.private.config.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"libVersion": "3.8.5",
|
||||
"projectname": "OTPM",
|
||||
"condition": {},
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"showShadowRootInWxmlPanel": false,
|
||||
"useStaticServer": false,
|
||||
"useLanDebug": false,
|
||||
"showES6CompileOption": false,
|
||||
"compileHotReLoad": true,
|
||||
"checkInvalidKey": true,
|
||||
"ignoreDevUnusedFiles": true,
|
||||
"bigPackageSizeSupport": false
|
||||
}
|
||||
}
|
84
miniprogram-example/services/auth.js
Normal file
84
miniprogram-example/services/auth.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
// auth.js - 认证相关服务
|
||||
|
||||
import request from '../utils/request';
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* 1. 调用wx.login获取code
|
||||
* 2. 发送code到服务端换取token和openid
|
||||
* 3. 保存token和openid到本地存储
|
||||
*/
|
||||
export const wxLogin = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
// 发送code到服务端
|
||||
request({
|
||||
url: '/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: res.code
|
||||
}
|
||||
}).then(response => {
|
||||
// 保存token和openid
|
||||
if (response.data && response.data.token && response.data.openid) {
|
||||
wx.setStorageSync('token', response.data.token);
|
||||
wx.setStorageSync('openid', response.data.openid);
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(new Error('登录失败,服务器返回数据格式错误'));
|
||||
}
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject(new Error('登录失败,获取code失败: ' + res.errMsg));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('微信登录失败: ' + err.errMsg));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
* 1. 检查本地是否有token和openid
|
||||
* 2. 如果有,验证token是否有效
|
||||
* 3. 如果无效,清除本地存储并返回false
|
||||
*/
|
||||
export const checkLoginStatus = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token');
|
||||
const openid = wx.getStorageSync('openid');
|
||||
|
||||
if (!token || !openid) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证token有效性
|
||||
request({
|
||||
url: '/verify-token',
|
||||
method: 'POST'
|
||||
}).then(() => {
|
||||
resolve(true);
|
||||
}).catch(() => {
|
||||
// token无效,清除本地存储
|
||||
wx.removeStorageSync('token');
|
||||
wx.removeStorageSync('openid');
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export const logout = () => {
|
||||
wx.removeStorageSync('token');
|
||||
wx.removeStorageSync('openid');
|
||||
return Promise.resolve();
|
||||
};
|
87
miniprogram-example/services/otp.js
Normal file
87
miniprogram-example/services/otp.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
// otp.js - OTP相关服务
|
||||
|
||||
import request from '../utils/request';
|
||||
|
||||
/**
|
||||
* 创建新的OTP
|
||||
* @param {Object} params - 创建OTP的参数
|
||||
* @param {string} params.name - OTP名称
|
||||
* @param {string} params.issuer - 发行方
|
||||
* @param {string} params.secret - 密钥
|
||||
* @param {string} params.algorithm - 算法,默认为SHA1
|
||||
* @param {number} params.digits - 位数,默认为6
|
||||
* @param {number} params.period - 周期,默认为30秒
|
||||
* @returns {Promise} - 返回创建结果
|
||||
*/
|
||||
export const createOTP = (params) => {
|
||||
return request({
|
||||
url: '/otp',
|
||||
method: 'POST',
|
||||
data: params
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户所有OTP列表
|
||||
* @returns {Promise} - 返回OTP列表
|
||||
*/
|
||||
export const getOTPList = () => {
|
||||
return request({
|
||||
url: '/otp',
|
||||
method: 'GET'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定OTP的当前验证码
|
||||
* @param {string} id - OTP的ID
|
||||
* @returns {Promise} - 返回当前验证码
|
||||
*/
|
||||
export const getOTPCode = (id) => {
|
||||
return request({
|
||||
url: `/otp/${id}/code`,
|
||||
method: 'GET'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证OTP
|
||||
* @param {string} id - OTP的ID
|
||||
* @param {string} code - 用户输入的验证码
|
||||
* @returns {Promise} - 返回验证结果
|
||||
*/
|
||||
export const verifyOTP = (id, code) => {
|
||||
return request({
|
||||
url: `/otp/${id}/verify`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: code
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新OTP信息
|
||||
* @param {string} id - OTP的ID
|
||||
* @param {Object} params - 更新的参数
|
||||
* @returns {Promise} - 返回更新结果
|
||||
*/
|
||||
export const updateOTP = (id, params) => {
|
||||
return request({
|
||||
url: `/otp/${id}`,
|
||||
method: 'PUT',
|
||||
data: params
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除OTP
|
||||
* @param {string} id - OTP的ID
|
||||
* @returns {Promise} - 返回删除结果
|
||||
*/
|
||||
export const deleteOTP = (id) => {
|
||||
return request({
|
||||
url: `/otp/${id}`,
|
||||
method: 'DELETE'
|
||||
});
|
||||
};
|
64
miniprogram-example/utils/request.js
Normal file
64
miniprogram-example/utils/request.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
// request.js - 网络请求工具类
|
||||
|
||||
const BASE_URL = 'https://otpm.zeroc.net'; // 替换为实际的API域名
|
||||
|
||||
// 请求拦截器
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token');
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header
|
||||
};
|
||||
|
||||
// 如果有token,添加到请求头
|
||||
if (token) {
|
||||
header['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
wx.request({
|
||||
url: `${BASE_URL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: header,
|
||||
success: (res) => {
|
||||
// 处理业务错误
|
||||
if (res.data.code !== 0) {
|
||||
// token过期,尝试刷新
|
||||
if (res.statusCode === 401) {
|
||||
refreshToken().then(() => {
|
||||
// 刷新token后重试请求
|
||||
request(options).then(resolve).catch(reject);
|
||||
}).catch((err) => {
|
||||
// 刷新失败,需要重新登录
|
||||
wx.removeStorageSync('token');
|
||||
wx.removeStorageSync('openid');
|
||||
reject(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
reject(new Error(res.data.message || '请求失败'));
|
||||
return;
|
||||
}
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 刷新token
|
||||
const refreshToken = () => {
|
||||
return request({
|
||||
url: '/refresh-token',
|
||||
method: 'POST'
|
||||
}).then(res => {
|
||||
if (res.data && res.data.token) {
|
||||
wx.setStorageSync('token', res.data.token);
|
||||
return res.data.token;
|
||||
}
|
||||
throw new Error('Failed to refresh token');
|
||||
});
|
||||
};
|
||||
|
||||
export default request;
|
Loading…
Add table
Add a link
Reference in a new issue