Compare commits

..

82 Commits

Author SHA1 Message Date
74d83468e3 更新 Dockerfile
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 48s
2026-04-30 10:01:27 +08:00
sexygoat
29928f121a feat: 新增txt
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 45s
2026-04-29 18:40:21 +08:00
sexygoat
e7b9c75ad0 feat: 新增txt
Some checks failed
构建并部署前端到生产环境 / build-and-deploy (push) Failing after 4s
2026-04-29 18:38:09 +08:00
sexygoat
cebe545440 msg
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 48s
2026-04-29 17:48:54 +08:00
sexygoat
d679cad55f fix: singer
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 51s
2026-04-29 17:09:39 +08:00
sexygoat
5f0c215bdc fix: status
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 52s
2026-04-29 16:26:37 +08:00
sexygoat
35424347f3 fix: h5
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 50s
2026-04-28 11:22:03 +08:00
sexygoat
e121fca8ca fix:流量
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 52s
2026-04-27 18:30:26 +08:00
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
39 changed files with 1752 additions and 6795 deletions

View File

@@ -2,13 +2,7 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git add:*)", "Bash(git add:*)",
"Read(//Users/break/.agents/skills/impeccable/**)", "Bash(npm list:*)"
"Bash(node .agents/skills/impeccable/scripts/cleanup-deprecated.mjs)",
"Bash(node *)",
"Bash(npm run *)",
"Bash(curl -s http://localhost:5173)",
"Bash(git checkout *)",
"Bash(git commit -m ' *)"
], ],
"deny": [], "deny": [],
"ask": [] "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

View File

