Compare commits

...

74 Commits

Author SHA1 Message Date
sexygoat
5de611d28f fix: ll
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 53s
2026-04-27 14:56:06 +08:00
sexygoat
24b2293d71 fix: ll 2026-04-27 14:45:03 +08:00
sexygoat
fbf5371086 fix: carrier_type
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m33s
2026-04-27 12:33:26 +08:00
sexygoat
f86281017a feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 50s
2026-04-24 15:03:31 +08:00
sexygoat
f51c8d912b feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-24 14:59:02 +08:00
sexygoat
bcc63f508a feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-24 14:57:42 +08:00
sexygoat
fad37f511c feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 48s
2026-04-24 14:54:31 +08:00
sexygoat
a9a1a557d9 feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-24 14:40:43 +08:00
sexygoat
383e7b819b feat: 钱包样式
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-24 14:36:34 +08:00
sexygoat
8f4ecda7eb feat: 钱包样式
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 49s
2026-04-24 14:30:35 +08:00
sexygoat
71bb404fe1 feat: 钱包样式
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 48s
2026-04-24 14:24:57 +08:00
sexygoat
ef77905dbb feat: 钱包样式
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 49s
2026-04-24 14:18:35 +08:00
sexygoat
69923b81b0 feat: 钱包样式
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Has been cancelled
2026-04-24 14:17:46 +08:00
sexygoat
791e034a6b feat: 跳转地址提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 51s
2026-04-24 14:15:13 +08:00
sexygoat
42f4d9f96a fix: bug
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 53s
2026-04-24 10:27:35 +08:00
sexygoat
1d6c12e8d7 fix: WiFi弹窗
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 55s
2026-04-23 17:15:21 +08:00
sexygoat
8dc8600b52 fix: 弹窗提示
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-23 16:49:24 +08:00
sexygoat
94a31b2e47 fix: bug
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m26s
2026-04-23 16:32:24 +08:00
sexygoat
8a8393eede fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-17 16:57:09 +08:00
sexygoat
882ec0a1b8 fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-17 16:55:28 +08:00
sexygoat
093c337bc4 fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-17 16:51:52 +08:00
sexygoat
e2007b008a fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-17 16:48:36 +08:00
sexygoat
372e22f4b7 fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 50s
2026-04-17 16:45:45 +08:00
sexygoat
0ada36a778 fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-17 16:31:37 +08:00
sexygoat
547d944812 fix: 扫一扫1
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-17 16:20:12 +08:00
sexygoat
bdaada8224 fix: 扫一扫1
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-17 16:06:06 +08:00
sexygoat
b3fea2100b fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-17 16:01:00 +08:00
sexygoat
1e03191e82 fix: 扫一扫
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 19s
2026-04-17 15:57:13 +08:00
sexygoat
25fc534619 fix: 扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-17 15:48:55 +08:00
sexygoat
5dc948efec fix: 扫一扫
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 20s
2026-04-17 15:35:03 +08:00
sexygoat
fc7305dffe fix: 扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-17 15:14:15 +08:00
sexygoat
35fa5fd2ae refactor: 删除多余的uview-plus SCSS导入,简化vite配置
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 19s
2026-04-17 15:10:02 +08:00
sexygoat
898aa41fd2 refactor: 删除多余的uview-plus SCSS导入,简化vite配置
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 20s
2026-04-17 15:07:40 +08:00
sexygoat
22538a8972 refactor: 删除多余的uview-plus SCSS导入,简化vite配置
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 14s
2026-04-17 15:03:14 +08:00
sexygoat
379ac883f0 fix: 移除多余的uview-plus SCSS导入,使用正确的JS配置方式
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 22s
2026-04-17 14:51:26 +08:00
sexygoat
2d27f55d69 fix: 修复SCSS导入路径和uni-app构建配置
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 10s
2026-04-17 14:43:26 +08:00
sexygoat
62751369a2 fix: 修复SCSS导入路径和uni-app构建配置
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 20s
2026-04-17 14:40:32 +08:00
sexygoat
10f8a1658e fix: 修复uni-app构建路径问题
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 45s
2026-04-17 14:34:20 +08:00
sexygoat
bbb840e521 fix: 修复SCSS编译和微信扫码功能
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 56s
2026-04-17 14:30:19 +08:00
sexygoat
404daa4b6d fix: 修复扫一扫登录
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 8s
2026-04-17 14:25:42 +08:00
sexygoat
40b8f76eef fix: 修复扫一扫登录
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 20s
2026-04-17 12:20:02 +08:00
sexygoat
5f4a26e693 fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 51s
2026-04-17 11:18:46 +08:00
sexygoat
f72b77fe0d fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 48s
2026-04-17 11:03:04 +08:00
sexygoat
39773341bd fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-17 10:56:46 +08:00
sexygoat
16a47c1229 fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-17 10:51:10 +08:00
sexygoat
715e4f6f7d fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 53s
2026-04-17 10:45:26 +08:00
sexygoat
b3c925b0ca fix: 修复扫一扫登录
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 51s
2026-04-17 09:40:34 +08:00
sexygoat
263515758c feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 18:15:23 +08:00
sexygoat
f4ac65acce feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 18:09:59 +08:00
sexygoat
f5f993d1e2 feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m24s
2026-04-16 17:57:38 +08:00
sexygoat
63af194ec5 feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m25s
2026-04-16 17:47:27 +08:00
sexygoat
6d327c41cd feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 57s
2026-04-16 17:41:07 +08:00
sexygoat
bd0bb149b8 feat: 登录扫一扫
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 43s
2026-04-16 17:31:46 +08:00
sexygoat
102e6c0867 feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-16 17:14:43 +08:00
sexygoat
b56f68636c feat: 登录扫一扫
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 17:11:09 +08:00
sexygoat
e0db2b6df0 feat: 登录缓存token
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 17:01:36 +08:00
sexygoat
25e9946c17 feat: 登录缓存token
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 49s
2026-04-16 16:56:52 +08:00
sexygoat
64f479c62a fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 12:10:19 +08:00
sexygoat
c1172f7591 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 12:00:04 +08:00
sexygoat
04ee3f2c87 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 11:54:57 +08:00
sexygoat
4cb1e65e6e fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 50s
2026-04-16 11:29:45 +08:00
sexygoat
89aafa7317 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-16 11:13:57 +08:00
sexygoat
e2d3d8172f fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 11:08:45 +08:00
sexygoat
7c35a0bad6 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 11:00:38 +08:00
sexygoat
bd0fc9cfdb fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 10:53:57 +08:00
sexygoat
cca7131d68 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 10:49:44 +08:00
sexygoat
0337d86ba2 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 10:46:01 +08:00
sexygoat
89a6441f62 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 10:44:20 +08:00
sexygoat
58d4b92287 fix: UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 10:38:56 +08:00
sexygoat
733fcc5511 fix: 订单列表
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 10:34:13 +08:00
sexygoat
c06d543af3 fix: 封装微信支付
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 46s
2026-04-16 10:28:02 +08:00
sexygoat
6b066e35da fix: 修改布局UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 50s
2026-04-16 10:21:11 +08:00
sexygoat
41349ac833 fix: 修改布局UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 47s
2026-04-16 10:01:15 +08:00
sexygoat
c6246c023d fix: 修改布局UI
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m21s
2026-04-16 09:54:55 +08:00
34 changed files with 7270 additions and 1671 deletions

View File

@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(npm list:*)"
],
"deny": [],
"ask": []

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
.git
.gitignore
*.md
.vscode
.idea
dist
unpackage
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env.local
.env.*.local

13
App.vue
View File

