feat: 提交模板代码
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import request from '../utils/request'
|
||||
import { StandardResponse } from './types'
|
||||
|
||||
/**
|
||||
* 登录接口请求参数类型
|
||||
*/
|
||||
export interface LoginParams {
|
||||
userName: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息数据类型
|
||||
*/
|
||||
export interface UserInfoData {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
email: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param params 登录参数
|
||||
* @returns 返回标准响应格式的登录数据
|
||||
*/
|
||||
export const loginReq = (params: LoginParams) => {
|
||||
return request.post<StandardResponse<UserInfoData>>('/wechatLogin/wechatLogin.action', params)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 标准响应数据格式
|
||||
*/
|
||||
export interface StandardResponse<T = unknown> {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"pages": ["pages/date/date", "pages/login/login", "pages/index/index"],
|
||||
"window": {
|
||||
"navigationStyle": "default",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTitleText": "My Weapp Template"
|
||||
},
|
||||
"style": "v2",
|
||||
"renderer": "webview",
|
||||
"componentFramework": "glass-easel",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**app.wxss**/
|
||||
page,
|
||||
view,
|
||||
scroll-view,
|
||||
image,
|
||||
text {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// app.ts
|
||||
App<IAppOption>({
|
||||
globalData: {},
|
||||
onLaunch() {},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* pages/date/date.wxss */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.example-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.example-item {
|
||||
padding: 20rpx;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12rpx;
|
||||
border-left: 4rpx solid #1890ff;
|
||||
}
|
||||
|
||||
.example-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.example-format {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 12rpx;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.example-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #1890ff;
|
||||
word-break: break-all;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
background-color: #40a9ff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// pages/date/date.ts
|
||||
import { formatTime, formatDateTime } from '../../utils/util'
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
// formatTime 示例
|
||||
formatTimeExamples: [] as Array<{ label: string; value: string }>,
|
||||
// formatDateTime 示例 - 不同格式模板
|
||||
formatDateTimeExamples: [] as Array<{ label: string; format: string; value: string }>,
|
||||
// formatDateTime 示例 - 不同输入类型
|
||||
inputTypeExamples: [] as Array<{ label: string; input: string; value: string }>,
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad() {
|
||||
this.initExamples()
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化所有示例数据
|
||||
*/
|
||||
initExamples() {
|
||||
const now = new Date()
|
||||
const timestamp = now.getTime()
|
||||
const dateString = '2024-01-15 14:30:45'
|
||||
const chineseDateString = '2024年1月15日'
|
||||
|
||||
// formatTime 示例
|
||||
const formatTimeExamples = [
|
||||
{
|
||||
label: '当前时间',
|
||||
value: formatTime(now),
|
||||
},
|
||||
{
|
||||
label: '指定日期',
|
||||
value: formatTime(new Date(2024, 0, 15, 14, 30, 45)),
|
||||
},
|
||||
{
|
||||
label: '昨天',
|
||||
value: formatTime(new Date(timestamp - 24 * 60 * 60 * 1000)),
|
||||
},
|
||||
{
|
||||
label: '明天',
|
||||
value: formatTime(new Date(timestamp + 24 * 60 * 60 * 1000)),
|
||||
},
|
||||
]
|
||||
|
||||
// formatDateTime 示例 - 不同格式模板
|
||||
const formatDateTimeExamples = [
|
||||
{
|
||||
label: '默认格式(YYYY/MM/DD HH:mm:ss)',
|
||||
format: 'YYYY/MM/DD HH:mm:ss',
|
||||
value: formatDateTime(now),
|
||||
},
|
||||
{
|
||||
label: '中文日期格式',
|
||||
format: 'YYYY年MM月DD日 HH:mm:ss',
|
||||
value: formatDateTime(now, 'YYYY年MM月DD日 HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
label: '短日期格式',
|
||||
format: 'YY/MM/DD',
|
||||
value: formatDateTime(now, 'YY/MM/DD'),
|
||||
},
|
||||
{
|
||||
label: '12小时制格式',
|
||||
format: 'YYYY-MM-DD hh:mm:ss A',
|
||||
value: formatDateTime(now, 'YYYY-MM-DD hh:mm:ss A'),
|
||||
},
|
||||
{
|
||||
label: '24小时制格式',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
value: formatDateTime(now, 'YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
label: '仅日期',
|
||||
format: 'YYYY-MM-DD',
|
||||
value: formatDateTime(now, 'YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
label: '仅时间',
|
||||
format: 'HH:mm:ss',
|
||||
value: formatDateTime(now, 'HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
label: '带毫秒',
|
||||
format: 'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
value: formatDateTime(now, 'YYYY-MM-DD HH:mm:ss.SSS'),
|
||||
},
|
||||
{
|
||||
label: '美式日期格式',
|
||||
format: 'MM/DD/YYYY',
|
||||
value: formatDateTime(now, 'MM/DD/YYYY'),
|
||||
},
|
||||
{
|
||||
label: '完整中文格式',
|
||||
format: 'YYYY年MM月DD日 HH时mm分ss秒',
|
||||
value: formatDateTime(now, 'YYYY年MM月DD日 HH时mm分ss秒'),
|
||||
},
|
||||
]
|
||||
|
||||
// formatDateTime 示例 - 不同输入类型
|
||||
const inputTypeExamples = [
|
||||
{
|
||||
label: 'Date 对象',
|
||||
input: 'new Date()',
|
||||
value: formatDateTime(now),
|
||||
},
|
||||
{
|
||||
label: '时间戳(数字)',
|
||||
input: `${timestamp}`,
|
||||
value: formatDateTime(timestamp),
|
||||
},
|
||||
{
|
||||
label: '日期字符串(标准格式)',
|
||||
input: dateString,
|
||||
value: formatDateTime(dateString),
|
||||
},
|
||||
{
|
||||
label: '中文日期字符串',
|
||||
input: chineseDateString,
|
||||
value: formatDateTime(chineseDateString),
|
||||
},
|
||||
{
|
||||
label: '仅年份字符串',
|
||||
input: '2024',
|
||||
value: formatDateTime('2024'),
|
||||
},
|
||||
{
|
||||
label: '年月字符串',
|
||||
input: '2024/01',
|
||||
value: formatDateTime('2024/01'),
|
||||
},
|
||||
]
|
||||
|
||||
this.setData({
|
||||
formatTimeExamples,
|
||||
formatDateTimeExamples,
|
||||
inputTypeExamples,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {},
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
*/
|
||||
copyText(e: WechatMiniprogram.Touch) {
|
||||
const { text } = e.currentTarget.dataset
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
wx.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--pages/date/date.wxml-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">日期时间格式化示例</text>
|
||||
</view>
|
||||
|
||||
<!-- formatTime 函数示例 -->
|
||||
<view class="section">
|
||||
<view class="section-title">formatTime 函数示例</view>
|
||||
<view class="section-desc">固定格式:YYYY/MM/DD HH:mm:ss</view>
|
||||
<view class="example-list">
|
||||
<view class="example-item" wx:for="{{formatTimeExamples}}" wx:key="label">
|
||||
<view class="example-label">{{item.label}}:</view>
|
||||
<view class="example-value">
|
||||
<text class="result-text" selectable="{{true}}">{{item.value}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{item.value}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{item.value}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- formatDateTime 函数示例 - 不同格式模板 -->
|
||||
<view class="section">
|
||||
<view class="section-title">formatDateTime 函数示例 - 不同格式模板</view>
|
||||
<view class="section-desc">支持自定义格式模板,灵活配置日期时间显示格式</view>
|
||||
<view class="example-list">
|
||||
<view class="example-item" wx:for="{{formatDateTimeExamples}}" wx:key="label">
|
||||
<view class="example-label">{{item.label}}</view>
|
||||
<view class="example-format">格式模板:{{item.format}}</view>
|
||||
<view class="example-value">
|
||||
<text class="result-text" selectable="{{true}}">{{item.value}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{item.value}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{item.value}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- formatDateTime 函数示例 - 不同输入类型 -->
|
||||
<view class="section">
|
||||
<view class="section-title">formatDateTime 函数示例 - 不同输入类型</view>
|
||||
<view class="section-desc">支持 Date 对象、时间戳、日期字符串等多种输入类型</view>
|
||||
<view class="example-list">
|
||||
<view class="example-item" wx:for="{{inputTypeExamples}}" wx:key="label">
|
||||
<view class="example-label">{{item.label}}</view>
|
||||
<view class="example-format">输入:{{item.input}}</view>
|
||||
<view class="example-value">
|
||||
<text class="result-text" selectable="{{true}}">{{item.value}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{item.value}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{item.value}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/* pages/index.wxss */
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
}
|
||||
|
||||
.input-textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #fafafa;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background-color: #06ad56;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #576b95;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background-color: #4a5d84;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #fafafa;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #07c160;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
background-color: #06ad56;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #fff3cd;
|
||||
border: 2rpx solid #ffc107;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 26rpx;
|
||||
color: #856404;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// pages/index.ts
|
||||
import { Encrypt, Decrypt, BASE64Encrypt, BASE64Decrypt, AESError } from '../../utils/aes'
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
// 输入原文
|
||||
plainText: 'Hello, 小程序 AES 加密示例!',
|
||||
// 标准 Base64 加密结果
|
||||
encryptedText: '',
|
||||
// 标准 Base64 解密结果
|
||||
decryptedText: '',
|
||||
// URL Safe Base64 加密结果
|
||||
base64EncryptedText: '',
|
||||
// URL Safe Base64 解密结果
|
||||
base64DecryptedText: '',
|
||||
// 错误信息
|
||||
errorMessage: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad() {
|
||||
// 页面加载时自动执行一次加密解密示例
|
||||
this.handleEncrypt()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {},
|
||||
|
||||
/**
|
||||
* 输入原文变化
|
||||
*/
|
||||
onPlainTextInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({
|
||||
plainText: e.detail.value,
|
||||
errorMessage: '',
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行标准 Base64 加密
|
||||
*/
|
||||
handleEncrypt() {
|
||||
try {
|
||||
const { plainText } = this.data
|
||||
if (!plainText.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入要加密的内容',
|
||||
icon: 'none',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行加密
|
||||
const encrypted = Encrypt(plainText)
|
||||
this.setData({
|
||||
encryptedText: encrypted,
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '加密成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof AESError ? error.message : '加密失败'
|
||||
this.setData({
|
||||
errorMessage: errorMsg,
|
||||
})
|
||||
wx.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行标准 Base64 解密
|
||||
*/
|
||||
handleDecrypt() {
|
||||
try {
|
||||
const { encryptedText } = this.data
|
||||
if (!encryptedText.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入要解密的内容',
|
||||
icon: 'none',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行解密
|
||||
const decrypted = Decrypt(encryptedText)
|
||||
if (decrypted === null) {
|
||||
this.setData({
|
||||
errorMessage: '解密失败:密钥不匹配或密文被篡改',
|
||||
})
|
||||
wx.showToast({
|
||||
title: '解密失败',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
decryptedText: decrypted,
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '解密成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof AESError ? error.message : '解密失败'
|
||||
this.setData({
|
||||
errorMessage: errorMsg,
|
||||
})
|
||||
wx.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行 URL Safe Base64 加密
|
||||
*/
|
||||
handleBase64Encrypt() {
|
||||
try {
|
||||
const { plainText } = this.data
|
||||
if (!plainText.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入要加密的内容',
|
||||
icon: 'none',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行 URL Safe Base64 加密
|
||||
const encrypted = BASE64Encrypt(plainText)
|
||||
this.setData({
|
||||
base64EncryptedText: encrypted,
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '加密成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof AESError ? error.message : '加密失败'
|
||||
this.setData({
|
||||
errorMessage: errorMsg,
|
||||
})
|
||||
wx.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行 URL Safe Base64 解密
|
||||
*/
|
||||
handleBase64Decrypt() {
|
||||
try {
|
||||
const { base64EncryptedText } = this.data
|
||||
if (!base64EncryptedText.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入要解密的内容',
|
||||
icon: 'none',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行 URL Safe Base64 解密
|
||||
const decrypted = BASE64Decrypt(base64EncryptedText)
|
||||
if (decrypted === null) {
|
||||
this.setData({
|
||||
errorMessage: '解密失败:密钥不匹配或密文被篡改',
|
||||
})
|
||||
wx.showToast({
|
||||
title: '解密失败',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
base64DecryptedText: decrypted,
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '解密成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof AESError ? error.message : '解密失败'
|
||||
this.setData({
|
||||
errorMessage: errorMsg,
|
||||
})
|
||||
wx.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
*/
|
||||
copyText(e: WechatMiniprogram.Touch) {
|
||||
const { text } = e.currentTarget.dataset
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
wx.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
}).catch(() => {
|
||||
console.error('showToast error')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
<!--pages/index.wxml-->
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">AES 加密解密示例</text>
|
||||
</view>
|
||||
|
||||
<!-- 原文输入区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">原文输入</view>
|
||||
<textarea
|
||||
class="input-textarea"
|
||||
value="{{plainText}}"
|
||||
placeholder="请输入要加密的内容"
|
||||
bindinput="onPlainTextInput"
|
||||
maxlength="500"
|
||||
show-confirm-bar="{{false}}"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 标准 Base64 加密解密 -->
|
||||
<view class="section">
|
||||
<view class="section-title">标准 Base64 加密/解密</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="btn btn-primary" bindtap="handleEncrypt">加密</button>
|
||||
<button class="btn btn-secondary" bindtap="handleDecrypt">解密</button>
|
||||
</view>
|
||||
|
||||
<view class="result-box">
|
||||
<view class="result-label">加密结果:</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text" selectable="{{true}}">{{encryptedText || '暂无'}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{encryptedText}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{encryptedText}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-box">
|
||||
<view class="result-label">解密结果:</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text" selectable="{{true}}">{{decryptedText || '暂无'}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{decryptedText}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{decryptedText}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- URL Safe Base64 加密解密 -->
|
||||
<view class="section">
|
||||
<view class="section-title">URL Safe Base64 加密/解密</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="btn btn-primary" bindtap="handleBase64Encrypt">加密</button>
|
||||
<button class="btn btn-secondary" bindtap="handleBase64Decrypt">解密</button>
|
||||
</view>
|
||||
|
||||
<view class="result-box">
|
||||
<view class="result-label">加密结果:</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text" selectable="{{true}}">{{base64EncryptedText || '暂无'}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{base64EncryptedText}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{base64EncryptedText}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-box">
|
||||
<view class="result-label">解密结果:</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text" selectable="{{true}}">{{base64DecryptedText || '暂无'}}</text>
|
||||
<text
|
||||
class="copy-btn"
|
||||
data-text="{{base64DecryptedText}}"
|
||||
bindtap="copyText"
|
||||
wx:if="{{base64DecryptedText}}"
|
||||
>
|
||||
复制
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误信息显示 -->
|
||||
<view class="error-box" wx:if="{{errorMessage}}">
|
||||
<text class="error-text">{{errorMessage}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "Request 使用示例",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {
|
||||
"t-input": "/miniprogram_npm/tdesign-miniprogram/input/input",
|
||||
"t-button": "/miniprogram_npm/tdesign-miniprogram/button/button"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* pages/login/login.wxss */
|
||||
|
||||
// 容器样式
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: white;
|
||||
padding: 40rpx 30rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 头部标题区域
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 功能区块样式
|
||||
.form-section {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
.input-item {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
// 按钮组样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
// 信息展示框
|
||||
.info-box {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border-left: 6rpx solid #0052d9;
|
||||
|
||||
&.success {
|
||||
background: #e8f5e9;
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 头像区域
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.avatar-preview {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 100rpx;
|
||||
border: 4rpx solid #0052d9;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 100rpx;
|
||||
border: 4rpx dashed #cccccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文档说明区域
|
||||
.doc-section {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
|
||||
.doc-item {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
line-height: 1.8;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Login 页面 - request.ts 使用示例
|
||||
* 演示网络请求封装的各种用法:GET、POST、PUT、DELETE、文件上传等
|
||||
*/
|
||||
|
||||
import { loginReq, type LoginParams } from '../../api/login'
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
// 登录表单数据
|
||||
username: '王波林0001',
|
||||
password: '1/OBSNrCge2rzKLaQduNpg==',
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad() {},
|
||||
|
||||
// ==================== 输入框事件处理 ====================
|
||||
|
||||
/**
|
||||
* 用户名输入变化
|
||||
*/
|
||||
onUsernameChange(e: WechatMiniprogram.Input) {
|
||||
this.setData({
|
||||
username: e.detail.value,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 密码输入变化
|
||||
*/
|
||||
onPasswordChange(e: WechatMiniprogram.Input) {
|
||||
this.setData({
|
||||
password: e.detail.value,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户ID输入变化
|
||||
*/
|
||||
onUserIdChange(e: WechatMiniprogram.Input) {
|
||||
this.setData({
|
||||
userId: e.detail.value,
|
||||
})
|
||||
},
|
||||
|
||||
// ==================== POST 请求示例 ====================
|
||||
|
||||
/**
|
||||
* 登录处理(显示 loading)
|
||||
* 演示:POST 请求 + 默认 loading + TypeScript 类型
|
||||
*/
|
||||
async handleLogin() {
|
||||
const { username, password } = this.data
|
||||
|
||||
// 表单验证
|
||||
if (!username || !password) {
|
||||
wx.showToast({
|
||||
title: '请输入用户名和密码',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// POST 请求示例 - 默认显示 loading
|
||||
const loginParams: LoginParams = { userName: username, password }
|
||||
const res = await loginReq(loginParams)
|
||||
console.warn(res)
|
||||
|
||||
// 模拟保存 token
|
||||
// const app = getApp<IAppOption>()
|
||||
// if (res.data?.token) {
|
||||
// app.globalData.token = res.data.token
|
||||
// }
|
||||
|
||||
// wx.showToast({
|
||||
// title: '登录成功',
|
||||
// icon: 'success',
|
||||
// })
|
||||
|
||||
// 实际项目中可以跳转到首页
|
||||
// setTimeout(() => {
|
||||
// wx.switchTab({
|
||||
// url: '/pages/index/index'
|
||||
// })
|
||||
// }, 1500)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
// 错误已在拦截器中统一处理,这里可以做额外处理
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {},
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
<!--pages/login/login.wxml-->
|
||||
<view class="login-container">
|
||||
<!-- 标题区域 -->
|
||||
<view class="header">
|
||||
<text class="title">Request.ts 使用示例</text>
|
||||
<text class="subtitle">演示网络请求封装的各种用法</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单区域 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">1. POST 请求示例 - 登录</view>
|
||||
|
||||
<t-input
|
||||
placeholder="请输入用户名"
|
||||
value="{{username}}"
|
||||
bind:change="onUsernameChange"
|
||||
class="input-item"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<t-input
|
||||
placeholder="请输入密码"
|
||||
value="{{password}}"
|
||||
type="password"
|
||||
bind:change="onPasswordChange"
|
||||
class="input-item"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<view class="button-group">
|
||||
<t-button theme="primary" size="large" bind:tap="handleLogin" block>
|
||||
登录
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [
|
||||
{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 小程序中使用命名空间导入 crypto-js(CommonJS 模块)
|
||||
// 注意:crypto-js 是 CommonJS 模块,在小程序中需要使用 import * as 方式导入
|
||||
import * as CryptoJS from 'crypto-js'
|
||||
|
||||
/**
|
||||
* AES 加密/解密错误类
|
||||
*/
|
||||
export class AESError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code?: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'AESError'
|
||||
}
|
||||
}
|
||||
|
||||
// KEY 和 IV
|
||||
const DEFAULT_KEY: string = 'J9w8x7z6y7A7B3C2D1E0F9G8H7I6J5K4'
|
||||
const DEFAULT_IV: string = 'Q1w2Q3T4T5y6U7i8'
|
||||
|
||||
/**
|
||||
* 统一解析 Key 和 IV
|
||||
* 强制校验长度,确保触发 AES-256
|
||||
* @throws {AESError} 当 KEY 或 IV 长度不符合要求时抛出错误
|
||||
*/
|
||||
const parseKeyIv = (keyStr: string, ivStr: string) => {
|
||||
try {
|
||||
// 验证 CryptoJS 是否正确加载
|
||||
if (!CryptoJS || !CryptoJS.enc || !CryptoJS.enc.Utf8) {
|
||||
throw new AESError('CryptoJS 模块未正确加载,请检查 crypto-js 依赖', 'MODULE_NOT_LOADED')
|
||||
}
|
||||
|
||||
const key = CryptoJS.enc.Utf8.parse(keyStr)
|
||||
const iv = CryptoJS.enc.Utf8.parse(ivStr)
|
||||
|
||||
// IV 必须是 16 字节 (128 bit),否则 AES 算法本身会出错或行为异常
|
||||
if (iv.sigBytes !== 16) {
|
||||
throw new AESError(
|
||||
`AES IV 错误: 期望 16 字节, 但实际为 ${iv.sigBytes} 字节.`,
|
||||
'INVALID_IV_LENGTH',
|
||||
)
|
||||
}
|
||||
|
||||
// Key 必须是 32 字节 (256 bit) 才能触发 AES-256
|
||||
// 如果是 16 字节会降级为 AES-128,如果是 24 字节是 AES-192
|
||||
// 为了强制 AES-256,这里必须报错
|
||||
if (key.sigBytes !== 32) {
|
||||
throw new AESError(
|
||||
`AES Key 错误: 期望 32 字节 (用于 AES-256), 但实际为 ${key.sigBytes} 字节.`,
|
||||
'INVALID_KEY_LENGTH',
|
||||
)
|
||||
}
|
||||
|
||||
return { key, iv }
|
||||
} catch (error) {
|
||||
// 如果是 AESError,直接抛出
|
||||
if (error instanceof AESError) {
|
||||
throw error
|
||||
}
|
||||
// 其他错误包装为 AESError
|
||||
throw new AESError(
|
||||
`解析 Key 或 IV 时发生错误: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'PARSE_ERROR',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准 AES 加密 (输出标准 Base64)
|
||||
* @param word 要加密的字符串
|
||||
* @param keyStr 密钥,可选,默认使用 DEFAULT_KEY
|
||||
* @param ivStr 初始化向量,可选,默认使用 DEFAULT_IV
|
||||
* @returns 加密后的字符串
|
||||
* @throws {AESError} 当加密失败时抛出错误
|
||||
*/
|
||||
export const Encrypt = (
|
||||
word: string,
|
||||
keyStr: string = DEFAULT_KEY,
|
||||
ivStr: string = DEFAULT_IV,
|
||||
): string => {
|
||||
try {
|
||||
// 验证输入参数
|
||||
if (typeof word !== 'string') {
|
||||
throw new AESError('要加密的内容必须是字符串类型', 'INVALID_INPUT')
|
||||
}
|
||||
|
||||
const { key, iv } = parseKeyIv(keyStr, ivStr)
|
||||
|
||||
const src = CryptoJS.enc.Utf8.parse(word)
|
||||
const encrypted = CryptoJS.AES.encrypt(src, key, {
|
||||
iv: iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
|
||||
const result = encrypted.toString()
|
||||
if (!result) {
|
||||
throw new AESError('加密结果为空,加密可能失败', 'ENCRYPT_FAILED')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
// 如果是 AESError,直接抛出
|
||||
if (error instanceof AESError) {
|
||||
throw error
|
||||
}
|
||||
// 其他错误包装为 AESError
|
||||
throw new AESError(
|
||||
`加密失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'ENCRYPT_ERROR',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准 AES 解密 (输入标准 Base64)
|
||||
* @param word 要解密的字符串
|
||||
* @param keyStr 密钥,可选,默认使用 DEFAULT_KEY
|
||||
* @param ivStr 初始化向量,可选,默认使用 DEFAULT_IV
|
||||
* @returns 解密后的字符串,解密失败
|
||||
* @throws {AESError} 当解密过程中发生真正的错误时抛出
|
||||
*/
|
||||
export const Decrypt = (
|
||||
word: string,
|
||||
keyStr: string = DEFAULT_KEY,
|
||||
ivStr: string = DEFAULT_IV,
|
||||
): string => {
|
||||
try {
|
||||
// 验证输入参数
|
||||
if (typeof word !== 'string' || !word.trim()) {
|
||||
throw new AESError('要解密的内容必须是非空字符串', 'INVALID_INPUT')
|
||||
}
|
||||
|
||||
const { key, iv } = parseKeyIv(keyStr, ivStr)
|
||||
|
||||
const decrypt = CryptoJS.AES.decrypt(word, key, {
|
||||
iv: iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
|
||||
const result = decrypt.toString(CryptoJS.enc.Utf8)
|
||||
|
||||
// 如果解密结果为空,可能是密钥不匹配或密文被篡改(这是业务逻辑,不是错误)
|
||||
if (!result) {
|
||||
throw new AESError('解密结果为空,可能是密钥不匹配或密文被篡改', 'DECRYPT_FAILED')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
// 如果是 AESError,直接抛出(这些是真正的错误,不应该静默处理)
|
||||
if (error instanceof AESError) {
|
||||
throw error
|
||||
}
|
||||
// 其他错误(如参数错误、模块未加载等)包装为 AESError
|
||||
throw new AESError(
|
||||
`解密失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'DECRYPT_ERROR',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL Safe Base64 加密
|
||||
* @param word 要加密的字符串
|
||||
* @param keyStr 密钥,可选,默认使用 DEFAULT_KEY
|
||||
* @param ivStr 初始化向量,可选,默认使用 DEFAULT_IV
|
||||
* @returns URL Safe Base64 编码的加密字符串
|
||||
* @throws {AESError} 当加密失败时抛出错误
|
||||
*/
|
||||
export const BASE64Encrypt = (
|
||||
word: string,
|
||||
keyStr: string = DEFAULT_KEY,
|
||||
ivStr: string = DEFAULT_IV,
|
||||
): string => {
|
||||
try {
|
||||
const encrypted = Encrypt(word, keyStr, ivStr)
|
||||
return encrypted.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
} catch (error) {
|
||||
// 如果是 AESError,直接抛出
|
||||
if (error instanceof AESError) {
|
||||
throw error
|
||||
}
|
||||
// 其他错误包装为 AESError
|
||||
throw new AESError(
|
||||
`URL Safe Base64 加密失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'BASE64_ENCRYPT_ERROR',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL Safe Base64 解密
|
||||
* @param word 要解密的字符串
|
||||
* @param keyStr 密钥,可选,默认使用 DEFAULT_KEY
|
||||
* @param ivStr 初始化向量,可选,默认使用 DEFAULT_IV
|
||||
* @returns URL Safe Base64 解密后的字符串
|
||||
* @throws {AESError} 当解密过程中发生真正的错误时抛出
|
||||
*/
|
||||
export const BASE64Decrypt = (
|
||||
word: string,
|
||||
keyStr: string = DEFAULT_KEY,
|
||||
ivStr: string = DEFAULT_IV,
|
||||
): string => {
|
||||
try {
|
||||
// 验证输入参数
|
||||
if (typeof word !== 'string' || !word.trim()) {
|
||||
throw new AESError('要解密的内容必须是非空字符串', 'INVALID_INPUT')
|
||||
}
|
||||
|
||||
// 还原 Base64 格式
|
||||
let base64Str = word.replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
||||
// 补全末尾的 = (Base64长度必须是4的倍数)
|
||||
const mod4 = base64Str.length % 4
|
||||
if (mod4 > 0) {
|
||||
base64Str += '='.repeat(4 - mod4)
|
||||
}
|
||||
|
||||
return Decrypt(base64Str, keyStr, ivStr)
|
||||
} catch (error) {
|
||||
// 如果是 AESError,直接抛出
|
||||
if (error instanceof AESError) {
|
||||
throw error
|
||||
}
|
||||
// 其他错误包装为 AESError
|
||||
throw new AESError(
|
||||
`URL Safe Base64 解密失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'BASE64_DECRYPT_ERROR',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 网络请求封装 - 基于 axios-miniprogram
|
||||
*
|
||||
* 功能特性:
|
||||
* - 自动添加 Token
|
||||
* - 统一错误处理
|
||||
* - 智能加载动画管理
|
||||
* - 完整的 TypeScript 类型支持
|
||||
* - 文件上传功能
|
||||
*/
|
||||
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios-miniprogram'
|
||||
import type { StandardResponse } from '../api/types'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 自定义请求配置接口
|
||||
* 扩展 axios 的请求配置,添加自定义参数
|
||||
*/
|
||||
interface CustomRequestConfig extends AxiosRequestConfig {
|
||||
loading?: boolean // 是否显示加载提示,默认 true
|
||||
upload?: boolean // 是否为上传请求
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传配置接口
|
||||
*/
|
||||
interface UploadOptions {
|
||||
apiUrl: string // 接口路径
|
||||
filePath: string // 文件本地路径
|
||||
name?: string // 文件对应的 key,默认 'file'
|
||||
formData?: Record<string, unknown> // 额外参数
|
||||
loading?: boolean // 是否显示加载提示,默认 true
|
||||
onUploadProgress?: (event: unknown) => void // 上传进度回调
|
||||
}
|
||||
|
||||
// ==================== 基础配置 ====================
|
||||
|
||||
// 配置 baseURL
|
||||
axios.defaults.baseURL = '你的后端域名'
|
||||
|
||||
// 配置超时时间(30秒)
|
||||
axios.defaults.timeout = 30000
|
||||
|
||||
// 配置默认请求头
|
||||
axios.defaults.headers.common['Content-Type'] = 'application/json'
|
||||
|
||||
// ==================== 请求白名单 ====================
|
||||
|
||||
/**
|
||||
* 请求白名单
|
||||
* 白名单内的接口不需要添加 Authorization 请求头
|
||||
* 例如:登录接口、注册接口等
|
||||
*/
|
||||
const requestWhiteList: string[] = []
|
||||
|
||||
// ==================== 加载动画管理 ====================
|
||||
|
||||
/**
|
||||
* 网络请求计数器
|
||||
* 用于管理多个并发请求的加载状态
|
||||
*/
|
||||
let netWorkNum = 0
|
||||
|
||||
/**
|
||||
* 开启加载动画
|
||||
* 使用计数器机制,防止多个请求导致的 loading 闪烁
|
||||
* @param title 加载提示文字
|
||||
*/
|
||||
function startLoading(title: string) {
|
||||
if (netWorkNum === 0) {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true,
|
||||
})
|
||||
}
|
||||
netWorkNum++
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭加载动画
|
||||
* 延迟 500ms 关闭,确保用户体验
|
||||
*/
|
||||
function endLoading() {
|
||||
netWorkNum--
|
||||
setTimeout(() => {
|
||||
if (netWorkNum === 0) {
|
||||
wx.hideLoading()
|
||||
wx.stopPullDownRefresh() // 停止当前页面下拉刷新
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// ==================== 请求拦截器 ====================
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config: CustomRequestConfig) => {
|
||||
// 加载提示
|
||||
if (config.loading ?? true) {
|
||||
startLoading('请稍等...')
|
||||
}
|
||||
|
||||
// Content-Type 设置
|
||||
// 注意:上传文件时 axios 会自动设置正确的 Content-Type
|
||||
if (config.headers && !config.headers['Content-Type']) {
|
||||
config.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// Token 自动注入(非白名单接口)
|
||||
if (!requestWhiteList.includes(config.url || '')) {
|
||||
const app = getApp<IAppOption>()
|
||||
if (app.globalData.token && config.headers) {
|
||||
config.headers['Authorization'] = app.globalData.token
|
||||
}
|
||||
}
|
||||
|
||||
// 详细日志记录
|
||||
/* eslint-disable no-console */
|
||||
console.log('============发送请求 start============')
|
||||
console.log(`[${new Date().toISOString()}] [INFO] 请求地址: ${config.baseURL}${config.url}`)
|
||||
console.log(`[${new Date().toISOString()}] [INFO] 请求方式: ${config.method?.toUpperCase()}`)
|
||||
console.log(`[${new Date().toISOString()}] [INFO] 上传模式: ${config.upload ? '是' : '否'}`)
|
||||
console.log(`[${new Date().toISOString()}] [INFO] params参数: ${JSON.stringify(config.params)}`)
|
||||
console.log(`[${new Date().toISOString()}] [INFO] data参数: ${JSON.stringify(config.data)}`)
|
||||
console.log('============发送请求 end============')
|
||||
/* eslint-enable no-console */
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 请求错误处理
|
||||
endLoading()
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// ==================== 响应拦截器 ====================
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 关闭加载动画
|
||||
const config = response.config as CustomRequestConfig
|
||||
if (config.loading ?? true) {
|
||||
endLoading()
|
||||
}
|
||||
|
||||
// 标准格式:{ code, data, message }
|
||||
const result = response.data as StandardResponse
|
||||
|
||||
if (result.code === 200) {
|
||||
// 只返回 data 字段(修改 response.data 为提取后的 data)
|
||||
return { ...response, data: result.data } as unknown as AxiosResponse
|
||||
} else {
|
||||
// 业务错误处理
|
||||
wx.showToast({
|
||||
title: result.message || '请求失败',
|
||||
icon: 'none',
|
||||
})
|
||||
return Promise.reject(new Error(result.message))
|
||||
}
|
||||
},
|
||||
(error: unknown) => {
|
||||
// 关闭加载动画
|
||||
const axiosError = error as {
|
||||
config?: CustomRequestConfig
|
||||
response?: AxiosResponse
|
||||
message?: string
|
||||
}
|
||||
const config = axiosError.config
|
||||
if (config?.loading ?? true) {
|
||||
endLoading()
|
||||
}
|
||||
|
||||
// 401 未授权,跳转登录页
|
||||
if (axiosError.response?.status === 401) {
|
||||
wx.showToast({
|
||||
title: '未授权,请重新登录',
|
||||
icon: 'none',
|
||||
})
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 统一错误提示
|
||||
const errorData = axiosError.response?.data as StandardResponse | undefined
|
||||
const message = errorData?.message || errorData?.data || axiosError.message || '网络请求失败'
|
||||
|
||||
wx.showToast({
|
||||
title: message as string,
|
||||
icon: 'none',
|
||||
})
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// ==================== 文件上传封装 ====================
|
||||
|
||||
/**
|
||||
* 文件上传函数
|
||||
* 基于 axios-miniprogram 内置的 upload 功能
|
||||
* @param options 上传配置
|
||||
* @returns Promise<T> 返回 data 字段的类型
|
||||
*/
|
||||
function upload<T = unknown>(options: UploadOptions): Promise<T> {
|
||||
const {
|
||||
apiUrl,
|
||||
filePath,
|
||||
name = 'file',
|
||||
formData = {},
|
||||
loading = true,
|
||||
onUploadProgress,
|
||||
} = options
|
||||
|
||||
return axios({
|
||||
url: apiUrl,
|
||||
method: 'POST',
|
||||
upload: true, // 启用上传模式
|
||||
data: {
|
||||
name, // 文件对应的 key
|
||||
filePath, // 要上传文件资源的路径
|
||||
...formData, // 额外的 formData 属性
|
||||
},
|
||||
loading, // 自定义 loading 参数(会被拦截器识别)
|
||||
onUploadProgress, // 上传进度回调
|
||||
} as CustomRequestConfig).then((res) => res.data as T)
|
||||
}
|
||||
|
||||
// ==================== 导出接口 ====================
|
||||
|
||||
// 默认导出 axios 实例(支持完整的泛型类型推导)
|
||||
export default axios
|
||||
|
||||
// 命名导出 upload 函数(支持泛型)
|
||||
export { upload }
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 格式化数字为两位数,不足两位前面补0
|
||||
* @param n 需要格式化的数字
|
||||
* @returns 格式化后的字符串(两位数)
|
||||
*/
|
||||
const formatNumber = (n: number) => {
|
||||
const s = n.toString()
|
||||
return s[1] ? s : '0' + s
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间为 YYYY/MM/DD HH:mm:ss 格式
|
||||
* @param date 需要格式化的日期对象
|
||||
* @returns 格式化后的日期时间字符串,格式为 YYYY/MM/DD HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return (
|
||||
[year, month, day].map(formatNumber).join('/') +
|
||||
' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param {Date|Number|String} date 日期对象、时间戳或日期字符串
|
||||
* @param {String} format 格式化模板,支持以下占位符:
|
||||
* - YYYY: 4位年份
|
||||
* - YY: 2位年份
|
||||
* - MM: 2位月份
|
||||
* - M: 1位月份
|
||||
* - DD: 2位日期
|
||||
* - D: 1位日期
|
||||
* - HH: 2位小时(24小时制)
|
||||
* - H: 1位小时(24小时制)
|
||||
* - hh: 2位小时(12小时制)
|
||||
* - h: 1位小时(12小时制)
|
||||
* - mm: 2位分钟
|
||||
* - m: 1位分钟
|
||||
* - ss: 2位秒
|
||||
* - s: 1位秒
|
||||
* - SSS: 3位毫秒
|
||||
* - A: 上午/下午
|
||||
* - a: 上午/下午(小写)
|
||||
* @return {String} 格式化后的日期时间字符串
|
||||
*/
|
||||
export const formatDateTime = (
|
||||
date: Date | number | string | undefined,
|
||||
format: string = 'YYYY/MM/DD HH:mm:ss',
|
||||
): string => {
|
||||
if (!date && date !== 0) return ''
|
||||
|
||||
let d: Date
|
||||
|
||||
// 1. 处理字符串类型的特殊清洗逻辑 (兼容原本 getDate 的脏数据处理能力)
|
||||
if (typeof date === 'string') {
|
||||
let cleanStr = date.trim()
|
||||
|
||||
// 替换中文符号和特殊符号为标准分隔符
|
||||
if (['年', '月', '日'].some((x) => cleanStr.includes(x))) {
|
||||
cleanStr = cleanStr.replace(/[年月日]/g, '/')
|
||||
}
|
||||
|
||||
// 补全简写日期 (例如 "2020" -> "2020/01/01") 以便 new Date 解析
|
||||
const parts = cleanStr.split(/[-/ :]/).filter(Boolean)
|
||||
if (parts.length === 1)
|
||||
cleanStr += '/01/01' // 只有年
|
||||
else if (parts.length === 2) cleanStr += '/01' // 只有年月
|
||||
|
||||
// 解决 iOS/Safari 不支持 "-" 连接符的问题
|
||||
cleanStr = cleanStr.replace(/-/g, '/')
|
||||
|
||||
d = new Date(cleanStr)
|
||||
} else if (typeof date === 'number') {
|
||||
d = new Date(date)
|
||||
} else {
|
||||
d = date
|
||||
}
|
||||
|
||||
// 有效性检查
|
||||
if (!(d instanceof Date) || isNaN(d.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 提取时间分量
|
||||
const year = d.getFullYear()
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const hours = d.getHours()
|
||||
const minutes = d.getMinutes()
|
||||
const seconds = d.getSeconds()
|
||||
const milliseconds = d.getMilliseconds()
|
||||
|
||||
// 定义替换映射表
|
||||
const matches: Record<string, string | number> = {
|
||||
YYYY: year,
|
||||
YY: String(year).slice(-2),
|
||||
MM: String(month).padStart(2, '0'),
|
||||
M: month,
|
||||
DD: String(day).padStart(2, '0'),
|
||||
D: day,
|
||||
HH: String(hours).padStart(2, '0'),
|
||||
H: hours,
|
||||
hh: String(hours % 12 || 12).padStart(2, '0'),
|
||||
h: hours % 12 || 12,
|
||||
mm: String(minutes).padStart(2, '0'),
|
||||
m: minutes,
|
||||
ss: String(seconds).padStart(2, '0'),
|
||||
s: seconds,
|
||||
SSS: String(milliseconds).padStart(3, '0'),
|
||||
A: hours < 12 ? '上午' : '下午',
|
||||
a: hours < 12 ? 'am' : 'pm',
|
||||
}
|
||||
|
||||
// 5. 生成结果
|
||||
return format.replace(/YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a/g, (match) =>
|
||||
String(matches[match]),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user