@@ -1,30 +0,0 @@
# Impeccable Design Context
## Design Context
### Users
- **Who**: Regular consumers on mobile who purchased a portable WiFi/MiFi device
- **Context**: Using the app on their phone to check data usage, manage their device settings, top up balance, and ensure connectivity while traveling
- **Job to be done**: Quick access to device status, data usage, and account management with minimal friction
### Brand Personality
- **Voice**: Clean, confident, trustworthy
- **Tone**: Fast and efficient, modern and sleek, friendly and helpful, reliable and secure
- **3-word personality**: Modern · Trustworthy · Effortless
- **Emotional goals**: Users should feel in control of their connectivity, confident their device is working, and that managing it is simple
### Aesthetic Direction
- **Visual tone**: Minimal and refined — clean layouts with purposeful use of space
- **References**: Consumer electronics (Apple, Tile, Anker) + Minimalist app interfaces
- **Anti-references**: Cluttered admin dashboards, AI slop aesthetics, legacy carrier apps, overly playful designs
- **Theme**: Light mode only — bright, clean, accessible
- **Colors**: Blue-based palette (iOS-like primary blue #0A84FF) with the existing color system as a base
### Design Principles
1. **Clarity first**: Every element earns its place. No decorative clutter.
2. **Confidence through space**: Generous whitespace creates premium feel. Tight grouping shows related content.
3. **Light and bright**: Light theme only. White surfaces, not gray. Blue accent for primary actions.
4. **Progressive disclosure**: Simple by default, reveal complexity on demand (e.g., WiFi settings behind a tap).
5. **Fast interactions**: Optimistic UI. Updates feel instant. Loading states are minimal.
6. **Typography hierarchy**: Bold headings, readable body. Clear contrast between levels.
7. **Trust signals**: Online/offline status, battery level, data usage — at a glance, always visible.

598
App.vue
View File

@@ -5,89 +5,86 @@
</template> </template>
<style lang="scss"> <style lang="scss">
@import "uview-plus/index.scss";
:root { :root {
/* 主色 - iOS Blue 家族 */ /* 主色 */
--primary: #007AFF; --primary: #0A84FF;
--primary-light: #5AC8FA; --primary-light: #5AC8FA;
--primary-dark: #0056CC; --primary-dark: #0066CC;
--primary-tint: rgba(0, 122, 255, 0.08);
/* 语义色 */ /* 辅助色 */
--success: #34C759; --success: #30D158;
--success-light: rgba(52, 199, 89, 0.1); --success-light: rgba(48, 209, 88, 0.12);
--warning: #FF9500; --warning: #FF9F0A;
--warning-light: rgba(255, 149, 0, 0.1); --warning-light: rgba(255, 159, 10, 0.12);
--danger: #FF3B30; --danger: #FF453A;
--danger-light: rgba(255, 59, 48, 0.1); --danger-light: rgba(255, 69, 58, 0.12);
--info: #5856D6; --info: #5E5CE6;
/* 中性色 - 微妙的蓝色底色 */ /* 扩展色 */
--gray-50: #F8F9FC; --purple: #BF5AF2;
--gray-100: #EEF1F5; --pink: #FF375F;
--gray-200: #D9DDE6; --teal: #64D2FF;
--gray-300: #B8C0D0; --indigo: #5E5CE6;
--gray-400: #8E95A8;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* 文字色 */ /* 中性色 - 灰阶 */
--text-primary: var(--gray-900); --gray-100: #F2F2F7;
--text-secondary: var(--gray-600); --gray-200: #E5E5EA;
--text-tertiary: var(--gray-400); --gray-300: #D1D1D6;
--text-inverse: #FFFFFF; --gray-400: #C7C7CC;
--gray-500: #AEAEB2;
--gray-600: #8E8E93;
--gray-700: #636366;
--gray-800: #3C3C43;
--gray-900: #000000;
/* 背景色 */ /* 背景色 */
--bg-base: #F5F7FA; --bg-primary: #FFFFFF;
--bg-surface: #FFFFFF; --bg-secondary: #F8F8F8;
--bg-elevated: #FFFFFF; --bg-tertiary: #FFFFFF;
--bg-grouped: #F2F2F7;
/* 阴影 - 更细腻 */ /* 文字色 */
--shadow-xs: 0 1rpx 2rpx rgba(0, 0, 0, 0.04); --text-primary: #000000;
--shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); --text-secondary: #3C3C43;
--shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); --text-tertiary: rgba(60, 60, 67, 0.6);
--shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.10); --text-quaternary: rgba(60, 60, 67, 0.3);
--shadow-xl: 0 16rpx 48rpx rgba(0, 0, 0, 0.12); --text-inverse: #FFFFFF;
/* 阴影 */
--shadow-small: 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
--shadow-large: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
--shadow-extra-large: 0 16rpx 48rpx rgba(0, 0, 0, 0.16);
/* 圆角 */ /* 圆角 */
--radius-xs: 6rpx; --radius-small: 8rpx;
--radius-sm: 10rpx; --radius-medium: 16rpx;
--radius-md: 16rpx; --radius-large: 24rpx;
--radius-lg: 24rpx; --radius-extra-large: 32rpx;
--radius-xl: 32rpx;
--radius-full: 9999rpx; --radius-full: 9999rpx;
/* 间距 - 4pt 基准 */ /* 间距 */
--space-1: 4rpx; --space-xs: 8rpx;
--space-2: 8rpx; --space-sm: 16rpx;
--space-3: 12rpx; --space-md: 24rpx;
--space-4: 16rpx; --space-lg: 32rpx;
--space-5: 20rpx; --space-xl: 40rpx;
--space-6: 24rpx; --space-2xl: 48rpx;
--space-8: 32rpx;
--space-10: 40rpx;
--space-12: 48rpx;
--space-16: 64rpx;
/* 过渡 */ /* 过渡 */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --transition-fast: 0.2s ease-out;
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --transition-normal: 0.3s ease-out;
--duration-fast: 0.15s; --transition-slow: 0.5s ease-out;
--duration-normal: 0.25s;
--duration-slow: 0.4s; /* 边框色 */
--border-color: var(--gray-200);
--border-light: var(--gray-100);
} }
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "MiSans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family: "MiSans", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
ul, li { ul, li {
@@ -96,31 +93,36 @@
page { page {
color: var(--text-primary); color: var(--text-primary);
background-color: var(--bg-base); background: var(--bg-secondary);
min-height: 100vh; // min-height: 100vh;
} }
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-4); gap: 20rpx;
padding: var(--space-5) var(--space-4); padding: 30rpx 20rpx;
max-width: 750rpx; max-width: 750rpx;
margin: 0 auto; margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.pd-20{
padding: 20rpx;
} }
.card { .card {
background: var(--bg-surface); background: var(--bg-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-medium);
padding: var(--space-5); padding: var(--space-md);
box-shadow: var(--shadow-sm); border: 1rpx solid var(--border-color);
transition: box-shadow var(--duration-normal) var(--ease-out), transition: all var(--transition-fast);
transform var(--duration-normal) var(--ease-out);
} }
.card-elevated { .card-elevated {
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-large);
transform: translateY(-2px); transform: translateY(-4rpx);
} }
.card-interactive { .card-interactive {
@@ -129,343 +131,155 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
/* 排版系统 */ .title {
.text-display {
font-size: 48rpx;
font-weight: 700; font-weight: 700;
line-height: 1.1; font-size: 34rpx;
letter-spacing: -0.03em;
color: var(--text-primary);
}
.text-title {
font-size: 36rpx;
font-weight: 600;
line-height: 1.2; line-height: 1.2;
color: var(--text-primary);
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: var(--text-primary);
} }
.text-heading { .subtitle {
font-size: 30rpx;
font-weight: 600; font-weight: 600;
line-height: 1.25;
letter-spacing: -0.01em;
color: var(--text-primary);
}
.text-body {
font-size: 28rpx; font-size: 28rpx;
font-weight: 400; line-height: 1.3;
line-height: 1.5;
color: var(--text-secondary); color: var(--text-secondary);
} }
.text-caption { .body {
font-size: 24rpx; font-weight: 400;
font-weight: 500;
line-height: 1.4;
color: var(--text-tertiary);
}
.text-label {
font-size: 22rpx;
font-weight: 600;
line-height: 1.3;
letter-spacing: 0.02em;
color: var(--text-tertiary);
text-transform: uppercase;
}
/* 间距 */
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mt-8 { margin-top: var(--space-8); }
.mt-10 { margin-top: var(--space-10); }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-5 { margin-bottom: var(--space-5); }
.mb-6 { margin-bottom: var(--space-6); }
.mb-8 { margin-bottom: var(--space-8); }
.p-4 { padding: var(--space-4); }
.p-5 { padding: var(--space-5); }
.p-6 { padding: var(--space-6); }
/* Flex */
.flex-row { display: flex; align-items: center; }
.flex-row-g2 { display: flex; align-items: center; gap: var(--space-2); }
.flex-row-g3 { display: flex; align-items: center; gap: var(--space-3); }
.flex-row-g4 { display: flex; align-items: center; gap: var(--space-4); }
.flex-row-g5 { display: flex; align-items: center; gap: var(--space-5); }
.flex-row-g6 { display: flex; align-items: center; gap: var(--space-6); }
.flex-row-sb { display: flex; align-items: center; justify-content: space-between; }
.flex-row-center { display: flex; align-items: center; justify-content: center; }
.flex-row-end { display: flex; align-items: center; justify-content: flex-end; }
.flex-col { display: flex; flex-direction: column; }
.flex-col-g2 { display: flex; flex-direction: column; gap: var(--space-2); }
.flex-col-g3 { display: flex; flex-direction: column; gap: var(--space-3); }
.flex-col-g4 { display: flex; flex-direction: column; gap: var(--space-4); }
.flex-col-g5 { display: flex; flex-direction: column; gap: var(--space-5); }
.flex-col-g6 { display: flex; flex-direction: column; gap: var(--space-6); }
.flex-col-center { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.w-full { width: 100%; }
.h-full { height: 100%; }
/* 交互状态 */
.interactive {
transition: all var(--duration-normal) var(--ease-out);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
/* 标签 */
.tag {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
font-size: 22rpx;
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.01em;
&.tag-default {
background: var(--gray-100);
color: var(--gray-600);
}
&.tag-success {
background: var(--success-light);
color: var(--success);
}
&.tag-primary {
background: var(--primary-tint);
color: var(--primary);
}
&.tag-warning {
background: var(--warning-light);
color: var(--warning);
}
&.tag-danger {
background: var(--danger-light);
color: var(--danger);
}
}
/* 进度条 */
.progress {
width: 100%;
height: 10rpx;
background: var(--gray-100);
border-radius: var(--radius-full);
overflow: hidden;
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: var(--radius-full);
transition: width var(--duration-slow) var(--ease-out);
}
}
/* 按钮 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: var(--radius-md);
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: all var(--duration-fast) var(--ease-out);
user-select: none;
-webkit-tap-highlight-color: transparent;
padding: var(--space-4) var(--space-5);
font-size: 28rpx; font-size: 28rpx;
background: var(--gray-100); line-height: 1.4;
color: var(--text-primary); color: var(--text-secondary);
&.btn-primary {
background: var(--primary);
color: var(--text-inverse);
}
&.btn-success {
background: var(--success);
color: var(--text-inverse);
}
&.btn-warning {
background: var(--warning);
color: var(--text-inverse);
}
&.btn-danger {
background: var(--danger);
color: var(--text-inverse);
}
&.btn-ghost {
background: transparent;
color: var(--primary);
}
&.btn-mini {
min-height: 52rpx;
padding: 0 var(--space-4);
font-size: 24rpx;
border-radius: var(--radius-sm);
}
&.btn-large {
min-height: 88rpx;
padding: 0 var(--space-8);
font-size: 32rpx;
border-radius: var(--radius-lg);
}
&.btn-disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
} }
.btn-group { .caption {
display: flex; font-weight: 500;
gap: var(--space-3); font-size: 24rpx;
line-height: 1.3;
.btn { color: var(--text-tertiary);
flex: 1;
}
&.btn-group-vertical {
flex-direction: column;
}
} }
/* 兼容旧类名 */ .mt-xs { margin-top: var(--space-xs); }
.mt-xs { margin-top: var(--space-2); } .mt-sm { margin-top: var(--space-sm); }
.mt-sm { margin-top: var(--space-4); } .mt-md { margin-top: var(--space-md); }
.mt-md { margin-top: var(--space-6); } .mt-lg { margin-top: var(--space-lg); }
.mt-lg { margin-top: var(--space-8); } .mt-xl { margin-top: var(--space-xl); }
.mt-xl { margin-top: var(--space-10); }
.mt-30 { margin-top: 30rpx; } .mt-30 { margin-top: 30rpx; }
.mt-20 { margin-top: 20rpx; } .mt-20 { margin-top: 20rpx; }
.mb-xs { margin-bottom: var(--space-2); } .mb-xs { margin-bottom: var(--space-xs); }
.mb-sm { margin-bottom: var(--space-4); } .mb-sm { margin-bottom: var(--space-sm); }
.mb-md { margin-bottom: var(--space-6); } .mb-md { margin-bottom: var(--space-md); }
.mb-lg { margin-bottom: var(--space-8); } .mb-lg { margin-bottom: var(--space-lg); }
.mb-xl { margin-bottom: var(--space-10); } .mb-xl { margin-bottom: var(--space-xl); }
.p-xs { padding: var(--space-2); } .p-xs { padding: var(--space-xs); }
.p-sm { padding: var(--space-4); } .p-sm { padding: var(--space-sm); }
.p-md { padding: var(--space-6); } .p-md { padding: var(--space-md); }
.p-lg { padding: var(--space-8); } .p-lg { padding: var(--space-lg); }
.p-xl { padding: var(--space-10); } .p-xl { padding: var(--space-xl); }
.flex-row-g8 { display: flex; align-items: center; gap: var(--space-2); } .flex-row { display: flex; align-items: center; }
.flex-row-g16 { display: flex; align-items: center; gap: var(--space-4); } .flex-row-g8 { display: flex; align-items: center; gap: var(--space-xs); }
.flex-row-g16 { display: flex; align-items: center; gap: var(--space-sm); }
.flex-row-g20 { display: flex; align-items: center; gap: 20rpx; } .flex-row-g20 { display: flex; align-items: center; gap: 20rpx; }
.flex-row-g24 { display: flex; align-items: center; gap: var(--space-6); } .flex-row-g24 { display: flex; align-items: center; gap: var(--space-md); }
.flex-row-sb { display: flex; align-items: center; justify-content: space-between; }
.flex-row-center { display: flex; align-items: center; justify-content: center; }
.flex-col-g8 { display: flex; flex-direction: column; gap: var(--space-2); } .flex-col { display: flex; flex-direction: column; }
.flex-col-g16 { display: flex; flex-direction: column; gap: var(--space-4); } .flex-col-g8 { display: flex; flex-direction: column; gap: var(--space-xs); }
.flex-col-g16 { display: flex; flex-direction: column; gap: var(--space-sm); }
.flex-col-g20 { display: flex; flex-direction: column; gap: 20rpx; } .flex-col-g20 { display: flex; flex-direction: column; gap: 20rpx; }
.flex-col-g24 { display: flex; flex-direction: column; gap: var(--space-6); } .flex-col-g24 { display: flex; flex-direction: column; gap: var(--space-md); }
.flex-col-center-g20 { display: flex; flex-direction: column; gap: 20rpx; align-items: center; justify-content: center; }
.flex-col-center { display: flex; flex-direction: column; align-items: center; justify-content: center; }
/* 背景色 */ .w-100 { width: 100%; }
.bg-primary { background-color: var(--bg-surface); } .h-100 { height: 100%; }
.bg-secondary { background-color: var(--bg-base); }
.bg-success { background-color: var(--success); }
.bg-warning { background-color: var(--warning); }
.bg-danger { background-color: var(--danger); }
/* 文字色 */ .interactive {
.text-primary { color: var(--text-primary); } transition: all var(--transition-normal);
.text-secondary { color: var(--text-secondary); } cursor: pointer;
.text-tertiary { color: var(--text-tertiary); } -webkit-tap-highlight-color: transparent;
.text-inverse { color: var(--text-inverse); }
.text-success { color: var(--success); }
.text-warning { color: var(--warning); }
.text-danger { color: var(--danger); }
/* 旧标签兼容 */
.tag-apple {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
background: var(--gray-100);
color: var(--gray-600);
border-radius: var(--radius-full);
font-size: 22rpx;
font-weight: 600;
letter-spacing: 0.01em;
&.tag-success {
background: var(--success-light);
color: var(--success);
}
&.tag-primary {
background: var(--primary-tint);
color: var(--primary);
}
&.tag-warning {
background: var(--warning-light);
color: var(--warning);
}
&.tag-danger {
background: var(--danger-light);
color: var(--danger);
}
} }
/* 旧按钮兼容 */
.button-apple { .button-apple {
background: var(--primary); background: var(--primary);
color: var(--text-inverse); color: var(--text-inverse);
border: none; border: none;
border-radius: var(--radius-md); border-radius: var(--radius-medium);
padding: var(--space-4) var(--space-5); padding: var(--space-sm) var(--space-md);
font-weight: 600; font-weight: 600;
font-size: 28rpx; font-size: 28rpx;
transition: all var(--duration-normal) var(--ease-out); transition: all var(--transition-normal);
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.tag-apple {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
background: var(--gray-100);
color: var(--text-secondary);
border-radius: var(--radius-small);
font-size: 22rpx;
font-weight: 600;
letter-spacing: 0.02em;
&.tag-success {
background: var(--success-light);
color: var(--success);
}
&.tag-primary {
background: rgba(10, 132, 255, 0.12);
color: var(--primary);
}
&.tag-warning {
background: var(--warning-light);
color: var(--warning);
}
&.tag-danger {
background: var(--danger-light);
color: var(--danger);
}
}
.progress-apple {
width: 100%;
height: 8rpx;
background: var(--gray-200);
border-radius: var(--radius-small);
overflow: hidden;
position: relative;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
border-radius: var(--radius-small);
transition: width var(--transition-normal);
}
}
.btn-apple { .btn-apple {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-small);
font-weight: 500; font-weight: 500;
font-size: 26rpx; font-size: 26rpx;
line-height: 1; line-height: 1;
cursor: pointer; cursor: pointer;
transition: all var(--duration-fast) var(--ease-out); transition: all 0.2s ease;
user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
padding: var(--space-5); padding: 20rpx;
background: var(--gray-100); background: var(--gray-100);
color: var(--text-primary); color: var(--text-primary);
@@ -490,44 +304,56 @@
} }
&.btn-secondary { &.btn-secondary {
background: var(--gray-200); background: var(--gray-300);
color: var(--gray-700); color: var(--text-secondary);
} }
&.btn-mini { &.btn-mini {
min-height: 52rpx; min-height: 40rpx;
padding: 0 var(--space-4); padding: 0 16rpx;
font-size: 24rpx; font-size: 24rpx;
border-radius: var(--radius-xs); border-radius: 6rpx;
} }
&.btn-large { &.btn-large {
min-height: 88rpx; min-height: 64rpx;
padding: 0 var(--space-8); padding: 0 32rpx;
font-size: 32rpx; font-size: 32rpx;
border-radius: var(--radius-md); border-radius: var(--radius-medium);
} }
&.btn-disabled { &.btn-disabled {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
pointer-events: none;
} }
} }
/* 旧进度条兼容 */ .btn-group {
.progress-apple { display: flex;
width: 100%; gap: 12rpx;
height: 10rpx;
background: var(--gray-100);
border-radius: var(--radius-full);
overflow: hidden;
.progress-fill { .btn-apple {
height: 100%; flex: 1;
background: var(--primary); }
border-radius: var(--radius-full);
transition: width var(--duration-slow) var(--ease-out); &.btn-group-vertical {
flex-direction: column;
} }
} }
/* 通用背景色 */
.bg-primary { background-color: var(--bg-primary); }
.bg-secondary { background-color: var(--bg-secondary); }
.bg-success { background-color: var(--success); }
.bg-warning { background-color: var(--warning); }
.bg-danger { background-color: var(--danger); }
/* 通用文字色 */
.text-primary { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-tertiary { color: var(--text-tertiary); }
.text-inverse { color: var(--text-inverse); }
.text-success { color: var(--success); }
.text-warning { color: var(--warning); }
.text-danger { color: var(--danger); }
</style> </style>

View File

@@ -18,9 +18,20 @@ COPY package.json ./
# 安装所有依赖 # 安装所有依赖
RUN npm install --legacy-peer-deps RUN npm install --legacy-peer-deps
# 安装 git (用于获取 commit 信息)
RUN apk add --no-cache git
# 复制源代码 # 复制源代码
COPY . . 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 生产版本 # 构建 H5 生产版本
RUN npm run build:h5 RUN npm run build:h5
@@ -46,6 +57,7 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# 从构建阶段复制构建产物 # 从构建阶段复制构建产物
COPY --from=builder /build/dist/build/h5 /usr/share/nginx/html COPY --from=builder /build/dist/build/h5 /usr/share/nginx/html
COPY MP_verify_yU2Z8mp831jh6QX7.txt /usr/share/nginx/html/MP_verify_yU2Z8mp831jh6QX7.txt COPY MP_verify_yU2Z8mp831jh6QX7.txt /usr/share/nginx/html/MP_verify_yU2Z8mp831jh6QX7.txt
COPY MP_verify_0UoX8yClVgREjgPj.txt /usr/share/nginx/html/MP_verify_0UoX8yClVgREjgPj.txt
# 暴露端口 # 暴露端口
EXPOSE 80 EXPOSE 80

View File

@@ -0,0 +1 @@
0UoX8yClVgREjgPj

View File

@@ -4,4 +4,5 @@ export { deviceApi } from './modules/device.js';
export { exchangeApi } from './modules/exchange.js'; export { exchangeApi } from './modules/exchange.js';
export { orderApi } from './modules/order.js'; export { orderApi } from './modules/order.js';
export { realnameApi } from './modules/realname.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

@@ -1,217 +1,88 @@
<template> <template>
<view class="device-status-card"> <view class="card device-status-card">
<view class="card-header"> <view class="card-header mb-md flex-row-sb">
<text class="text-heading">设备信息</text> <view class="title">设备信息</view>
<view class="operator-badge"> <view class="tag-apple tag-success">{{ deviceInfo.carrier_name || '-' }}</view>
<text class="operator-name">中国电信</text>
</view>
</view> </view>
<view class="device-info flex-col-g16">
<view class="status-row"> <view class="info-row flex-row-sb">
<view class="status-item"> <view class="info-label">
<text class="status-label">当前卡</text> <view class="subtitle">当前卡号 {{ deviceInfo.currentIccid }}</view>
<text class="status-value iccid">{{ deviceInfo.currentIccid }}</text> </view>
<view class="info-values flex-row-g20" @tap="$emit('authentication')">
<view class="tag-apple" :class="isRealName ? 'tag-success' : 'tag-warning'">
{{ isRealName ? '已实名' : '未实名' }}
</view>
</view>
</view> </view>
<view class="status-item"> <view class="info-row flex-row-sb">
<text class="status-label">实名状态</text> <view class="info-label">
<view class="auth-badge" :class="isRealName ? 'auth-done' : 'auth-pending'" @tap="$emit('authentication')"> <view class="subtitle" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</view>
<text>{{ isRealName ? '已实名' : '未实名' }}</text> <view class="subtitle" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</view>
<text class="arrow"></text>
</view> </view>
</view> </view>
</view> </view>
<view class="device-metrics flex-row-sb mt-md">
<view class="status-row"> <view class="metric-item flex-col-center">
<view class="status-item full-width"> <view class="caption mb-xs">信号强度</view>
<text class="status-label">到期时间</text> <view class="metric-value">{{ getSignalText(deviceInfo) }}</view>
<text class="status-value expire">{{ deviceInfo.expireDate }}</text>
</view> </view>
</view> <view class="metric-item flex-col-center">
<view class="caption mb-xs">设备电量</view>
<view class="metrics-grid"> <view class="metric-value">{{ deviceInfo.battery }} %</view>
<view class="metric-card">
<view class="metric-icon signal">
<image src="/static/signal.png" mode="aspectFit" v-if="false"></image>
<text class="metric-icon-text">📶</text>
</view>
<text class="metric-label">信号强度</text>
<text class="metric-value">{{ getSignalText(deviceInfo.rssi) }}</text>
</view> </view>
<view class="metric-card"> <view class="metric-item flex-col-center">
<view class="metric-icon battery"> <view class="caption mb-xs">连接数量</view>
<text class="metric-icon-text">🔋</text> <view class="metric-value">{{ deviceInfo.connCnt }}/{{ deviceInfo.max_clients }}</view>
</view>
<text class="metric-label">设备电量</text>
<text class="metric-value">{{ deviceInfo.battery }}%</text>
</view>
<view class="metric-card">
<view class="metric-icon connections">
<text class="metric-icon-text">📱</text>
</view>
<text class="metric-label">连接数量</text>
<text class="metric-value">{{ deviceInfo.connCnt }}/{{ deviceInfo.max_clients }}</text>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'; import { getNetworkLevelByMetrics } from '@/utils/networkSignal.js';
const props = defineProps({ defineProps({
deviceInfo: { type: Object, default: () => ({}) }, deviceInfo: { type: Object, default: () => ({}) },
isRealName: { type: Boolean, default: false }, isRealName: { type: Boolean, default: false },
isDevice: { type: Boolean, default: true },
opratorList: { type: Array, default: () => [] } opratorList: { type: Array, default: () => [] }
}); });
defineEmits(['authentication']); defineEmits(['authentication']);
const getSignalText = (rssi) => rssi || '强'; const getSignalText = (deviceInfo) =>
getNetworkLevelByMetrics(deviceInfo?.rsrp, deviceInfo?.rsrq, deviceInfo?.sinr);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.device-status-card { .device-status-card {
background: var(--bg-surface); .info-row {
border-radius: var(--radius-xl); padding: var(--space-sm) 0;
padding: var(--space-5); border-bottom: 1rpx solid var(--gray-200);
box-shadow: var(--shadow-sm);
}
.card-header { &:last-child {
display: flex; border-bottom: none;
align-items: center; }
justify-content: space-between;
padding-bottom: var(--space-4);
border-bottom: 1rpx solid var(--gray-100);
margin-bottom: var(--space-4);
}
.operator-badge { .info-label {
padding: var(--space-1) var(--space-3); min-width: 200rpx;
background: var(--primary-tint); }
border-radius: var(--radius-full);
}
.operator-name { .info-values {
font-size: 22rpx; flex: 1;
font-weight: 600; justify-content: flex-end;
color: var(--primary); }
}
.status-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) 0;
& + .status-row {
border-top: 1rpx solid var(--gray-50);
}
}
.status-item {
display: flex;
flex-direction: column;
gap: var(--space-1);
&.full-width {
flex: 1;
}
}
.status-label {
font-size: 24rpx;
color: var(--text-tertiary);
}
.status-value {
font-size: 28rpx;
font-weight: 600;
color: var(--text-primary);
&.iccid {
font-family: "SF Mono", "Menlo", monospace;
font-size: 24rpx;
} }
&.expire { .device-metrics {
color: var(--primary); background: var(--gray-100);
border-radius: var(--radius-small);
padding: 20rpx;
.metric-item {
flex: 1;
}
} }
} }
</style>
.auth-badge {
display: inline-flex;
align-items: center;
gap: 4rpx;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
font-size: 24rpx;
font-weight: 600;
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-out);
&:active {
transform: scale(0.96);
}
&.auth-done {
background: var(--success-light);
color: var(--success);
}
&.auth-pending {
background: var(--warning-light);
color: var(--warning);
}
.arrow {
font-size: 28rpx;
line-height: 1;
}
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-3);
margin-top: var(--space-5);
padding-top: var(--space-5);
border-top: 1rpx solid var(--gray-100);
}
.metric-card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4);
background: var(--gray-50);
border-radius: var(--radius-md);
}
.metric-icon {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
.metric-icon-text {
font-size: 32rpx;
}
}
.metric-label {
font-size: 22rpx;
color: var(--text-tertiary);
text-align: center;
}
.metric-value {
font-size: 28rpx;
font-weight: 700;
color: var(--text-primary);
text-align: center;
}
</style>