@@ -5,8 +5,6 @@
</template>
<style lang="scss">
@import "uview-plus/index.scss";
:root {
/* 主色 */
--primary: #0A84FF;
@@ -41,7 +39,7 @@
/* 背景色 */
--bg-primary: #FFFFFF;
--bg-secondary: #F2F2F7;
--bg-secondary: #F8F8F8;
--bg-tertiary: #FFFFFF;
--bg-grouped: #F2F2F7;
@@ -95,7 +93,8 @@
page {
color: var(--text-primary);
background-color: var(--bg-secondary);
background: var(--bg-secondary);
// min-height: 100vh;
}
.container {
@@ -105,6 +104,12 @@
padding: 30rpx 20rpx;
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.pd-20{
padding: 20rpx;
}
.card {

View File

@@ -18,9 +18,20 @@ COPY package.json ./
# 安装所有依赖
RUN npm install --legacy-peer-deps
# 安装 git (用于获取 commit 信息)
RUN apk add --no-cache git
# 复制源代码
COPY . .
# 列出文件确认复制成功
RUN ls -la
# 创建 src 目录软链接 (兼容 uni-app 新版本)
RUN mkdir -p src && \
ln -sf /build/manifest.json /build/src/manifest.json && \
ln -sf /build/pages.json /build/src/pages.json
# 构建 H5 生产版本
RUN npm run build:h5

View File

@@ -4,4 +4,5 @@ export { deviceApi } from './modules/device.js';
export { exchangeApi } from './modules/exchange.js';
export { orderApi } from './modules/order.js';
export { realnameApi } from './modules/realname.js';
export { walletApi } from './modules/wallet.js';
export { walletApi } from './modules/wallet.js';
export { wechatApi } from './modules/wechat.js';

11
api/modules/wechat.js Normal file
View File

@@ -0,0 +1,11 @@
import request from '@/utils/request.js';
export const wechatApi = {
getJssdkConfig(url) {
return request({
url: '/api/c/v1/wechat/jssdk-config',
method: 'GET',
data: { url }
});
}
};

View File

@@ -2,7 +2,7 @@
<view class="card device-status-card">
<view class="card-header mb-md flex-row-sb">
<view class="title">设备信息</view>
<view class="tag-apple tag-success">{{ getOperator(deviceInfo.currentIccid) }}</view>
<view class="tag-apple tag-success">{{ deviceInfo.carrier_name || '-' }}</view>
</view>
<view class="device-info flex-col-g16">
<view class="info-row flex-row-sb">
@@ -17,7 +17,8 @@
</view>
<view class="info-row flex-row-sb">
<view class="info-label">
<view class="subtitle">到期时间{{ deviceInfo.expireDate }}</view>
<view class="subtitle" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</view>
<view class="subtitle" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</view>
</view>
<view class="info-values flex-row-g20">
<view class="tag-apple tag-primary">{{ statusText }}</view>
@@ -47,15 +48,12 @@
const props = defineProps({
deviceInfo: { type: Object, default: () => ({}) },
isRealName: { type: Boolean, default: false },
isDevice: { type: Boolean, default: true },
opratorList: { type: Array, default: () => [] }
});
defineEmits(['authentication']);
const getOperator = (iccid) => {
return '中国电信';
};
const getSignalText = (rssi) => rssi || '强';
const statusText = computed(() => {

View File

@@ -53,7 +53,7 @@
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'asset-package')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/shop.png" mode="aspectFit" alt="资产套餐历史"></image>
<image src="/static/asset-package-history.png" mode="aspectFit" alt="资产套餐历史"></image>
</view>
<view class="function-name">资产套餐历史</view>
</view>
@@ -61,8 +61,8 @@
<view class="function-icon">
<image src="/static/bind-phone.png" mode="aspectFit" alt="绑定手机号"></image>
</view>
<view :class="['function-name', alreadyBindPhone ? 'text-primary' : '']">
{{ alreadyBindPhone ? '已绑定' : '绑定手机号' }}
<view :class="['function-name', alreadyBindPhone ? 'text-primary' : 'text-danger']">
{{ alreadyBindPhone ? '已绑定' : '绑定' }}
</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'change-phone')" role="button"

View File

@@ -108,17 +108,13 @@
// 只有一个生效中的套餐
const activePackage = data.items[0];
if (activePackage.enable_virtual_data) {
// 启用虚流量
if (activePackage.enable_virtual_data) {
usedMB.value = activePackage.virtual_used_mb || 0;
totalMB.value = activePackage.virtual_limit_mb || 0;
remainMB.value = activePackage.virtual_remain_mb || 0;
} else {
// 未启用虚流量,使用真流量
usedMB.value = activePackage.data_usage_mb || 0;
totalMB.value = activePackage.data_limit_mb || 0;
remainMB.value = totalMB.value - usedMB.value;
}
totalMB.value = activePackage.data_limit_mb || 0;
remainMB.value = totalMB.value - usedMB.value;
// 将套餐信息(包括到期时间)传递给父组件
emit('packageLoaded', activePackage);

View File

@@ -8,8 +8,7 @@
<view class="flex-row-g20">
<view class="title">{{ currentCardNo || '-' }}</view>
</view>
<view class="caption" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</view>
<view class="caption" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</view>
<view class="caption">套餐到期时间{{ deviceInfo.expireDate || '-' }}</view>
</view>
<view v-if="isDevice" class="tag-apple" :class="onlineStatus === '在线' ? 'tag-success' : 'tag-warning'">
{{ onlineStatus }}

View File

@@ -44,4 +44,8 @@
.wifi-details { flex: 1; }
}
}
.btn-group{
padding-right: var(--space-md);
}
</style>

View File

@@ -0,0 +1,187 @@
# uniapp H5 调用微信扫一扫配置文档
## 一、微信 JSSDK 加载
**方式:通过 CDN 加载微信官方 JSSDK**
`index.html``<head>` 中添加:
```html
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
```
> **注意:** 不要使用 `import wx from 'jweixin-module'` 或 npm 包方式导入uni-app 会错误解析导致构建失败。
---
## 二、wxsdk.js 封装
创建 `utils/wxsdk.js`
```javascript
// utils/wxsdk.js
import { wechatApi } from '@/api/index.js'
// 注意:是 jWeixin不是 wx
// window.wx 会被 uni-app 框架占用为 Vue 模块
const wx = window.jWeixin
/**
* 初始化微信 JS-SDK
*/
export async function initWxConfig() {
const url = window.location.href.split('#')[0]
try {
const data = await wechatApi.getJssdkConfig(url)
const { app_id, timestamp, nonce_str, signature } = data
return new Promise((resolve, reject) => {
wx.config({
debug: true,
appId: app_id,
timestamp: timestamp,
nonceStr: nonce_str,
signature: signature,
jsApiList: ['scanQRCode']
})
wx.ready(() => {
console.log('微信 SDK 就绪')
resolve()
})
wx.error((err) => {
reject(err)
})
})
} catch (e) {
throw e
}
}
/**
* 调起微信扫一扫
* @returns {Promise<string>} 扫码结果字符串
*/
export function wxScan() {
return new Promise((resolve, reject) => {
wx.scanQRCode({
needResult: 1,
scanType: ['qrCode', 'barCode'],
success(res) {
resolve(res.resultStr)
},
fail(err) {
reject(new Error(err?.errMsg || '扫码失败'))
}
})
})
}
/**
* 检查是否在微信环境中
*/
export function isInWechat() {
return /micromessenger/i.test(navigator.userAgent)
}
```
---
## 三、后端接口要求
后端需提供 `/api/c/v1/wechat/jssdk-config` 接口,返回格式:
```json
{
"app_id": "wx1234567890",
"timestamp": 1234567890,
"nonce_str": "randomstring",
"signature": "xxxxx"
}
```
> **注意:** `nonce_str` 必须与生成 signature 时使用的一致
---
## 四、前端调用示例
```javascript
import { initWxConfig, wxScan, isInWechat } from '@/utils/wxsdk.js'
async function handleScan() {
if (!isInWechat()) {
uni.showToast({ title: '请在微信环境中使用', icon: 'none' })
return
}
try {
// 1. 初始化微信 SDK 配置
await initWxConfig()
// 2. 调起扫一扫
const result = await wxScan()
console.log('扫码结果:', result)
// 在此处理扫码结果
} catch (err) {
console.error('扫码错误:', err)
uni.showToast({ title: err.message || JSON.stringify(err), icon: 'none' })
}
}
```
---
## 五、注意事项
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| `window.wx` 输出 `Module {$emit...}` | `window.wx` 被 uni-app 框架占用为 Vue 事件模块 | 使用 `window.jWeixin` |
| `jweixin-module` 打包报 `"default" is not exported` | uni 构建系统错误解析无 default export 的 UMD 模块 | 使用 CDN 加载,不走 npm |
| `wx.scanQRCode is not a function` | 微信 SDK 未正确加载 | 确认 `window.jWeixin` 有值 |
| 扫码提示 `{}` | `catch` 捕获空错误对象 | 需将错误信息转换为 `Error` 对象返回 |
---
## 六、调试建议
在微信开发者工具中打开控制台,检查以下变量:
```javascript
// 错误情况:被 uni-app 框架占用为 Vue 模块
window.wx
// 输出Module {$emit: ƒ, $off: ƒ, $on: ƒ, $once: ƒ, ...}
// 正确情况:微信 JSSDK
window.jWeixin
// 输出:{config: ƒ, ready: ƒ, error: ƒ, checkJsApi: ƒ, ...}
```
---
## 七、完整文件路径
```
project/
├── index.html # 引入微信 JSSDK CDN
├── utils/
│ └── wxsdk.js # 微信扫一扫封装
└── api/
└── modules/
└── wechat.js # 后端接口封装
```
`api/modules/wechat.js` 示例:
```javascript
import request from '@/utils/request.js'
export const wechatApi = {
getJssdkConfig(url) {
return request({
url: '/api/c/v1/wechat/jssdk-config',
method: 'GET',
data: { url }
})
}
}
```

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"/>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div id="app"><!--app-html--></div>

15
main.js
View File

@@ -16,7 +16,20 @@ app.$mount()
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
// 使用 uview-plus (版本 3.4.0+)
app.use(uviewPlus, () => {
return {
options: {
config: {
// 默认单位 rpx
unit: 'rpx'
},
props: {
// 可以在这里配置组件默认属性
}
}
}
})
return {
app
}

View File

@@ -17,7 +17,10 @@
"delay" : 0
},
/* */
"modules" : {},
"modules" : {
"Barcode" : {},
"Camera" : {}
},
/* */
"distribute" : {
/* android */
@@ -97,6 +100,8 @@
}
}
},
"sdkConfigs" : {}
"sdkConfigs" : {
"maps" : {}
}
}
}