View File

@@ -1,17 +1,13 @@
<template> <template>
<view class="service-card" @click="handleClick"> <view class="floating-service-card" @click="handleClick">
<view class="service-inner"> <view class="service-left">
<view class="service-left"> <image class="service-icon" src="/static/link.png" mode="aspectFit"></image>
<view class="service-icon-wrap"> <view class="service-content">
<image class="service-icon" src="/static/link.png" mode="aspectFit"></image> <text class="service-title">需要帮助</text>
</view> <text class="service-desc">专属客服在线解答</text>
<view class="service-content">
<text class="service-title">需要帮助</text>
<text class="service-desc">专属客服在线解答</text>
</view>
</view> </view>
<view class="service-btn">立即联系</view>
</view> </view>
<view class="service-btn">立即联系</view>
</view> </view>
</template> </template>
@@ -24,71 +20,56 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.service-card { .floating-service-card {
position: fixed; position: fixed;
bottom: calc(var(--space-5) + env(safe-area-inset-bottom)); bottom: 20rpx;
left: var(--space-4); left: 20rpx;
right: var(--space-4); right: 20rpx;
z-index: 999; height: 120rpx;
} background: #fff;
border-radius: 60rpx;
.service-inner { box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-4) var(--space-5); padding: 0 24rpx 0 24rpx;
background: var(--bg-surface); z-index: 1000;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
} }
.service-left { .service-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-4); gap: 20rpx;
}
.service-icon-wrap { .service-icon {
width: 80rpx; width: 60rpx;
height: 80rpx; height: 60rpx;
display: flex; }
align-items: center;
justify-content: center;
background: var(--primary-tint);
border-radius: var(--radius-md);
}
.service-icon { .service-content {
width: 44rpx; display: flex;
height: 44rpx; flex-direction: column;
} gap: 4rpx;
.service-content { .service-title {
display: flex; font-size: 28rpx;
flex-direction: column; font-weight: 600;
gap: 2rpx; color: var(--text-primary);
} }
.service-title { .service-desc {
font-size: 28rpx; font-size: 22rpx;
font-weight: 600; color: var(--text-tertiary);
color: var(--text-primary); }
line-height: 1.3; }
}
.service-desc {
font-size: 24rpx;
color: var(--text-tertiary);
line-height: 1.3;
} }
.service-btn { .service-btn {
padding: var(--space-3) var(--space-5); padding: 16rpx 32rpx;
background: var(--primary); background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: var(--text-inverse); color: #fff;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
border-radius: var(--radius-full); border-radius: 40rpx;
line-height: 1;
} }
</style> </style>