7072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
{
"scripts": {
"dev:h5": "uni",
"build:h5": "UNI_INPUT_DIR=. uni build -p h5"
"build:h5": "cross-env UNI_INPUT_DIR=. uni build -p h5"
},
"devDependencies": {
"@dcloudio/types": "3.4.19",
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"cross-env": "^10.1.0",
"less": "^4.0.0",
"sass": "^1.63.2",
"vite": "5.2.8"
@@ -16,9 +17,10 @@
"@dcloudio/uni-app": "3.0.0-4080720251210001",
"@dcloudio/uni-components": "3.0.0-4080720251210001",
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
"vue": "^3.4.0",
"clipboard": "^2.0.11",
"dayjs": "^1.11.19",
"uview-plus": "^3.6.29"
"jweixin-module": "^1.6.0",
"uview-plus": "^3.6.29",
"vue": "^3.4.0"
}
}

View File

@@ -84,8 +84,8 @@
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "信息查询",
"navigationBarBackgroundColor": "#F8F8F8",
"navigationBarTitleText": "设备卡信息查询",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}

View File

@@ -15,30 +15,30 @@
</view>
<view class="package-info">
<view class="info-row flex-row-sb">
<!-- <view class="info-row flex-row-sb">
<view class="info-label">套餐类型</view>
<view class="info-value">{{ item.package_type === 'formal' ? '普通套餐' : '加油包' }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">使用类型</view>
<view class="info-value">{{ item.usage_type === 'single_card' ? '单卡' : '设备' }}</view>
</view>
</view> -->
<view class="info-row flex-row-sb">
<view class="info-label">激活时间</view>
<view class="info-value">{{ item.activated_at || '-' }}</view>
</view>
<view class="info-row flex-row-sb">
<!-- <view class="info-row flex-row-sb">
<view class="info-label">创建时间</view>
<view class="info-value">{{ item.created_at }}</view>
</view>
</view> -->
<view class="info-row flex-row-sb">
<view class="info-label">到期时间</view>
<view class="info-value">{{ item.expires_at || '-' }}</view>
</view>
<view class="info-row flex-row-sb">
<!-- <view class="info-row flex-row-sb">
<view class="info-label">优先级</view>
<view class="info-value">{{ item.priority }}</view>
</view>
</view> -->
</view>
<view class="divider"></view>

View File

@@ -3,7 +3,7 @@
<!-- 自定义导航栏 -->
<view v-if="!fromLogin" class="custom-navbar">
<view class="navbar-back" @tap="goBack">
<text class="back-text">🔙</text>
<image class="back-icon" src="../../static/left.png" mode="aspectFit" />
</view>
<view class="navbar-title">绑定手机号</view>
<view class="navbar-placeholder"></view>
@@ -129,7 +129,9 @@
};
const goBack = () => {
uni.navigateBack();
uni.navigateTo({
url: '/pages/index/index'
})
};
</script>
@@ -137,6 +139,10 @@
.page {
min-height: 100vh;
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.custom-navbar {
@@ -160,8 +166,8 @@
cursor: pointer;
.back-icon {
font-size: 36rpx;
color: var(--text-primary);
width: 40rpx;
height: 40rpx;
}
.back-text {

View File

@@ -4,7 +4,7 @@
:isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status" />
<DeviceStatusCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" :isRealName="isRealName"
@authentication="enterDetail('authentication')" />
:isDevice="userInfo.isDevice" @authentication="enterDetail('authentication')" />
<!-- <VoiceCard v-if="!userInfo.isDevice" :voiceStats="voiceStats" :dateRangeText="dateRangeText"
@openDatePicker="openDateRangePicker" /> -->
@@ -25,11 +25,11 @@
<view class="flex-col-g20">
<view class="flex-col-g8">
<label class="caption">名称:</label>
<up-input placeholder="WIFI名称" border="surround" v-model="wifi_info.ssid" />
<up-input placeholder="WIFI名称" class="wifi-input" v-model="wifi_info.ssid" />
</view>
<view class="flex-col-g8">
<label class="caption">密码:</label>
<up-input placeholder="WIFI密码" border="surround" v-model="wifi_info.pwd" />
<up-input placeholder="WIFI密码" class="wifi-input" v-model="wifi_info.pwd" />
</view>
</view>
<view class="btn-group mt-20">
@@ -229,9 +229,9 @@
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
// 当前卡信息
let currentCard = null;
if (data.cards && data.cards.length > 0) {
// 找到当前卡
const currentCard = data.cards.find(card => card.is_current) || data.cards[0];
currentCard = data.cards.find(card => card.is_current) || '';
deviceInfo.currentIccid = currentCard.iccid || '-';
// 卡列表
@@ -245,6 +245,15 @@
deviceInfo.mchList = [];
}
// 运营商信息
const carrierTypeMap = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
};
deviceInfo.carrier_name = (currentCard && carrierTypeMap[currentCard.carrier_type]) || '-';
// 设备实时信息
if (data.device_realtime) {
const rt = data.device_realtime;
@@ -379,15 +388,27 @@
// #endif
};
const enterBack = () => {
const enterBack = async () => {
if (!deviceInfo.lan_ip || deviceInfo.lan_ip === '-') {
uni.showToast({
title: '暂无后台地址',
icon: 'none'
uni.showModal({
title: '提示',
content: '请将设备开机后, 等待2分钟左右进行连接WiFi',
showCancel: false,
buttonText: '我知道了'
});
return;
}
await new Promise((resolve) => {
uni.showModal({
title: '提示',
content: '请先连接当前设备WiFi否则无法访问后台管理',
cancelText: '取消',
confirmText: '我知道了',
success: (res) => res.confirm && resolve()
});
});
// 跳转到设备后台管理地址
const url = deviceInfo.lan_ip.startsWith('http') ? deviceInfo.lan_ip : `http://${deviceInfo.lan_ip}`;
@@ -593,52 +614,55 @@
return;
}
uni.showLoading({
title: '同步中...',
mask: true
uni.showModal({
title: '确认同步',
content: '确定要同步运营商数据吗?',
success: (res) => {
if (!res.confirm) return;
uni.showLoading({
title: '同步中...',
mask: true
});
assetApi.refresh(identifier).then((data) => {
uni.hideLoading();
if (data.accepted) {
const refreshTypeText = data.refresh_type === 'device' ? '设备' : '卡';
uni.showToast({
title: `${refreshTypeText}数据同步成功`,
icon: 'success',
duration: 2000
});
setTimeout(() => {
loadAssetInfo();
}, 2000);
} else {
uni.showToast({
title: '同步失败,请稍后重试',
icon: 'none'
});
}
}).catch((e) => {
uni.hideLoading();
console.error('同步数据失败', e);
if (e.msg && e.msg.includes('冷却')) {
uni.showToast({
title: e.msg,
icon: 'none',
duration: 2000
});
} else {
uni.showToast({
title: '同步失败',
icon: 'none'
});
}
});
}
});
try {
const data = await assetApi.refresh(identifier);
uni.hideLoading();
if (data.accepted) {
const refreshTypeText = data.refresh_type === 'device' ? '设备' : '卡';
uni.showToast({
title: `${refreshTypeText}数据同步成功`,
icon: 'success',
duration: 2000
});
// 同步成功后,延迟重新加载资产信息
setTimeout(() => {
loadAssetInfo();
}, 2000);
} else {
uni.showToast({
title: '同步失败,请稍后重试',
icon: 'none'
});
}
} catch (e) {
uni.hideLoading();
console.error('同步数据失败', e);
// 如果是冷却时间错误,显示提示
if (e.msg && e.msg.includes('冷却')) {
uni.showToast({
title: e.msg,
icon: 'none',
duration: 2000
});
} else {
uni.showToast({
title: '同步失败',
icon: 'none'
});
}
}
};
const enterDetail = (name) => {
@@ -712,6 +736,11 @@
};
onMounted(() => {
const token = uni.getStorageSync('token');
if (!token) {
uni.reLaunch({ url: '/pages/login/login' });
return;
}
initCurrentMonth();
loadAssetInfo();
});
@@ -727,6 +756,12 @@
font-weight: 600;
color: var(--text-primary);
}
.wifi-input {
border: 2rpx solid var(--border-light);
border-radius: 8rpx;
padding: 8rpx 16rpx;
}
}
.bottom-spacer {

View File

@@ -1,63 +1,64 @@
<template>
<view class="login-page">
<view class="overlay"></view>
<view class="animated-bg">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
<view class="circle circle-4"></view>
<view class="circle circle-5"></view>
<view class="shape shape-triangle"></view>
<view class="shape shape-square"></view>
<view class="shape shape-pentagon"></view>
<view class="shape shape-hexagon"></view>
</view>
<view class="content-wrapper">
<view class="content">
<view class="header">
<view class="main-title">登录</view>
<view class="main-title">欢迎登录</view>
<view class="sub-title">请输入标识进行登录</view>
</view>
<view class="form-section">
<view class="input-wrap" :class="{ focus: inputFocus }">
<input
v-model="identifier"
class="input"
placeholder="请输入标识"
placeholder-class="placeholder"
@focus="inputFocus = true"
@blur="inputFocus = false"
@input="showError = false"
/>
</view>
<text v-if="showError" class="error">请输入标识</text>
</view>
<view class="btn-section">
<button
class="btn-login"
:class="{ disabled: !identifier || loading || !agreed }"
:disabled="!identifier || loading || !agreed"
@tap="handleLogin"
>
<view v-if="loading" class="loading">
<view class="dot"/>
<view class="dot"/>
<view class="dot"/>
<view class="form-section">
<view class="input-wrap" :class="{ focus: inputFocus }">
<input
v-model="identifier"
class="input"
placeholder="请输入标识"
placeholder-class="placeholder"
@focus="inputFocus = true"
@blur="handleBlur"
@input="showError = false"
/>
<image
v-if="identifier && inputFocus"
class="clear-icon"
src="/static/clear.png"
mode="aspectFit"
@mousedown.prevent="clearInput"
@touchstart.prevent="clearInput"
></image>
</view>
<text v-if="showError" class="error">请输入标识</text>
</view>
<view class="btn-section">
<button
class="btn-login"
:class="{ disabled: !identifier || loading || !agreed }"
:disabled="!identifier || loading || !agreed"
@tap="handleLogin"
>
<view v-if="loading" class="loading">
<view class="dot"/>
<view class="dot"/>
<view class="dot"/>
</view>
<text v-else>立即登录</text>
</button>
<button class="btn-scan" @tap="handleScanLogin">
<image src="/static/scan-code.png" mode="aspectFit" class="scan-icon"></image>
<text>扫一扫登录</text>
</button>
</view>
<view class="agreement">
<view class="agreement-text">
<text class="text">登录即表示同意</text>
<text class="link">用户协议</text>
<text class="text"></text>
<text class="link">隐私政策</text>
</view>
<text v-else>登录</text>
</button>
</view>
<view class="agreement">
<view class="agreement-text">
<text class="text">登录即表示同意</text>
<text class="link">用户协议</text>
<text class="text"></text>
<text class="link">隐私政策</text>
</view>
</view>
</view>
</view>
</view>
@@ -67,10 +68,32 @@
import { ref, onMounted } from 'vue';
import { authApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import { APP_ID, USE_WECHAT_AUTH } from '@/utils/env.js';
import { APP_ID } from '@/utils/env.js';
import { initWxConfig, wxScan, isInWechat } from '@/utils/wxsdk.js';
const userStore = useUserStore();
onMounted(async () => {
const token = uni.getStorageSync('token');
if (token) {
uni.reLaunch({ url: '/pages/index/index' });
}
handleWechatCallback();
getPathDeviceId();
// 初始化微信 SDK (仅在微信浏览器内执行)
// #ifdef H5
if (isInWechat()) {
try {
await initWxConfig();
console.log('微信 SDK 初始化成功');
} catch (e) {
console.error('微信 SDK 初始化失败', e);
}
}
// #endif
});
const identifier = ref('');
const loading = ref(false);
const agreed = ref(true);
@@ -88,11 +111,23 @@
const redirectToWxAuth = (assetToken) => {
sessionStorage.setItem('assetToken', assetToken);
const redirectUri = encodeURIComponent('https://cmp-c.boss160.cn/');
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=wechat_auth#wechat_redirect`;
window.location.href = url;
};
const clearInput = () => {
identifier.value = '';
showError.value = false;
inputFocus.value = true;
};
const handleBlur = () => {
setTimeout(() => {
inputFocus.value = false;
}, 200);
};
const handleLogin = () => {
if (!identifier.value) {
showError.value = true;
@@ -101,6 +136,57 @@
doLogin();
};
const handleScanLogin = async () => {
// #ifdef H5
try {
// 检查是否在微信环境
if (!isInWechat()) {
uni.showToast({ title: '请在微信中打开', icon: 'none' });
return;
}
// 调用扫一扫
const resultStr = await wxScan();
console.log('扫码结果:', resultStr);
let value = '';
// 解析二维码内容
if (resultStr.includes('?')) {
const params = new URLSearchParams(resultStr.split('?')[1]);
value = params.get('cardNum') || '';
}
if (!value) {
const match = resultStr.match(/=([^=]+)$/);
value = match ? match[1] : '';
}
if (value) {
identifier.value = value;
handleLogin();
} else {
uni.showToast({ title: '无效的二维码', icon: 'none' });
}
} catch (err) {
console.error('扫码错误:', err);
// uni.showToast({ title: JSON.stringify(err), icon: 'none' });
}
// #endif
// #ifndef H5
uni.showToast({ title: '请在微信中打开', icon: 'none' });
// #endif
};
const getPathDeviceId = () => {
const params = new URLSearchParams(window.location.search);
const deviceId = params.get('device_id');
if (deviceId) {
identifier.value = deviceId;
handleLogin();
}
};
const doLogin = async () => {
loading.value = true;
try {
@@ -108,20 +194,6 @@
userStore.setAssetToken(verifyData.asset_token);
userStore.setIdentifier(identifier.value);
if (!USE_WECHAT_AUTH) {
const loginData = await authApi.wechatLogin(verifyData.asset_token, 'mock_code');
userStore.setToken(loginData.token);
uni.setStorageSync('identifier', userStore.state.identifier);
// 判断是否需要绑定手机号
if (loginData.need_bind_phone) {
uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' });
} else {
uni.redirectTo({ url: '/pages/index/index' });
}
return;
}
redirectToWxAuth(verifyData.asset_token);
} catch (e) {
console.error('登录失败', e);
@@ -130,7 +202,6 @@
};
const handleWechatCallback = async () => {
if (!USE_WECHAT_AUTH) return;
const code = getCode();
const assetToken = sessionStorage.getItem('assetToken');
@@ -145,44 +216,18 @@
userStore.setToken(loginData.token);
uni.setStorageSync('identifier', userStore.state.identifier);
// H5 环境获取微信用户信息
// #ifdef H5
try {
// 通过 uni.getUserProfile 获取用户信息(需要用户点击授权)
// 注意:这个方法在微信公众号 H5 中不可用,需要通过后端接口获取
// 这里暂时保存默认信息,实际应该由后端返回用户信息
if (loginData.user_info) {
userStore.setUserInfo({
avatar: loginData.user_info.avatar || '',
nickname: loginData.user_info.nickname || ''
});
}
} catch (err) {
console.log('获取用户信息失败', err);
}
// #endif
// #ifdef MP-WEIXIN
// 小程序环境可以直接获取用户信息
try {
const { userInfo } = await uni.getUserProfile({
desc: '用于完善用户资料'
});
if (loginData.user_info) {
userStore.setUserInfo({
avatar: userInfo.avatarUrl || '',
nickname: userInfo.nickName || ''
avatar: loginData.user_info.avatar || '',
nickname: loginData.user_info.nickname || ''
});
} catch (err) {
console.log('用户取消授权');
}
// #endif
// 判断是否需要绑定手机号
if (loginData.need_bind_phone) {
// 需要绑定手机号,跳转到绑定页面
uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' });
} else {
// 不需要绑定,直接进入首页
uni.redirectTo({ url: '/pages/index/index' });
}
} catch (e) {
@@ -190,343 +235,172 @@
loading.value = false;
}
};
onMounted(() => {
handleWechatCallback();
});
</script>
<style lang="scss" scoped>
.login-page {
position: relative;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #005cbf 0%, #007aff 50%, #4da6ff 100%);
align-items: center;
justify-content: center;
min-height: 100vh;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 50%);
z-index: 0;
}
.animated-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 0;
}
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
}
.circle-1 {
width: 300rpx;
height: 300rpx;
top: 10%;
left: -100rpx;
animation: float1 20s infinite ease-in-out;
}
.circle-2 {
width: 200rpx;
height: 200rpx;
top: 60%;
right: -50rpx;
animation: float2 15s infinite ease-in-out;
}
.circle-3 {
width: 150rpx;
height: 150rpx;
top: 30%;
right: 20%;
animation: float3 18s infinite ease-in-out;
}
.circle-4 {
width: 250rpx;
height: 250rpx;
bottom: 10%;
left: 10%;
animation: float4 22s infinite ease-in-out;
}
.circle-5 {
width: 180rpx;
height: 180rpx;
top: 50%;
left: 50%;
animation: float5 16s infinite ease-in-out;
}
@keyframes float1 {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: 0.3;
}
50% {
transform: translate(100rpx, -80rpx) scale(1.2);
opacity: 0.5;
}
}
@keyframes float2 {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: 0.2;
}
50% {
transform: translate(-80rpx, 100rpx) scale(1.3);
opacity: 0.4;
}
}
@keyframes float3 {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: 0.25;
}
50% {
transform: translate(-60rpx, -60rpx) scale(1.1);
opacity: 0.45;
}
}
@keyframes float4 {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: 0.3;
}
50% {
transform: translate(120rpx, -100rpx) scale(1.15);
opacity: 0.5;
}
}
@keyframes float5 {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.2;
}
50% {
transform: translate(calc(-50% + 80rpx), calc(-50% + 80rpx)) scale(1.25);
opacity: 0.4;
}
}
.shape {
position: absolute;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(1px);
}
.shape-triangle {
width: 0;
height: 0;
background: transparent;
border-left: 80rpx solid transparent;
border-right: 80rpx solid transparent;
border-bottom: 140rpx solid rgba(255, 255, 255, 0.1);
top: 15%;
right: 15%;
animation: rotate1 25s infinite linear;
}
.shape-square {
width: 120rpx;
height: 120rpx;
top: 70%;
left: 5%;
transform: rotate(45deg);
animation: float-rotate 20s infinite ease-in-out;
}
.shape-pentagon {
width: 100rpx;
height: 100rpx;
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
top: 40%;
left: 80%;
animation: pulse 18s infinite ease-in-out;
}
.shape-hexagon {
width: 90rpx;
height: 90rpx;
clip-path: polygon(30% 0%, 70% 0%, 100% 50%, 70% 100%, 30% 100%, 0% 50%);
bottom: 25%;
right: 25%;
animation: float-spin 22s infinite ease-in-out;
}
@keyframes rotate1 {
0% {
transform: rotate(0deg);
opacity: 0.15;
}
50% {
opacity: 0.25;
}
100% {
transform: rotate(360deg);
opacity: 0.15;
}
}
@keyframes float-rotate {
0%, 100% {
transform: translate(0, 0) rotate(45deg);
opacity: 0.2;
}
50% {
transform: translate(80rpx, -60rpx) rotate(225deg);
opacity: 0.35;
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.2;
}
50% {
transform: scale(1.3);
opacity: 0.4;
}
}
@keyframes float-spin {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
opacity: 0.25;
}
50% {
transform: translate(-70rpx, 90rpx) rotate(180deg);
opacity: 0.4;
}
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
padding: 40rpx;
box-sizing: border-box;
}
.content-wrapper {
position: relative;
width: 100%;
}
.content {
background: var(--bg-primary);
border-radius: var(--radius-large);
padding: 60rpx 40rpx;
z-index: 1;
box-shadow: var(--shadow-medium);
}
.header {
margin-bottom: 80rpx;
text-align: center;
margin-bottom: 60rpx;
}
.main-title {
font-size: 56rpx;
font-size: 48rpx;
font-weight: 700;
color: #FFFFFF;
color: var(--text-primary);
line-height: 1.3;
margin-bottom: 24rpx;
letter-spacing: 0.5rpx;
text-shadow: 0 2rpx 8rpx rgba(64, 40, 128, 0.3);
margin-bottom: 16rpx;
letter-spacing: -0.5rpx;
}
.sub-title {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
color: var(--text-tertiary);
line-height: 1.5;
letter-spacing: 0.5rpx;
text-shadow: 0 1rpx 4rpx rgba(64, 40, 128, 0.2);
}
.form-section {
margin-bottom: 60rpx;
margin-bottom: 48rpx;
}
.input-wrap {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 32rpx 40rpx;
transition: all 0.3s;
box-shadow: 0 4rpx 16rpx rgba(64, 40, 128, 0.1);
gap: 16rpx;
background: var(--bg-secondary);
border: 2rpx solid var(--border-color);
border-radius: var(--radius-medium);
padding: 28rpx 32rpx;
transition: all 0.3s ease;
}
.input-wrap.focus {
background: rgba(255, 255, 255, 1);
box-shadow: 0 4rpx 20rpx rgba(64, 40, 128, 0.2);
background: var(--bg-primary);
border-color: var(--primary);
box-shadow: 0 0 0 4rpx rgba(10, 132, 255, 0.1);
}
.input {
flex: 1;
font-size: 32rpx;
color: #1A1A1A;
font-size: 30rpx;
color: var(--text-primary);
background: transparent;
}
.placeholder {
color: #C4C4C4;
color: var(--text-quaternary);
}
.clear-icon {
width: 32rpx;
height: 32rpx;
opacity: 0.4;
transition: opacity 0.3s ease;
cursor: pointer;
flex-shrink: 0;
}
.clear-icon:active {
opacity: 0.6;
}
.error {
font-size: 24rpx;
color: #EF4444;
color: var(--danger);
margin-top: 16rpx;
padding-left: 8rpx;
}
.btn-section {
margin-bottom: 60rpx;
margin-bottom: 48rpx;
}
.btn-login {
width: 100%;
height: 108rpx;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
border: 2rpx solid rgba(255, 255, 255, 0.4);
border-radius: 20rpx;
font-size: 36rpx;
height: 96rpx;
background: var(--primary);
border: none;
border-radius: var(--radius-medium);
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
color: var(--text-inverse);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(10, 132, 255, 0.2);
}
.btn-login.disabled {
background: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.7);
border: 2rpx solid rgba(255, 255, 255, 0.2);
background: var(--gray-300);
color: var(--text-quaternary);
box-shadow: none;
}
.btn-login:active:not(.disabled) {
opacity: 0.8;
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(10, 132, 255, 0.2);
}
.btn-scan {
width: 100%;
height: 88rpx;
background: var(--bg-primary);
border: 2rpx solid var(--primary);
border-radius: var(--radius-medium);
font-size: 30rpx;
font-weight: 500;
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 24rpx;
transition: all 0.3s ease;
}
.btn-scan:active {
background: rgba(10, 132, 255, 0.05);
}
.scan-icon {
width: 40rpx;
height: 40rpx;
}
.loading {
display: flex;
align-items: center;
gap: 8rpx;
gap: 12rpx;
}
.dot {
width: 10rpx;
height: 10rpx;
background: #FFFFFF;
width: 12rpx;
height: 12rpx;
background: var(--text-inverse);
border-radius: 50%;
animation: load 1.4s infinite ease-in-out both;
}
@@ -563,14 +437,13 @@
}
.text {
color: rgba(255, 255, 255, 0.8);
color: var(--text-tertiary);
white-space: nowrap;
}
.link {
color: #FFFFFF;
color: var(--primary);
font-weight: 500;
white-space: nowrap;
text-decoration: underline;
}
</style>
</style>

View File

@@ -1,24 +1,18 @@
<template>
<view class="container">
<view class="pd-20">
<view class="wallet-card">
<view class="balance-section">
<view class="balance-header">
<view class="balance-label">账户余额()</view>
<view class="recharge-btn-wrapper">
<button class="btn-recharge" @tap="showRechargeModal = true">充值</button>
</view>
<view class="wallet-header">
<view class="header-left">
<view class="balance-label">账户余额</view>
<view class="balance-amount">¥{{ formatMoney(walletDetail.balance) }}</view>
</view>
<view class="balance-amount">{{ formatMoney(walletDetail.balance) }}</view>
<button class="btn-recharge" @tap="showRechargeModal = true">充值</button>
</view>
<view class="wallet-footer">
<view class="footer-item">
<view class="footer-label">冻结金额</view>
<view class="footer-value">{{ formatMoney(walletDetail.frozen_balance) }}</view>
</view>
<view class="footer-divider"></view>
<view class="footer-item">
<view class="footer-label">更新时间</view>
<view class="footer-value">{{ formatDateTime(walletDetail.updated_at) }}</view>
<view class="wallet-info">
<view class="info-item">
<view class="info-label">冻结金额</view>
<view class="info-value">¥{{ formatMoney(walletDetail.frozen_balance) }}</view>
</view>
</view>
</view>
@@ -37,7 +31,7 @@
<swiper class="tab-swiper" :current="currentTab" @change="onSwiperChange" :circular="false"
:acceleration="true">
<swiper-item class="swiper-item-content">
<scroll-view scroll-y class="list-content">
<scroll-view scroll-y class="list-content" @scrolltolower="loadMoreRecharge">
<view v-if="rechargeList.length === 0 && !rechargeLoading" class="empty-state">
<view class="empty-icon">📋</view>
<view class="empty-title">暂无充值订单</view>
@@ -77,14 +71,13 @@
<view class="load-more" v-if="rechargeList.length > 0">
<text v-if="rechargeLoading">加载中...</text>
<text v-else-if="rechargeNoMore"> 没有更多了 </text>
<text v-else @tap="loadMoreRecharge">点击加载更多</text>
</view>
</view>
</scroll-view>
</swiper-item>
<swiper-item class="swiper-item-content">
<scroll-view scroll-y class="list-content">
<scroll-view scroll-y class="list-content" @scrolltolower="loadMoreTransaction">
<view v-if="transactionList.length === 0 && !transactionLoading" class="empty-state">
<view class="empty-icon">💰</view>
<view class="empty-title">暂无钱包流水</view>
@@ -122,7 +115,6 @@
<view class="load-more" v-if="transactionList.length > 0">
<text v-if="transactionLoading">加载中...</text>
<text v-else-if="transactionNoMore"> 没有更多了 </text>
<text v-else @tap="loadMoreTransaction">点击加载更多</text>
</view>
</view>
</scroll-view>
@@ -162,6 +154,7 @@
</up-popup>
</view>
</view>
</view>
</view>
</template>
@@ -177,6 +170,7 @@
import {
useUserStore
} from '@/store/index.js';
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
const userStore = useUserStore();
@@ -278,10 +272,13 @@
// 自定义金额
const customAmountNum = parseFloat(customAmount.value);
if (!customAmountNum || customAmountNum < 0.01) {
uni.showToast({
title: '请输入正确的充值金额',
icon: 'none'
});
showRechargeModal.value = false;
setTimeout(() => {
uni.showToast({
title: '请输入正确的充值金额',
icon: 'none'
});
}, 50);
return;
}
amount = Math.round(customAmountNum * 100); // 转换为分
@@ -289,10 +286,13 @@
// 预设金额
amount = selectedAmount.value;
} else {
uni.showToast({
title: '请选择充值金额',
icon: 'none'
});
showRechargeModal.value = false;
setTimeout(() => {
uni.showToast({
title: '请选择充值金额',
icon: 'none'
});
}, 50);
return;
}
@@ -319,11 +319,14 @@
// 检查金额范围
if (amount < checkData.min_amount || amount > checkData.max_amount) {
uni.hideLoading();
uni.showToast({
title: `充值金额范围:¥${(checkData.min_amount / 100).toFixed(2)} - ¥${(checkData.max_amount / 100).toFixed(2)}`,
icon: 'none',
duration: 2500
});
showRechargeModal.value = false;
setTimeout(() => {
uni.showToast({
title: `充值金额范围:¥${(checkData.min_amount / 100).toFixed(2)} - ¥${(checkData.max_amount / 100).toFixed(2)}`,
icon: 'none',
duration: 2500
});
}, 50);
return;
}
@@ -339,103 +342,42 @@
// 验证支付配置
if (!rechargeData.pay_config || !rechargeData.pay_config.package) {
uni.showToast({
title: '支付参数获取失败',
icon: 'none'
});
setTimeout(() => {
uni.showToast({
title: '支付参数获取失败',
icon: 'none'
});
}, 50);
return;
}
// 第三步:调起微信支付
// #ifdef H5
if (typeof WeixinJSBridge === 'undefined') {
uni.showToast({
title: '请在微信中打开',
icon: 'none'
});
return;
try {
await wechatH5Pay(rechargeData.pay_config);
// 支付成功
showPaymentToast(true, '充值成功');
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
} catch (error) {
// 支付失败或取消
handlePaymentError(error);
}
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: rechargeData.pay_config.app_id,
timeStamp: rechargeData.pay_config.timestamp,
nonceStr: rechargeData.pay_config.nonce_str,
package: rechargeData.pay_config.package,
signType: rechargeData.pay_config.sign_type,
paySign: rechargeData.pay_config.pay_sign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
console.log('支付成功', res);
uni.showToast({
title: '充值成功',
icon: 'success'
});
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
console.error('支付失败', res);
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
});
// #endif
// #ifndef H5
uni.requestPayment({
timeStamp: rechargeData.pay_config.timestamp,
nonceStr: rechargeData.pay_config.nonce_str,
package: rechargeData.pay_config.package,
signType: rechargeData.pay_config.sign_type,
paySign: rechargeData.pay_config.pay_sign,
success: (res) => {
console.log('支付成功', res);
uni.showToast({
title: '充值成功',
icon: 'success'
});
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
},
fail: (err) => {
console.error('支付失败', err);
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
}
});
// #endif
} catch (e) {
uni.hideLoading();
console.error('充值失败', e);
const errorMsg = e.msg || e.message || '充值失败,请稍后重试';
uni.showToast({
title: errorMsg,
icon: 'none'
});
showRechargeModal.value = false;
setTimeout(() => {
uni.showToast({
title: errorMsg,
icon: 'none'
});
}, 50);
}
};
@@ -563,83 +505,21 @@
}
// 调起微信支付
// #ifdef H5
if (typeof WeixinJSBridge === 'undefined') {
uni.showToast({
title: '请在微信中打开',
icon: 'none'
});
return;
try {
await wechatH5Pay(rechargeData.pay_config);
// 支付成功
showPaymentToast(true, '充值成功');
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
} catch (error) {
// 支付失败或取消
handlePaymentError(error);
}
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: rechargeData.pay_config.app_id,
timeStamp: rechargeData.pay_config.timestamp,
nonceStr: rechargeData.pay_config.nonce_str,
package: rechargeData.pay_config.package,
signType: rechargeData.pay_config.sign_type,
paySign: rechargeData.pay_config.pay_sign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
uni.showToast({
title: '充值成功',
icon: 'success'
});
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
});
// #endif
// #ifndef H5
uni.requestPayment({
timeStamp: rechargeData.pay_config.timestamp,
nonceStr: rechargeData.pay_config.nonce_str,
package: rechargeData.pay_config.package,
signType: rechargeData.pay_config.sign_type,
paySign: rechargeData.pay_config.pay_sign,
success: (res) => {
uni.showToast({
title: '充值成功',
icon: 'success'
});
setTimeout(() => {
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
},
fail: (err) => {
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
}
});
// #endif
} catch (e) {
uni.hideLoading();
console.error('获取支付配置失败', e);
@@ -714,84 +594,86 @@
<style lang="scss" scoped>
.container {
height: 100vh;
min-height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 20rpx;
box-sizing: border-box;
max-width: 750rpx;
margin: 0 auto;
padding: 0 !important;
width: 100%;
}
.wallet-card {
background: linear-gradient(135deg, #0A84FF 0%, #5AC8FA 50%, #64D2FF 100%);
border-radius: 24rpx;
padding: 36rpx;
background: var(--primary);
border-radius: var(--radius-large);
padding: 40rpx;
color: #fff;
box-shadow: 0 8rpx 32rpx rgba(10, 132, 255, 0.3);
.wallet-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 32rpx;
.balance-section {
margin-bottom: 28rpx;
.balance-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.header-left {
flex: 1;
.balance-label {
font-size: 24rpx;
opacity: 0.75;
font-size: 26rpx;
opacity: 0.85;
margin-bottom: 12rpx;
font-weight: 500;
}
.recharge-btn-wrapper {
.btn-recharge {
background: #fff;
border: none;
color: var(--primary);
font-size: 26rpx;
padding: 12rpx 32rpx;
border-radius: 40rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
&::after {
border: none;
}
}
.balance-amount {
font-size: 64rpx;
font-weight: 700;
letter-spacing: -2rpx;
line-height: 1;
}
}
.balance-amount {
font-size: 72rpx;
font-weight: 700;
letter-spacing: -2rpx;
.btn-recharge {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 2rpx solid rgba(255, 255, 255, 0.3);
color: #fff;
font-size: 28rpx;
padding: 0rpx 32rpx;
border-radius: 50rpx;
font-weight: 600;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&::after {
border: none;
}
&:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.95);
}
}
}
.wallet-footer {
display: flex;
align-items: center;
.wallet-info {
padding-top: 24rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
.footer-divider {
width: 1rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.3);
margin: 0 32rpx;
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
.footer-item {
flex: 1;
.footer-label {
font-size: 22rpx;
opacity: 0.7;
margin-bottom: 6rpx;
.info-label {
font-size: 24rpx;
opacity: 0.8;
font-weight: 500;
}
.footer-value {
font-size: 26rpx;
.info-value {
font-size: 28rpx;
font-weight: 600;
}
}
@@ -846,17 +728,17 @@
}
.tab-swiper {
flex: 1;
margin-top: 20rpx;
min-height: 0;
height: calc(100vh - 500rpx);
}
.swiper-item-content {
height: 100%;
height: calc(100vh - 500rpx);
}
.list-content {
height: 100%;
height: calc(100vh - 500rpx);
padding-top: 16rpx;
box-sizing: border-box;
}

View File

@@ -8,14 +8,16 @@
</view>
</view>
<view v-if="orderList.length === 0 && !loading" class="empty-state">
<view class="empty-icon">📋</view>
<view class="empty-title">暂无订单</view>
<view class="empty-desc">当前账号下暂无订单信息</view>
</view>
<scroll-view scroll-y class="scroll-container" @scrolltolower="loadMore">
<scroll-view v-else scroll-y class="order-scroll" @scrolltolower="loadMore">
<view class="order-card" v-for="(item, index) in orderList" :key="index">
<view v-if="orderList.length === 0 && !loading" class="empty-state">
<view class="empty-icon">📋</view>
<view class="empty-title">暂无订单</view>
<view class="empty-desc">当前账号下暂无订单信息</view>
</view>
<view v-else class="order-list">
<view class="order-card" v-for="(item, index) in orderList" :key="index">
<view class="card-header">
<view class="header-left">
<view class="header-row">
@@ -31,16 +33,24 @@
<view class="card-body">
<view class="info-grid">
<view class="info-item">
<view class="info-label">订单时间</view>
<view class="info-value">{{ item.created_at }}</view>
<view class="info-label">套餐名称</view>
<view class="info-value">
<view class="package-item" v-for="(pkgName, pIndex) in item.package_names" :key="pIndex">
<text class="package-name">{{ pkgName }}</text>
</view>
</view>
</view>
<view class="info-item">
<view class="info-label">订单金额</view>
<view class="info-value amount">¥{{ formatMoney(item.total_amount) }}</view>
</view>
<view class="info-item">
<view class="info-label">下单时间</view>
<view class="info-value">{{ item.created_at }}</view>
</view>
</view>
<view class="divider"></view>
<!-- <view class="divider"></view>
<view class="package-section">
<view class="section-label">套餐名称</view>
@@ -49,7 +59,7 @@
<text class="package-name">{{ pkgName }}</text>
</view>
</view>
</view>
</view> -->
<view v-if="item.payment_status === 1" class="card-footer">
<button class="btn-pay" @tap="handleOrderPayment(item)">立即支付</button>
@@ -57,10 +67,11 @@
</view>
</view>
<view class="load-more" v-if="orderList.length > 0">
<text v-if="loading">加载中...</text>
<text v-else-if="noMore">没有更多了</text>
<text v-else>上拉加载更多</text>
<view class="load-more" v-if="orderList.length > 0">
<text v-if="loading">加载中...</text>
<text v-else-if="noMore">没有更多了</text>
<text v-else>上拉加载更多</text>
</view>
</view>
</scroll-view>
</view>
@@ -78,6 +89,7 @@
import {
useUserStore
} from '@/store/index.js';
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
const userStore = useUserStore();
@@ -195,81 +207,21 @@
return;
}
// #ifdef H5
if (typeof WeixinJSBridge === 'undefined') {
uni.showToast({
title: '请在微信中打开',
icon: 'none'
});
return;
// 调起微信支付
try {
await wechatH5Pay(payData.pay_config);
// 支付成功
showPaymentToast(true, '支付成功');
setTimeout(() => {
page.value = 1;
noMore.value = false;
loadOrderList();
}, 1500);
} catch (error) {
// 支付失败或取消
handlePaymentError(error);
}
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: payData.pay_config.app_id,
timeStamp: payData.pay_config.timestamp,
nonceStr: payData.pay_config.nonce_str,
package: payData.pay_config.package,
signType: payData.pay_config.sign_type,
paySign: payData.pay_config.pay_sign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
uni.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
page.value = 1;
noMore.value = false;
loadOrderList();
}, 1500);
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
});
// #endif
// #ifndef H5
uni.requestPayment({
timeStamp: payData.pay_config.timestamp,
nonceStr: payData.pay_config.nonce_str,
package: payData.pay_config.package,
signType: payData.pay_config.sign_type,
paySign: payData.pay_config.pay_sign,
success: () => {
uni.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
page.value = 1;
noMore.value = false;
loadOrderList();
}, 1500);
},
fail: (err) => {
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({
title: '已取消支付',
icon: 'none'
});
} else {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
}
});
// #endif
} catch (e) {
uni.hideLoading();
console.error('支付失败', e);
@@ -288,19 +240,29 @@
<style lang="scss" scoped>
.container {
height: 100vh;
position: fixed;
top: var(--window-top);
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
background: #f5f6f7;
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
padding-top: 0;
}
.filter-tabs {
display: flex;
background: #fff;
position: sticky;
top: 0;
border-radius: 10rpx;
z-index: 10;
border-radius: 20rpx;
z-index: 100;
flex-shrink: 0;
margin-top: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
.tab-item {
position: relative;
@@ -330,8 +292,9 @@
}
}
.order-scroll {
.scroll-container {
flex: 1;
height: 100%;
}
.empty-state {

View File

@@ -81,6 +81,7 @@
import { ref, reactive, onMounted } from 'vue';
import { assetApi, orderApi, walletApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
const userStore = useUserStore();
@@ -276,63 +277,18 @@
};
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
// 验证参数
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
uni.showToast({ title: '支付参数无效', icon: 'none' });
return;
try {
await wechatH5Pay(payConfig);
// 支付成功
const successMsg = isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功';
showPaymentToast(true, successMsg);
setTimeout(() => {
loadWalletBalance();
}, 1500);
} catch (error) {
// 支付失败或取消
handlePaymentError(error);
}
// #ifdef H5
if (typeof WeixinJSBridge === 'undefined') {
uni.showToast({ title: '请在微信中打开', icon: 'none' });
return;
}
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: payConfig.app_id,
timeStamp: payConfig.timestamp,
nonceStr: payConfig.nonce_str,
package: payConfig.package,
signType: payConfig.sign_type,
paySign: payConfig.pay_sign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
const successMsg = isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功';
uni.showToast({ title: successMsg, icon: 'success', duration: 2000 });
setTimeout(() => {
loadWalletBalance();
}, 1500);
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
uni.showToast({ title: '已取消支付', icon: 'none' });
} else {
uni.showToast({ title: '支付失败', icon: 'none' });
}
});
// #endif
// #ifndef H5
uni.requestPayment({
timeStamp: payConfig.timestamp,
nonceStr: payConfig.nonce_str,
package: payConfig.package,
signType: payConfig.sign_type,
paySign: payConfig.pay_sign,
success: (res) => {
const successMsg = isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功';
uni.showToast({ title: successMsg, icon: 'success', duration: 2000 });
setTimeout(() => {
loadWalletBalance();
}, 1500);
},
fail: (err) => {
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({ title: '已取消支付', icon: 'none' });
} else {
uni.showToast({ title: '支付失败', icon: 'none' });
}
}
});
// #endif
};
onMounted(() => {

BIN
static/clear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
static/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
static/scan-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -36,7 +36,7 @@ export const useUserStore = () => {
};
const setUserInfo = (userInfo) => {
state.userInfo = userInfo;
Object.assign(state.userInfo, userInfo);
uni.setStorageSync('userInfo', userInfo);
};

View File

@@ -3,5 +3,3 @@ export const BASE_URL = 'https://cmp-api.boss160.cn';
export const APP_TYPE = 'official_account';
export const APP_ID = 'wx404dc822ce15c5f0';
export const USE_WECHAT_AUTH = true;

96
utils/payment.js Normal file
View File

@@ -0,0 +1,96 @@
/**
* 微信H5支付工具
* 统一处理微信JSAPI支付逻辑
*/
/**
* 调起微信H5支付
* @param {Object} payConfig - 支付配置对象
* @param {string} payConfig.app_id - 微信公众号appid
* @param {string} payConfig.timestamp - 时间戳
* @param {string} payConfig.nonce_str - 随机字符串
* @param {string} payConfig.package - 预支付交易会话标识
* @param {string} payConfig.sign_type - 签名方式
* @param {string} payConfig.pay_sign - 签名
* @returns {Promise} 支付结果
*/
export function wechatH5Pay(payConfig) {
return new Promise((resolve, reject) => {
// 验证支付配置
if (!payConfig || !payConfig.package) {
reject(new Error('支付参数无效'));
return;
}
// 验证 package 格式
if (!payConfig.package.startsWith('prepay_id=')) {
reject(new Error('支付参数无效'));
return;
}
// 检查是否在微信环境中
if (typeof WeixinJSBridge === 'undefined') {
reject(new Error('请在微信中打开'));
return;
}
// 调起微信支付
WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: payConfig.app_id,
timeStamp: payConfig.timestamp,
nonceStr: payConfig.nonce_str,
package: payConfig.package,
signType: payConfig.sign_type,
paySign: payConfig.pay_sign
}, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
// 支付成功
resolve({
success: true,
message: '支付成功'
});
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
// 用户取消支付
reject({
cancelled: true,
message: '已取消支付'
});
} else {
// 支付失败
reject({
success: false,
message: '支付失败',
error: res
});
}
});
});
}
/**
* 显示支付结果提示
* @param {boolean} success - 是否成功
* @param {string} message - 提示消息
*/
export function showPaymentToast(success, message) {
uni.showToast({
title: message || (success ? '支付成功' : '支付失败'),
icon: success ? 'success' : 'none',
duration: 2000
});
}
/**
* 处理支付错误
* @param {Error|Object} error - 错误对象
*/
export function handlePaymentError(error) {
if (error.cancelled) {
// 用户主动取消,提示取消
showPaymentToast(false, '已取消支付');
} else {
// 其他错误
const message = error.message || '支付失败';
showPaymentToast(false, message);
}
}