View File

@@ -1,22 +1,111 @@
<template> <template>
<view class="card function-card"> <view class="card function-card">
<view class="card-header mb-5"> <view class="card-header">
<text class="text-heading">功能服务</text> <view class="title">功能菜单</view>
<!-- <view class="header-actions">
<button class="btn-apple btn-primary" @tap="$emit('sync')">运营商数据同步</button>
</view> -->
</view> </view>
<view class="function-grid"> <view class="function-grid">
<view <view class="function-item interactive card-interactive" @tap="$emit('enter', 'authentication')"
v-for="item in displayFunctions" role="button" tabindex="0">
:key="item.key" <view class="function-icon">
class="function-item interactive" <image src="/static/authentication.png" mode="aspectFit" alt="实名认证"></image>
:class="{ 'is-disabled': item.disabled }"
@click="handleItemClick(item)"
role="button"
tabindex="0"
>
<view class="function-icon-wrap">
<image class="function-icon" :src="item.icon" mode="aspectFit" :alt="item.name"></image>
</view> </view>
<text class="function-name" :class="{ 'text-primary-color': item.highlight }">{{ item.name }}</text> <view :class="['function-name', realNameStatus === '已实名' ? 'text-primary' : '']">
{{ realNameStatus || '实名认证' }}
</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('sync')" role="button" tabindex="0">
<view class="function-icon">
<image src="/static/data.png" mode="aspectFit" alt="运营数据同步"></image>
</view>
<view class="function-name">运营数据同步</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'package-order')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/shop.png" mode="aspectFit" alt="套餐订购"></image>
</view>
<view class="function-name">套餐订购</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'order-list')" role="button"
tabindex="0">
<view class="function-icon">
<image src="/static/order.png" mode="aspectFit" alt="我的订单"></image>
</view>
<view class="function-name">我的订单</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'back')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/back.png" mode="aspectFit" alt="后台管理"></image>
</view>
<view class="function-name">后台管理</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'switch')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/change.png" mode="aspectFit" alt="切换运营商"></image>
</view>
<view class="function-name">切换运营商</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'asset-package')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/asset-package-history.png" mode="aspectFit" alt="资产套餐历史"></image>
</view>
<view class="function-name">资产套餐历史</view>
</view>
<view class="function-item interactive card-interactive" @tap="handleBindPhone" role="button" tabindex="0">
<view class="function-icon">
<image src="/static/bind-phone.png" mode="aspectFit" alt="绑定手机号"></image>
</view>
<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"
tabindex="0">
<view class="function-icon">
<image src="/static/change-phone.png" mode="aspectFit" alt="更换手机号"></image>
</view>
<view class="function-name">更换手机号</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice"
@tap="$emit('enter', 'device-exchange')" role="button" tabindex="0">
<view class="function-icon">
<image src="/static/change.png" mode="aspectFit" alt="设备换货"></image>
</view>
<view class="function-name">设备换货</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'wallet')" role="button"
tabindex="0">
<view class="function-icon">
<image src="/static/wallet.png" mode="aspectFit" alt="我的钱包"></image>
</view>
<view class="function-name">我的钱包</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'recover')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/recover.png" mode="aspectFit" alt="重启设备"></image>
</view>
<view class="function-name">重启设备</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'restart')"
role="button" tabindex="0">
<view class="function-icon">
<image src="/static/restart.png" mode="aspectFit" alt="恢复出厂"></image>
</view>
<view class="function-name">恢复出厂</view>
</view>
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'out')" role="button"
tabindex="0">
<view class="function-icon">
<image src="/static/out.png" mode="aspectFit" alt="退出登录"></image>
</view>
<view class="function-name">退出登录</view>
</view> </view>
</view> </view>
@@ -25,8 +114,6 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue';
const props = defineProps({ const props = defineProps({
realNameStatus: { realNameStatus: {
type: String, type: String,
@@ -44,95 +131,62 @@
const emit = defineEmits(['enter', 'sync']); const emit = defineEmits(['enter', 'sync']);
const functions = computed(() => { const handleBindPhone = () => {
const list = [ if (props.alreadyBindPhone) {
{ key: 'authentication', name: props.realNameStatus || '实名认证', icon: '/static/authentication.png', action: 'authentication', highlight: props.realNameStatus === '已实名' }, // 已绑定,不可点击
{ key: 'sync', name: '运营数据同步', icon: '/static/data.png', action: 'sync' }, return;
{ key: 'package-order', name: '套餐订购', icon: '/static/shop.png', action: 'package-order' },
{ key: 'order-list', name: '我的订单', icon: '/static/order.png', action: 'order-list' },
{ key: 'back', name: '后台管理', icon: '/static/back.png', action: 'back', deviceOnly: true },
{ key: 'switch', name: '切换运营商', icon: '/static/change.png', action: 'switch', deviceOnly: true },
{ key: 'asset-package', name: '资产套餐历史', icon: '/static/shop.png', action: 'asset-package' },
{ key: 'bind', name: props.alreadyBindPhone ? '已绑定' : '绑定手机号', icon: '/static/bind-phone.png', action: 'bind', highlight: props.alreadyBindPhone, disabled: props.alreadyBindPhone },
{ key: 'change-phone', name: '更换手机号', icon: '/static/change-phone.png', action: 'change-phone' },
{ key: 'device-exchange', name: '设备换货', icon: '/static/change.png', action: 'device-exchange', deviceOnly: true },
{ key: 'wallet', name: '我的钱包', icon: '/static/wallet.png', action: 'wallet' },
{ key: 'recover', name: '重启设备', icon: '/static/recover.png', action: 'recover', deviceOnly: true },
{ key: 'restart', name: '恢复出厂', icon: '/static/restart.png', action: 'restart', deviceOnly: true },
{ key: 'out', name: '退出登录', icon: '/static/out.png', action: 'out' }
];
return list.filter(item => !item.deviceOnly || props.isDevice);
});
const displayFunctions = computed(() => functions.value);
const handleItemClick = (item) => {
if (item.disabled) return;
if (item.action === 'bind' && props.alreadyBindPhone) return;
if (item.action === 'sync') {
emit('sync');
} else {
emit('enter', item.action);
} }
// 未绑定,可以点击进入绑定页面
emit('enter', 'bind');
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.function-card { .function-card {
.card-header { .card-header {
padding-bottom: var(--space-4); display: flex;
border-bottom: 1rpx solid var(--gray-100); justify-content: space-between;
align-items: center;
}
.function-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
width: 100%;
.function-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx;
.function-icon {
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
image {
width: 100%;
height: 100%;
}
}
.function-name {
font-size: 28rpx;
font-weight: 500;
color: var(--text-primary);
text-align: center;
margin-top: 15rpx;
}
}
} }
} }
.function-grid { .text-primary {
display: grid; color: var(--primary) !important;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
padding-top: var(--space-5);
}
.function-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
border-radius: var(--radius-md);
transition: background var(--duration-fast) var(--ease-out);
&:active {
background: var(--gray-50);
}
&.is-disabled {
opacity: 0.5;
pointer-events: none;
}
}
.function-icon-wrap {
width: 88rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-50);
border-radius: var(--radius-md);
}
.function-icon {
width: 48rpx;
height: 48rpx;
}
.function-name {
font-size: 22rpx;
font-weight: 500;
color: var(--text-secondary);
text-align: center;
line-height: 1.3;
white-space: nowrap;
} }
</style> </style>

View File

@@ -1,40 +1,36 @@
<template> <template>
<view class="traffic-card"> <view class="card traffic-card">
<view class="card-header"> <view class="card-header flex-row-sb mb-lg">
<text class="text-heading">本月流量</text> <view class="title">本月流量</view>
<view class="usage-indicator"> <view class="usage-percent">
<text class="usage-value">{{ usedFlowPercent }}</text> <text class="title">{{ usedFlowPercent }}</text>
<text class="usage-unit">%</text> <text class="caption">%</text>
</view> </view>
</view> </view>
<view class="progress-section mb-lg">
<view class="progress-section"> <view class="progress-apple">
<view class="progress-track"> <view class="progress-fill" :style="{width: usedFlowPercent + '%'}"></view>
<view class="progress-fill" :style="{ width: usedFlowPercent + '%' }"></view>
</view> </view>
</view> </view>
<view class="traffic-stats flex-row-sb">
<view class="traffic-stats"> <view class="stat-item flex-col-center">
<view class="stat-item"> <view class="caption mb-xs">已使用</view>
<text class="stat-label">已使用</text> <view class="stat-value-container">
<view class="stat-value-wrap">
<text class="stat-value">{{ usedValue }}</text> <text class="stat-value">{{ usedValue }}</text>
<text class="stat-unit">{{ usedUnit }}</text> <text class="stat-unit">{{ usedUnit }}</text>
</view> </view>
</view> </view>
<view class="stat-divider"></view> <view class="stat-item flex-col-center">
<view class="stat-item"> <view class="caption mb-xs">总流量</view>
<text class="stat-label">总流量</text> <view class="stat-value-container">
<view class="stat-value-wrap">
<text class="stat-value">{{ totalValue }}</text> <text class="stat-value">{{ totalValue }}</text>
<text class="stat-unit">{{ totalUnit }}</text> <text class="stat-unit">{{ totalUnit }}</text>
</view> </view>
</view> </view>
<view class="stat-divider"></view> <view class="stat-item flex-col-center">
<view class="stat-item"> <view class="caption mb-xs">剩余</view>
<text class="stat-label">剩余</text> <view class="stat-value-container">
<view class="stat-value-wrap"> <text class="stat-value">{{ remainValue }}</text>
<text class="stat-value remain">{{ remainValue }}</text>
<text class="stat-unit">{{ remainUnit }}</text> <text class="stat-unit">{{ remainUnit }}</text>
</view> </view>
</view> </view>
@@ -43,12 +39,23 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from 'vue'; import {
import { assetApi } from '@/api/index.js'; ref,
import { useUserStore } from '@/store/index.js'; computed,
watch
} from 'vue';
import {
assetApi
} from '@/api/index.js';
import {
useUserStore
} from '@/store/index.js';
const props = defineProps({ const props = defineProps({
identifier: { type: String, default: '' } identifier: {
type: String,
default: ''
}
}); });
const emit = defineEmits(['packageLoaded']); const emit = defineEmits(['packageLoaded']);
@@ -63,7 +70,10 @@
// 格式化流量数据MB 转 GB // 格式化流量数据MB 转 GB
const formatTraffic = (mb) => { const formatTraffic = (mb) => {
if (!mb || mb === 0) { if (!mb || mb === 0) {
return { value: '0.00', unit: 'MB' }; return {
value: '0.00',
unit: 'MB'
};
} }
if (mb >= 1024) { if (mb >= 1024) {
@@ -95,9 +105,9 @@
const usedFlowPercent = computed(() => { const usedFlowPercent = computed(() => {
if (totalMB.value && usedMB.value) { if (totalMB.value && usedMB.value) {
const percent = (usedMB.value / totalMB.value * 100); const percent = (usedMB.value / totalMB.value * 100);
return Math.min(percent, 100).toFixed(1); return Math.min(percent, 100).toFixed(2);
} }
return '0.0'; return '0.00';
}); });
// 加载套餐流量数据 // 加载套餐流量数据
@@ -106,28 +116,19 @@
if (!identifier) return; if (!identifier) return;
try { try {
const data = await assetApi.getPackageHistory(identifier, 1, 10, { status: 1 }); const data = await assetApi.getInfo(identifier);
if (data.items && data.items.length > 0) { if (data.current_package_usage_id) {
// 只有一个生效中的套餐 if (data.enable_virtual_data) {
const activePackage = data.items[0]; usedMB.value = data.virtual_used_mb || 0;
totalMB.value = data.virtual_total_mb || 0;
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 { } else {
// 未启用虚流量,使用真流量 usedMB.value = data.real_used_mb || 0;
usedMB.value = activePackage.data_usage_mb || 0; totalMB.value = data.real_total_mb || 0;
totalMB.value = activePackage.data_limit_mb || 0;
remainMB.value = totalMB.value - usedMB.value;
} }
remainMB.value = Math.max(totalMB.value - usedMB.value, 0);
// 将套餐信息(包括到期时间)传递给父组件 emit('packageLoaded', data);
emit('packageLoaded', activePackage);
} else { } else {
// 没有生效中的套餐
usedMB.value = 0; usedMB.value = 0;
totalMB.value = 0; totalMB.value = 0;
remainMB.value = 0; remainMB.value = 0;
@@ -143,110 +144,34 @@
if (newVal) { if (newVal) {
loadPackageData(); loadPackageData();
} }
}, { immediate: true }); }, {
immediate: true
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.traffic-card { .traffic-card {
background: var(--bg-surface); .usage-percent {
border-radius: var(--radius-xl); display: flex;
padding: var(--space-5); align-items: baseline;
box-shadow: var(--shadow-sm); gap: 4rpx;
}
.card-header { .title {
display: flex; font-size: 35rpx;
align-items: center; font-weight: 700;
justify-content: space-between; margin-right: 5rpx;
margin-bottom: var(--space-5); }
} }
.usage-indicator { .traffic-stats {
display: flex; background: var(--gray-100);
align-items: baseline; border-radius: var(--radius-small);
gap: 2rpx; padding: 20rpx;
} margin-top: 16rpx;
.usage-value { .stat-item {
font-size: 48rpx; flex: 1;
font-weight: 700; }
color: var(--primary);
letter-spacing: -0.02em;
line-height: 1;
}
.usage-unit {
font-size: 28rpx;
font-weight: 600;
color: var(--text-tertiary);
}
.progress-section {
margin-bottom: var(--space-5);
}
.progress-track {
height: 14rpx;
background: var(--gray-100);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: var(--radius-full);
transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.traffic-stats {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
background: var(--gray-50);
border-radius: var(--radius-lg);
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: var(--gray-200);
}
.stat-label {
font-size: 24rpx;
color: var(--text-tertiary);
}
.stat-value-wrap {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: var(--text-primary);
line-height: 1.1;
&.remain {
color: var(--success);
} }
} }
.stat-unit {
font-size: 22rpx;
font-weight: 500;
color: var(--text-tertiary);
}
</style> </style>

View File

@@ -1,25 +1,19 @@
<template> <template>
<view class="user-info-card"> <view class="card user-info-card interactive">
<view class="card-main"> <view class="flex-row-g20">
<view class="user-avatar"> <view class="user-avatar">
<image :src="avatarUrl" mode="aspectFill" alt="用户头像" /> <image :src="avatarUrl" mode="aspectFill" alt="用户头像" />
</view> </view>
<view class="user-details"> <view class="user-details flex-col-g8">
<view class="user-id"> <view class="flex-row-g20">
<text class="device-id">{{ currentCardNo || '-' }}</text> <view class="title">{{ currentCardNo || '-' }}</view>
<view class="status-badge" :class="isOnline ? 'status-online' : 'status-offline'">
<text class="status-dot"></text>
<text class="status-text">{{ isOnline ? '在线' : '离线' }}</text>
</view>
</view>
<view class="user-meta">
<text class="meta-text" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</text>
<text class="meta-text" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</text>
</view> </view>
<view class="caption">套餐到期时间{{ deviceInfo.expireDate || '-' }}</view>
</view> </view>
</view> <view v-if="isDevice" class="tag-apple" :class="onlineStatus === '在线' ? 'tag-success' : 'tag-warning'">
<view class="card-footer" v-if="!isDevice"> {{ onlineStatus }}
<view class="network-badge" :class="networkStatus == 1 ? 'network-ok' : 'network-stop'"> </view>
<view v-else class="tag-apple" :class="networkStatus == 1 ? 'tag-success' : 'tag-warning'">
{{ networkStatus == 1 ? '正常' : '停机' }} {{ networkStatus == 1 ? '正常' : '停机' }}
</view> </view>
</view> </view>
@@ -30,137 +24,36 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useUserStore } from '@/store/index.js'; import { useUserStore } from '@/store/index.js';
const props = defineProps({ const userStore = useUserStore();
defineProps({
currentCardNo: { type: String, default: '' }, currentCardNo: { type: String, default: '' },
deviceInfo: { type: Object, default: () => ({}) }, deviceInfo: { type: Object, default: () => ({}) },
onlineStatus: { type: String, default: '离线' }, onlineStatus: { type: String, default: '离线' },
networkStatus: { type: [String, Number], default: '离线' }, networkStatus: { type: String, default: '离线' },
isDevice: { type: Boolean, default: true } isDevice: { type: Boolean, default: true }
}); });
const userStore = useUserStore(); // 默认头像
const defaultAvatar = 'https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500'; const defaultAvatar = 'https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500';
// 用户头像,优先使用 store 中的头像,否则使用默认头像
const avatarUrl = computed(() => { const avatarUrl = computed(() => {
return userStore.state.userInfo.avatar || defaultAvatar; return userStore.state.userInfo.avatar || defaultAvatar;
}); });
const isOnline = computed(() => {
return props.onlineStatus === '在线';
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.user-info-card { .user-info-card {
background: var(--bg-surface);
border-radius: var(--radius-xl);
padding: var(--space-5);
box-shadow: var(--shadow-md);
}
.card-main {
display: flex;
align-items: center;
gap: var(--space-4);
}
.user-avatar {
width: 96rpx;
height: 96rpx;
border-radius: var(--radius-lg);
overflow: hidden;
border: 4rpx solid var(--gray-100);
flex-shrink: 0;
image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user-details {
flex: 1;
min-width: 0;
}
.user-id {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.device-id {
font-size: 32rpx;
font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
letter-spacing: -0.01em; .user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: var(--radius-medium);
overflow: hidden;
border: 2rpx solid var(--border-light);
image { width: 100%; height: 100%; object-fit: cover; }
}
.user-details { flex: 1; }
} }
</style>
.status-badge {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 12rpx;
border-radius: var(--radius-full);
font-size: 20rpx;
font-weight: 600;
}
.status-online {
background: var(--success-light);
color: var(--success);
}
.status-offline {
background: var(--warning-light);
color: var(--warning);
}
.status-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: currentColor;
}
.status-text {
line-height: 1;
}
.user-meta {
margin-top: var(--space-2);
}
.meta-text {
font-size: 24rpx;
color: var(--text-tertiary);
}
.card-footer {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1rpx solid var(--gray-100);
}
.network-badge {
display: inline-flex;
align-items: center;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-full);
font-size: 24rpx;
font-weight: 600;
}
.network-ok {
background: var(--success-light);
color: var(--success);
}
.network-stop {
background: var(--danger-light);
color: var(--danger);
}
</style>

View File

@@ -1,29 +1,25 @@
<template> <template>
<view class="wifi-card"> <view class="card wifi-card">
<view class="card-header"> <view class="card-header flex-row-sb">
<text class="text-heading">WiFi配置</text> <view class="title">WiFi配置</view>
<button class="btn btn-mini btn-primary" @tap="$emit('modify')">修改</button> <view class="btn-group">
</view> <button class="btn-apple btn-primary btn-mini" @tap="$emit('modify')">修改</button>
<view class="wifi-list">
<view class="wifi-item">
<view class="wifi-icon-wrap">
<text class="wifi-icon-text">📶</text>
</view>
<view class="wifi-content">
<text class="wifi-label">网络名称</text>
<text class="wifi-value">{{ deviceInfo.ssidName }}</text>
</view>
<button class="btn-copy" @tap="$emit('copy', deviceInfo.ssidName)">复制</button>
</view> </view>
<view class="wifi-item"> </view>
<view class="wifi-icon-wrap"> <view class="wifi-info flex-col-g16 mt-30">
<text class="wifi-icon-text">🔑</text> <view class="wifi-item flex-row-sb">
<view class="wifi-details flex-col-g8">
<view class="caption">网络名称</view>
<view class="subtitle">{{ deviceInfo.ssidName }}</view>
</view> </view>
<view class="wifi-content"> <button class="btn-apple btn-primary btn-mini" @tap="$emit('copy', deviceInfo.ssidName)">复制</button>
<text class="wifi-label">连接密码</text> </view>
<text class="wifi-value">{{ deviceInfo.ssidPwd }}</text> <view class="wifi-item flex-row-sb">
<view class="wifi-details flex-col-g8">
<view class="caption">连接密码</view>
<view class="subtitle">{{ deviceInfo.ssidPwd }}</view>
</view> </view>
<button class="btn-copy" @tap="$emit('copy', deviceInfo.ssidPwd)">复制</button> <button class="btn-apple btn-primary btn-mini" @tap="$emit('copy', deviceInfo.ssidPwd)">复制</button>
</view> </view>
</view> </view>
</view> </view>
@@ -39,86 +35,17 @@
<style scoped lang="scss"> <style scoped lang="scss">
.wifi-card { .wifi-card {
background: var(--bg-surface); .wifi-item {
border-radius: var(--radius-xl); padding: var(--space-md);
padding: var(--space-5); background: var(--gray-100);
box-shadow: var(--shadow-sm); border-radius: var(--radius-medium);
} margin-bottom: var(--space-sm);
&:last-child { margin-bottom: 0; }
.card-header { .wifi-details { flex: 1; }
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: var(--space-4);
border-bottom: 1rpx solid var(--gray-100);
margin-bottom: var(--space-4);
}
.wifi-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.wifi-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
background: var(--gray-50);
border-radius: var(--radius-md);
}
.wifi-icon-wrap {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-surface);
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.wifi-icon-text {
font-size: 32rpx;
}
.wifi-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2rpx;
}
.wifi-label {
font-size: 22rpx;
color: var(--text-tertiary);
}
.wifi-value {
font-size: 28rpx;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-copy {
padding: var(--space-2) var(--space-4);
background: var(--bg-surface);
color: var(--primary);
font-size: 24rpx;
font-weight: 600;
border: none;
border-radius: var(--radius-full);
line-height: 1.5;
flex-shrink: 0;
&::after {
border: none;
} }
} }
</style>
.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" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<title></title> <title></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"/> <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> </head>
<body> <body>
<div id="app"><!--app-html--></div> <div id="app"><!--app-html--></div>

15
main.js
View File

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

View File

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

110
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"@dcloudio/uni-h5": "3.0.0-4080720251210001", "@dcloudio/uni-h5": "3.0.0-4080720251210001",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"jweixin-module": "^1.6.0",
"uview-plus": "^3.6.29", "uview-plus": "^3.6.29",
"vue": "^3.4.0" "vue": "^3.4.0"
}, },
@@ -18,6 +19,7 @@
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001", "@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001", "@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001", "@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"cross-env": "^10.1.0",
"less": "^4.0.0", "less": "^4.0.0",
"sass": "^1.63.2", "sass": "^1.63.2",
"vite": "5.2.8" "vite": "5.2.8"
@@ -2332,6 +2334,13 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.20.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -4216,6 +4225,39 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/css-font-size-keywords": { "node_modules/css-font-size-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
@@ -5048,6 +5090,13 @@
"url": "https://github.com/sponsors/gjtorikian/" "url": "https://github.com/sponsors/gjtorikian/"
} }
}, },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5096,6 +5145,12 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jweixin-module": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/jweixin-module/-/jweixin-module-1.6.0.tgz",
"integrity": "sha512-dGk9cf+ipipHmtzYmKZs5B2toX+p4hLyllGLF6xuC8t+B05oYxd8fYoaRz0T30U2n3RUv8a4iwvjhA+OcYz52w==",
"license": "ISC"
},
"node_modules/lcid": { "node_modules/lcid": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmmirror.com/lcid/-/lcid-3.1.1.tgz", "resolved": "https://registry.npmmirror.com/lcid/-/lcid-3.1.1.tgz",
@@ -5565,6 +5620,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
@@ -5625,9 +5690,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.9", "version": "8.5.10",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.9.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -6339,6 +6404,29 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
@@ -6955,6 +7043,22 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/xmlhttprequest": { "node_modules/xmlhttprequest": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmmirror.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", "resolved": "https://registry.npmmirror.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",

View File

@@ -1,13 +1,14 @@
{ {
"scripts": { "scripts": {
"dev:h5": "uni", "dev:h5": "uni",
"build:h5": "UNI_INPUT_DIR=. uni build -p h5" "build:h5": "cross-env UNI_INPUT_DIR=. uni build -p h5"
}, },
"devDependencies": { "devDependencies": {
"@dcloudio/types": "3.4.19", "@dcloudio/types": "3.4.19",
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001", "@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001", "@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001", "@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"cross-env": "^10.1.0",
"less": "^4.0.0", "less": "^4.0.0",
"sass": "^1.63.2", "sass": "^1.63.2",
"vite": "5.2.8" "vite": "5.2.8"
@@ -16,9 +17,10 @@
"@dcloudio/uni-app": "3.0.0-4080720251210001", "@dcloudio/uni-app": "3.0.0-4080720251210001",
"@dcloudio/uni-components": "3.0.0-4080720251210001", "@dcloudio/uni-components": "3.0.0-4080720251210001",
"@dcloudio/uni-h5": "3.0.0-4080720251210001", "@dcloudio/uni-h5": "3.0.0-4080720251210001",
"vue": "^3.4.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"dayjs": "^1.11.19", "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": { "globalStyle": {
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
"navigationBarTitleText": "信息查询", "navigationBarTitleText": "设备卡信息查询",
"navigationBarBackgroundColor": "#F8F8F8", "navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8" "backgroundColor": "#F8F8F8"
}, },
"uniIdRouter": {} "uniIdRouter": {}

View File

@@ -15,30 +15,30 @@
</view> </view>
<view class="package-info"> <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-label">套餐类型</view>
<view class="info-value">{{ item.package_type === 'formal' ? '普通套餐' : '加油包' }}</view> <view class="info-value">{{ item.package_type === 'formal' ? '普通套餐' : '加油包' }}</view>
</view> </view>
<view class="info-row flex-row-sb"> <view class="info-row flex-row-sb">
<view class="info-label">使用类型</view> <view class="info-label">使用类型</view>
<view class="info-value">{{ item.usage_type === 'single_card' ? '单卡' : '设备' }}</view> <view class="info-value">{{ item.usage_type === 'single_card' ? '单卡' : '设备' }}</view>
</view> </view> -->
<view class="info-row flex-row-sb"> <view class="info-row flex-row-sb">
<view class="info-label">激活时间</view> <view class="info-label">激活时间</view>
<view class="info-value">{{ item.activated_at || '-' }}</view> <view class="info-value">{{ item.activated_at || '-' }}</view>
</view> </view>
<view class="info-row flex-row-sb"> <!-- <view class="info-row flex-row-sb">
<view class="info-label">创建时间</view> <view class="info-label">创建时间</view>
<view class="info-value">{{ item.created_at }}</view> <view class="info-value">{{ item.created_at }}</view>
</view> </view> -->
<view class="info-row flex-row-sb"> <view class="info-row flex-row-sb">
<view class="info-label">到期时间</view> <view class="info-label">到期时间</view>
<view class="info-value">{{ item.expires_at || '-' }}</view> <view class="info-value">{{ item.expires_at || '-' }}</view>
</view> </view>
<view class="info-row flex-row-sb"> <!-- <view class="info-row flex-row-sb">
<view class="info-label">优先级</view> <view class="info-label">优先级</view>
<view class="info-value">{{ item.priority }}</view> <view class="info-value">{{ item.priority }}</view>
</view> </view> -->
</view> </view>
<view class="divider"></view> <view class="divider"></view>
@@ -111,33 +111,28 @@
// 获取已使用流量 // 获取已使用流量
const getUsedFlow = (item) => { const getUsedFlow = (item) => {
if (item.enable_virtual_data) { if (item.enable_virtual_data) {
// 启用虚流量,显示已用虚流量
return formatMB(item.virtual_used_mb || 0); return formatMB(item.virtual_used_mb || 0);
} else { } else {
// 未启用虚流量,显示已用真流量 return formatMB(item.real_used_mb || 0);
return formatMB(item.data_usage_mb || 0);
} }
}; };
// 获取总流量 // 获取总流量
const getTotalFlow = (item) => { const getTotalFlow = (item) => {
if (item.enable_virtual_data) { if (item.enable_virtual_data) {
// 启用虚流量,显示虚流量总量 return formatMB(item.virtual_total_mb || 0);
return formatMB(item.virtual_limit_mb || 0);
} else { } else {
// 未启用虚流量,显示真流量总量 return formatMB(item.real_total_mb || 0);
return formatMB(item.data_limit_mb || 0);
} }
}; };
// 获取剩余流量 // 获取剩余流量
const getRemainFlow = (item) => { const getRemainFlow = (item) => {
if (item.enable_virtual_data) { if (item.enable_virtual_data) {
// 启用虚流量,显示剩余虚流量 const remain = (item.virtual_total_mb || 0) - (item.virtual_used_mb || 0);
return formatMB(item.virtual_remain_mb || 0); return formatMB(remain);
} else { } else {
// 未启用虚流量,计算剩余真流量 const remain = (item.real_total_mb || 0) - (item.real_used_mb || 0);
const remain = (item.data_limit_mb || 0) - (item.data_usage_mb || 0);
return formatMB(remain); return formatMB(remain);
} }
}; };
@@ -147,13 +142,11 @@
let used, total; let used, total;
if (item.enable_virtual_data) { if (item.enable_virtual_data) {
// 启用虚流量
used = item.virtual_used_mb || 0; used = item.virtual_used_mb || 0;
total = item.virtual_limit_mb || 0; total = item.virtual_total_mb || 0;
} else { } else {
// 未启用虚流量 used = item.real_used_mb || 0;
used = item.data_usage_mb || 0; total = item.real_total_mb || 0;
total = item.data_limit_mb || 0;
} }
if (!total) return 0; if (!total) return 0;

View File

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

View File

@@ -4,7 +4,7 @@
:isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status" /> :isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status" />
<DeviceStatusCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" :isRealName="isRealName" <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" <!-- <VoiceCard v-if="!userInfo.isDevice" :voiceStats="voiceStats" :dateRangeText="dateRangeText"
@openDatePicker="openDateRangePicker" /> --> @openDatePicker="openDateRangePicker" /> -->
@@ -25,11 +25,11 @@
<view class="flex-col-g20"> <view class="flex-col-g20">
<view class="flex-col-g8"> <view class="flex-col-g8">
<label class="caption">名称:</label> <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>
<view class="flex-col-g8"> <view class="flex-col-g8">
<label class="caption">密码:</label> <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> </view>
<view class="btn-group mt-20"> <view class="btn-group mt-20">
@@ -156,6 +156,9 @@
ssidName: '-', ssidName: '-',
ssidPwd: '-', ssidPwd: '-',
rssi: '-', rssi: '-',
rsrp: '-',
rsrq: '-',
sinr: '-',
onlineStatus: '0', onlineStatus: '0',
connCnt: 0, connCnt: 0,
run_time: 0, run_time: 0,
@@ -229,9 +232,9 @@
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理 // 流量信息已由 TrafficCard 组件独立获取,这里不再处理
// 当前卡信息 // 当前卡信息
let currentCard = null;
if (data.cards && data.cards.length > 0) { if (data.cards && data.cards.length > 0) {
// 找到当前卡 currentCard = data.cards.find(card => card.is_current) || '';
const currentCard = data.cards.find(card => card.is_current) || data.cards[0];
deviceInfo.currentIccid = currentCard.iccid || '-'; deviceInfo.currentIccid = currentCard.iccid || '-';
// 卡列表 // 卡列表
@@ -245,6 +248,15 @@
deviceInfo.mchList = []; deviceInfo.mchList = [];
} }
// 运营商信息
const carrierTypeMap = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
};
deviceInfo.carrier_name = (currentCard && carrierTypeMap[currentCard.carrier_type]) || '-';
// 设备实时信息 // 设备实时信息
if (data.device_realtime) { if (data.device_realtime) {
const rt = data.device_realtime; const rt = data.device_realtime;
@@ -256,7 +268,10 @@
deviceInfo.connCnt = rt.client_number ?? 0; deviceInfo.connCnt = rt.client_number ?? 0;
deviceInfo.max_clients = rt.max_clients ?? 1; deviceInfo.max_clients = rt.max_clients ?? 1;
deviceInfo.run_time = rt.run_time || 0; deviceInfo.run_time = rt.run_time || 0;
deviceInfo.rssi = calculateSignalStrength(rt.rsrp, rt.rsrq, rt.rssi); deviceInfo.rssi = rt.rssi ?? '-';
deviceInfo.rsrp = rt.rsrp ?? '-';
deviceInfo.rsrq = rt.rsrq ?? '-';
deviceInfo.sinr = rt.sinr ?? '-';
} else { } else {
// 没有实时信息时设置默认值 // 没有实时信息时设置默认值
deviceInfo.onlineStatus = '0'; deviceInfo.onlineStatus = '0';
@@ -268,6 +283,9 @@
deviceInfo.max_clients = 1; deviceInfo.max_clients = 1;
deviceInfo.run_time = 0; deviceInfo.run_time = 0;
deviceInfo.rssi = '-'; deviceInfo.rssi = '-';
deviceInfo.rsrp = '-';
deviceInfo.rsrq = '-';
deviceInfo.sinr = '-';
} }
// 到期时间由 TrafficCard 组件获取套餐信息时一并返回 // 到期时间由 TrafficCard 组件获取套餐信息时一并返回
@@ -379,15 +397,27 @@
// #endif // #endif
}; };
const enterBack = () => { const enterBack = async () => {
if (!deviceInfo.lan_ip || deviceInfo.lan_ip === '-') { if (!deviceInfo.lan_ip || deviceInfo.lan_ip === '-') {
uni.showToast({ uni.showModal({
title: '暂无后台地址', title: '提示',
icon: 'none' content: '请将设备开机后, 等待2分钟左右进行连接WiFi',
showCancel: false,
buttonText: '我知道了'
}); });
return; 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}`; const url = deviceInfo.lan_ip.startsWith('http') ? deviceInfo.lan_ip : `http://${deviceInfo.lan_ip}`;
@@ -593,52 +623,55 @@
return; return;
} }
uni.showLoading({ uni.showModal({
title: '同步中...', title: '确认同步',
mask: true 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) => { const enterDetail = (name) => {
@@ -712,6 +745,11 @@
}; };
onMounted(() => { onMounted(() => {
const token = uni.getStorageSync('token');
if (!token) {
uni.reLaunch({ url: '/pages/login/login' });
return;
}
initCurrentMonth(); initCurrentMonth();
loadAssetInfo(); loadAssetInfo();
}); });
@@ -720,19 +758,22 @@
<style scoped lang="scss"> <style scoped lang="scss">
.wifi-popup { .wifi-popup {
width: 600rpx; width: 600rpx;
padding: var(--space-6); padding: 30rpx;
border-radius: var(--radius-xl);
background: var(--bg-surface);
.title { .title {
font-size: 34rpx; font-size: 32rpx;
font-weight: 700; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: var(--space-5); }
.wifi-input {
border: 2rpx solid var(--border-light);
border-radius: 8rpx;
padding: 8rpx 16rpx;
} }
} }
.bottom-spacer { .bottom-spacer {
height: 140rpx; height: 120rpx;
} }
</style> </style>

View File

@@ -1,63 +1,64 @@
<template> <template>
<view class="login-page"> <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-wrapper">
<view class="content"> <view class="content">
<view class="header"> <view class="header">
<view class="main-title">登录</view> <view class="main-title">欢迎登录</view>
<view class="sub-title">请输入标识进行登录</view> <view class="sub-title">请输入标识进行登录</view>
</view> </view>
<view class="form-section"> <view class="form-section">
<view class="input-wrap" :class="{ focus: inputFocus }"> <view class="input-wrap" :class="{ focus: inputFocus }">
<input <input
v-model="identifier" v-model="identifier"
class="input" class="input"
placeholder="请输入标识" placeholder="请输入标识"
placeholder-class="placeholder" placeholder-class="placeholder"
@focus="inputFocus = true" @focus="inputFocus = true"
@blur="inputFocus = false" @blur="handleBlur"
@input="showError = false" @input="showError = false"
/> />
</view> <image
<text v-if="showError" class="error">请输入标识</text> v-if="identifier && inputFocus"
</view> class="clear-icon"
src="/static/clear.png"
<view class="btn-section"> mode="aspectFit"
<button @mousedown.prevent="clearInput"
class="btn-login" @touchstart.prevent="clearInput"
:class="{ disabled: !identifier || loading || !agreed }" ></image>
:disabled="!identifier || loading || !agreed" </view>
@tap="handleLogin" <text v-if="showError" class="error">请输入标识</text>
> </view>
<view v-if="loading" class="loading">
<view class="dot"/> <view class="btn-section">
<view class="dot"/> <button
<view class="dot"/> 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> </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>
</view> </view>
</view> </view>
@@ -67,10 +68,32 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { authApi } from '@/api/index.js'; import { authApi } from '@/api/index.js';
import { useUserStore } from '@/store/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(); 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 identifier = ref('');
const loading = ref(false); const loading = ref(false);
const agreed = ref(true); const agreed = ref(true);
@@ -88,11 +111,23 @@
const redirectToWxAuth = (assetToken) => { const redirectToWxAuth = (assetToken) => {
sessionStorage.setItem('assetToken', 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`; 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; window.location.href = url;
}; };
const clearInput = () => {
identifier.value = '';
showError.value = false;
inputFocus.value = true;
};
const handleBlur = () => {
setTimeout(() => {
inputFocus.value = false;
}, 200);
};
const handleLogin = () => { const handleLogin = () => {
if (!identifier.value) { if (!identifier.value) {
showError.value = true; showError.value = true;
@@ -101,6 +136,57 @@
doLogin(); 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 () => { const doLogin = async () => {
loading.value = true; loading.value = true;
try { try {
@@ -108,20 +194,6 @@
userStore.setAssetToken(verifyData.asset_token); userStore.setAssetToken(verifyData.asset_token);
userStore.setIdentifier(identifier.value); 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); redirectToWxAuth(verifyData.asset_token);
} catch (e) { } catch (e) {
console.error('登录失败', e); console.error('登录失败', e);
@@ -130,7 +202,6 @@
}; };
const handleWechatCallback = async () => { const handleWechatCallback = async () => {
if (!USE_WECHAT_AUTH) return;
const code = getCode(); const code = getCode();
const assetToken = sessionStorage.getItem('assetToken'); const assetToken = sessionStorage.getItem('assetToken');
@@ -145,44 +216,18 @@
userStore.setToken(loginData.token); userStore.setToken(loginData.token);
uni.setStorageSync('identifier', userStore.state.identifier); uni.setStorageSync('identifier', userStore.state.identifier);
// H5 环境获取微信用户信息
// #ifdef H5 // #ifdef H5
try { if (loginData.user_info) {
// 通过 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: '用于完善用户资料'
});
userStore.setUserInfo({ userStore.setUserInfo({
avatar: userInfo.avatarUrl || '', avatar: loginData.user_info.avatar || '',
nickname: userInfo.nickName || '' nickname: loginData.user_info.nickname || ''
}); });
} catch (err) {
console.log('用户取消授权');
} }
// #endif // #endif
// 判断是否需要绑定手机号
if (loginData.need_bind_phone) { if (loginData.need_bind_phone) {
// 需要绑定手机号,跳转到绑定页面
uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' }); uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' });
} else { } else {
// 不需要绑定,直接进入首页
uni.redirectTo({ url: '/pages/index/index' }); uni.redirectTo({ url: '/pages/index/index' });
} }
} catch (e) { } catch (e) {
@@ -190,343 +235,172 @@
loading.value = false; loading.value = false;
} }
}; };
onMounted(() => {
handleWechatCallback();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.login-page { .login-page {
position: relative;
display: flex; display: flex;
flex-direction: column; align-items: center;
background: linear-gradient(135deg, #005cbf 0%, #007aff 50%, #4da6ff 100%); justify-content: center;
min-height: 100vh; min-height: 100vh;
} background: var(--bg-secondary);
max-width: 750rpx;
.overlay { margin: 0 auto;
position: absolute; width: 100%;
top: 0; padding: 40rpx;
left: 0; box-sizing: border-box;
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;
}
} }
.content-wrapper { .content-wrapper {
position: relative; width: 100%;
}
.content {
background: var(--bg-primary);
border-radius: var(--radius-large);
padding: 60rpx 40rpx; padding: 60rpx 40rpx;
z-index: 1; box-shadow: var(--shadow-medium);
} }
.header { .header {
margin-bottom: 80rpx; text-align: center;
margin-bottom: 60rpx;
} }
.main-title { .main-title {
font-size: 56rpx; font-size: 48rpx;
font-weight: 700; font-weight: 700;
color: #FFFFFF; color: var(--text-primary);
line-height: 1.3; line-height: 1.3;
margin-bottom: 24rpx; margin-bottom: 16rpx;
letter-spacing: 0.5rpx; letter-spacing: -0.5rpx;
text-shadow: 0 2rpx 8rpx rgba(64, 40, 128, 0.3);
} }
.sub-title { .sub-title {
font-size: 28rpx; font-size: 26rpx;
color: rgba(255, 255, 255, 0.9); color: var(--text-tertiary);
line-height: 1.5; line-height: 1.5;
letter-spacing: 0.5rpx;
text-shadow: 0 1rpx 4rpx rgba(64, 40, 128, 0.2);
} }
.form-section { .form-section {
margin-bottom: 60rpx; margin-bottom: 48rpx;
} }
.input-wrap { .input-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
background: rgba(255, 255, 255, 0.95); gap: 16rpx;
border-radius: 20rpx; background: var(--bg-secondary);
padding: 32rpx 40rpx; border: 2rpx solid var(--border-color);
transition: all 0.3s; border-radius: var(--radius-medium);
box-shadow: 0 4rpx 16rpx rgba(64, 40, 128, 0.1); padding: 28rpx 32rpx;
transition: all 0.3s ease;
} }
.input-wrap.focus { .input-wrap.focus {
background: rgba(255, 255, 255, 1); background: var(--bg-primary);
box-shadow: 0 4rpx 20rpx rgba(64, 40, 128, 0.2); border-color: var(--primary);
box-shadow: 0 0 0 4rpx rgba(10, 132, 255, 0.1);
} }
.input { .input {
flex: 1; flex: 1;
font-size: 32rpx; font-size: 30rpx;
color: #1A1A1A; color: var(--text-primary);
background: transparent; background: transparent;
} }
.placeholder { .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 { .error {
font-size: 24rpx; font-size: 24rpx;
color: #EF4444; color: var(--danger);
margin-top: 16rpx; margin-top: 16rpx;
padding-left: 8rpx; padding-left: 8rpx;
} }
.btn-section { .btn-section {
margin-bottom: 60rpx; margin-bottom: 48rpx;
} }
.btn-login { .btn-login {
width: 100%; width: 100%;
height: 108rpx; height: 96rpx;
background: rgba(255, 255, 255, 0.25); background: var(--primary);
backdrop-filter: blur(10px); border: none;
border: 2rpx solid rgba(255, 255, 255, 0.4); border-radius: var(--radius-medium);
border-radius: 20rpx; font-size: 32rpx;
font-size: 36rpx;
font-weight: 600; font-weight: 600;
color: #FFFFFF; color: var(--text-inverse);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.3s; transition: all 0.3s ease;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); box-shadow: 0 4rpx 12rpx rgba(10, 132, 255, 0.2);
} }
.btn-login.disabled { .btn-login.disabled {
background: rgba(255, 255, 255, 0.3); background: var(--gray-300);
color: rgba(255, 255, 255, 0.7); color: var(--text-quaternary);
border: 2rpx solid rgba(255, 255, 255, 0.2);
box-shadow: none; box-shadow: none;
} }
.btn-login:active:not(.disabled) { .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 { .loading {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8rpx; gap: 12rpx;
} }
.dot { .dot {
width: 10rpx; width: 12rpx;
height: 10rpx; height: 12rpx;
background: #FFFFFF; background: var(--text-inverse);
border-radius: 50%; border-radius: 50%;
animation: load 1.4s infinite ease-in-out both; animation: load 1.4s infinite ease-in-out both;
} }
@@ -563,14 +437,13 @@
} }
.text { .text {
color: rgba(255, 255, 255, 0.8); color: var(--text-tertiary);
white-space: nowrap; white-space: nowrap;
} }
.link { .link {
color: #FFFFFF; color: var(--primary);
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
text-decoration: underline;
} }
</style> </style>

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<view class="container"> <view class="container">
<view v-if="packageList.length === 0 && !loading" class="empty-state"> <view v-if="packageList.length === 0 && !loading" class="empty-state">
<view class="empty-icon">📦</view> <view class="empty-icon">📦</view>
<view class="empty-title">暂无可购套餐</view> <view class="empty-title">暂无可购套餐,请联系客服</view>
<view class="empty-desc">当前资产暂无适用的套餐</view> <view class="empty-desc">当前资产暂无适用的套餐</view>
</view> </view>
@@ -81,6 +81,7 @@
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import { assetApi, orderApi, walletApi } from '@/api/index.js'; import { assetApi, orderApi, walletApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js'; import { useUserStore } from '@/store/index.js';
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
const userStore = useUserStore(); const userStore = useUserStore();
@@ -276,63 +277,18 @@
}; };
const handleWechatPay = async (payConfig, isForceRecharge = false) => { const handleWechatPay = async (payConfig, isForceRecharge = false) => {
// 验证参数 try {
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) { await wechatH5Pay(payConfig);
uni.showToast({ title: '支付参数无效', icon: 'none' }); // 支付成功
return; 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(() => { onMounted(() => {

4757
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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) => { const setUserInfo = (userInfo) => {
state.userInfo = userInfo; Object.assign(state.userInfo, userInfo);
uni.setStorageSync('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_TYPE = 'official_account';
export const APP_ID = 'wx404dc822ce15c5f0'; export const APP_ID = 'wx404dc822ce15c5f0';
export const USE_WECHAT_AUTH = true;

39
utils/networkSignal.js Normal file
View File

@@ -0,0 +1,39 @@
const isEmptyValue = (value) => value === null || value === undefined || value === '' || value === '-';
const toFiniteNumber = (value) => {
if (isEmptyValue(value)) return null;
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;
};
const clampScore = (value) => Math.min(Math.max(value, 0), 100);
export const getNetworkScore = (rsrp, rsrq, sinr) => {
const rsrpValue = toFiniteNumber(rsrp);
const rsrqValue = toFiniteNumber(rsrq);
const sinrValue = toFiniteNumber(sinr);
const metrics = [
{ score: rsrpValue === null ? null : clampScore((rsrpValue + 120) * 2), weight: 0.5 },
{ score: sinrValue === null ? null : clampScore(sinrValue * 5), weight: 0.3 },
{ score: rsrqValue === null ? null : clampScore((rsrqValue + 20) * 5), weight: 0.2 }
].filter(item => item.score !== null);
if (!metrics.length) return null;
const totalWeight = metrics.reduce((sum, item) => sum + item.weight, 0);
return metrics.reduce((sum, item) => sum + item.score * item.weight, 0) / totalWeight;
};
export const getNetworkLevel = (score) => {
if (score === null || score === undefined || !Number.isFinite(score)) return '-';
if (score >= 85) return '极好';
if (score >= 70) return '良好';
if (score >= 50) return '一般';
if (score >= 30) return '较差';
return '很差';
};
export const getNetworkLevelByMetrics = (rsrp, rsrq, sinr) =>
getNetworkLevel(getNetworkScore(rsrp, rsrq, sinr));

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

@@ -1,5 +1,13 @@
import { BASE_URL } from './env.js'; import { BASE_URL } from './env.js';
const showErrorToast = (message) => {
uni.showToast({
title: message || '请求失败,请稍后重试',
icon: 'none',
duration: 2000
});
};
const request = (options) => { const request = (options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token') || ''; const token = uni.getStorageSync('token') || '';
@@ -14,44 +22,36 @@ const request = (options) => {
...options.header ...options.header
}, },
success: (res) => { success: (res) => {
const responseData = res.data || {};
const { code, msg, data } = responseData;
if (res.statusCode === 401) { if (res.statusCode === 401) {
uni.removeStorageSync('token'); uni.removeStorageSync('token');
uni.removeStorageSync('identifier'); uni.removeStorageSync('identifier');
uni.showToast({ showErrorToast(msg || '登录已过期,请重新登录');
title: '登录已过期,请重新登录',
icon: 'none',
duration: 2000
});
setTimeout(() => { setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' }); uni.reLaunch({ url: '/pages/login/login' });
}, 2000); }, 2000);
reject(res.data); reject(responseData);
return; return;
} }
const { code, msg, data } = res.data; if (code === 0) {
if (code === 0 || code === 200) {
resolve(data); resolve(data);
} else { return;
uni.showToast({
title: msg || '请求失败',
icon: 'none',
duration: 2000
});
reject(res.data);
} }
if (options.showError !== false) {
showErrorToast(msg);
}
reject(responseData);
}, },
fail: (err) => { fail: (err) => {
uni.showToast({ showErrorToast('网络请求失败');
title: '网络请求失败',
icon: 'none',
duration: 2000
});
reject(err); reject(err);
} }
}); });
}); });
}; };
export default request; export default request;

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({ export default defineConfig({
plugins: [uni()], plugins: [uni()],
}) root: process.cwd(),
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ['legacy-js-api', 'import']
}
}
}
})