View File

@@ -34,11 +34,6 @@ const request = (options) => {
if (code === 0 || code === 200) {
resolve(data);
} else {
uni.showToast({
title: msg || '请求失败',
icon: 'none',
duration: 2000
});
reject(res.data);
}
},

66
utils/wxsdk.js Normal file
View File

@@ -0,0 +1,66 @@
// utils/wxsdk.js —— 封装微信扫一扫 不用旧的了
import { wechatApi } from '@/api/index.js'
const wx = window.jWeixin
/**
* 初始化微信 JS-SDK
*/
export async function initWxConfig() {
const url = window.location.href.split('#')[0]
console.log(url);
try {
const data = await wechatApi.getJssdkConfig(url)
const { app_id, timestamp, nonce_str, signature } = data
return new Promise((resolve, reject) => {
wx.config({
debug: false,
appId: app_id,
timestamp: timestamp,
nonceStr: nonce_str,
signature: signature,
jsApiList: ['scanQRCode']
})
wx.ready(() => {
console.log('微信 SDK 就绪')
resolve()
})
wx.error((err) => {
console.error('wx.config 失败', err)
reject(err)
})
})
} catch (e) {
console.error('获取微信签名失败', e)
throw e
}
}
/**
* 调起微信扫一扫
* @returns {Promise<string>} 扫码结果字符串
*/
export function wxScan() {
return new Promise((resolve, reject) => {
wx.scanQRCode({
needResult: 1,
scanType: ['qrCode', 'barCode'],
success(res) {
console.log('扫码成功:', res)
resolve(res.resultStr)
},
fail(err) {
console.error('扫码失败:', err)
reject(err)
}
})
})
}
/**
* 检查是否在微信环境中
*/
export function isInWechat() {
return /micromessenger/i.test(navigator.userAgent)
}

View File

@@ -3,4 +3,12 @@ import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
plugins: [uni()],
})
root: process.cwd(),
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ['legacy-js-api', 'import']
}
}
}
})