Compare commits
53 Commits
ac8578b89a
...
feat/redes
| Author | SHA1 | Date | |
|---|---|---|---|
| c341ef1aac | |||
|
|
727c4126dd | ||
|
|
6916be02da | ||
|
|
68af449610 | ||
|
|
b3fcdba80f | ||
|
|
7a78e05570 | ||
|
|
17e4b84e4c | ||
|
|
427b25cabd | ||
|
|
af1dc68681 | ||
|
|
9c5778b706 | ||
|
|
9797de4202 | ||
|
|
dc3a37f60f | ||
|
|
8bf89dbe57 | ||
|
|
6da9353203 | ||
|
|
a30a689dba | ||
|
|
5113520b46 | ||
|
|
43a13bf863 | ||
|
|
d3a40f0d77 | ||
|
|
181d24a388 | ||
|
|
0be233e0c0 | ||
|
|
1477444599 | ||
|
|
9c56aa7e30 | ||
|
|
7ef8615103 | ||
|
|
1336b86fc2 | ||
|
|
e79d17058a | ||
|
|
ea7e2b7362 | ||
|
|
3f0a04f2ce | ||
|
|
3f792d642a | ||
|
|
eb81a5cb21 | ||
|
|
e9634cd4bf | ||
|
|
7a7d63d327 | ||
|
|
b39dff0293 | ||
|
|
bd9f3e6141 | ||
|
|
5e8ff94b7a | ||
|
|
1db7ea9b7c | ||
|
|
f634af1d65 | ||
|
|
52b9027b4f | ||
|
|
4ce6b8ddc1 | ||
|
|
5fa2b6dacf | ||
|
|
1a407924de | ||
|
|
0b8dd7af00 | ||
| 36a1e94d34 | |||
|
|
243f795e3e | ||
|
|
8bdd3a2d2a | ||
| 8d2b3094c6 | |||
| 26ac4da0ad | |||
| fdfe79fe36 | |||
| 6090fb255a | |||
| 3b501d304b | |||
| b247e1873d | |||
| 038334630e | |||
| 3fd3f9dba0 | |||
| 2ffdba83cc |
@@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Read(//Users/break/.agents/skills/impeccable/**)",
|
||||||
|
"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": []
|
||||||
|
|||||||
30
.impeccable.md
Normal file
30
.impeccable.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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.
|
||||||
569
App.vue
569
App.vue
@@ -8,85 +8,86 @@
|
|||||||
@import "uview-plus/index.scss";
|
@import "uview-plus/index.scss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* 主色 */
|
/* 主色 - iOS Blue 家族 */
|
||||||
--primary: #0A84FF;
|
--primary: #007AFF;
|
||||||
--primary-light: #5AC8FA;
|
--primary-light: #5AC8FA;
|
||||||
--primary-dark: #0066CC;
|
--primary-dark: #0056CC;
|
||||||
|
--primary-tint: rgba(0, 122, 255, 0.08);
|
||||||
|
|
||||||
/* 辅助色 */
|
/* 语义色 */
|
||||||
--success: #30D158;
|
--success: #34C759;
|
||||||
--success-light: rgba(48, 209, 88, 0.12);
|
--success-light: rgba(52, 199, 89, 0.1);
|
||||||
--warning: #FF9F0A;
|
--warning: #FF9500;
|
||||||
--warning-light: rgba(255, 159, 10, 0.12);
|
--warning-light: rgba(255, 149, 0, 0.1);
|
||||||
--danger: #FF453A;
|
--danger: #FF3B30;
|
||||||
--danger-light: rgba(255, 69, 58, 0.12);
|
--danger-light: rgba(255, 59, 48, 0.1);
|
||||||
--info: #5E5CE6;
|
--info: #5856D6;
|
||||||
|
|
||||||
/* 扩展色 */
|
/* 中性色 - 微妙的蓝色底色 */
|
||||||
--purple: #BF5AF2;
|
--gray-50: #F8F9FC;
|
||||||
--pink: #FF375F;
|
--gray-100: #EEF1F5;
|
||||||
--teal: #64D2FF;
|
--gray-200: #D9DDE6;
|
||||||
--indigo: #5E5CE6;
|
--gray-300: #B8C0D0;
|
||||||
|
--gray-400: #8E95A8;
|
||||||
/* 中性色 - 灰阶 */
|
--gray-500: #6B7280;
|
||||||
--gray-100: #F2F2F7;
|
--gray-600: #4B5563;
|
||||||
--gray-200: #E5E5EA;
|
--gray-700: #374151;
|
||||||
--gray-300: #D1D1D6;
|
--gray-800: #1F2937;
|
||||||
--gray-400: #C7C7CC;
|
--gray-900: #111827;
|
||||||
--gray-500: #AEAEB2;
|
|
||||||
--gray-600: #8E8E93;
|
|
||||||
--gray-700: #636366;
|
|
||||||
--gray-800: #3C3C43;
|
|
||||||
--gray-900: #000000;
|
|
||||||
|
|
||||||
/* 背景色 */
|
|
||||||
--bg-primary: #FFFFFF;
|
|
||||||
--bg-secondary: #F2F2F7;
|
|
||||||
--bg-tertiary: #FFFFFF;
|
|
||||||
--bg-grouped: #F2F2F7;
|
|
||||||
|
|
||||||
/* 文字色 */
|
/* 文字色 */
|
||||||
--text-primary: #000000;
|
--text-primary: var(--gray-900);
|
||||||
--text-secondary: #3C3C43;
|
--text-secondary: var(--gray-600);
|
||||||
--text-tertiary: rgba(60, 60, 67, 0.6);
|
--text-tertiary: var(--gray-400);
|
||||||
--text-quaternary: rgba(60, 60, 67, 0.3);
|
|
||||||
--text-inverse: #FFFFFF;
|
--text-inverse: #FFFFFF;
|
||||||
|
|
||||||
/* 阴影 */
|
/* 背景色 */
|
||||||
--shadow-small: 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
|
--bg-base: #F5F7FA;
|
||||||
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
--bg-surface: #FFFFFF;
|
||||||
--shadow-large: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
--bg-elevated: #FFFFFF;
|
||||||
--shadow-extra-large: 0 16rpx 48rpx rgba(0, 0, 0, 0.16);
|
|
||||||
|
/* 阴影 - 更细腻 */
|
||||||
|
--shadow-xs: 0 1rpx 2rpx rgba(0, 0, 0, 0.04);
|
||||||
|
--shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.10);
|
||||||
|
--shadow-xl: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
/* 圆角 */
|
/* 圆角 */
|
||||||
--radius-small: 8rpx;
|
--radius-xs: 6rpx;
|
||||||
--radius-medium: 16rpx;
|
--radius-sm: 10rpx;
|
||||||
--radius-large: 24rpx;
|
--radius-md: 16rpx;
|
||||||
--radius-extra-large: 32rpx;
|
--radius-lg: 24rpx;
|
||||||
|
--radius-xl: 32rpx;
|
||||||
--radius-full: 9999rpx;
|
--radius-full: 9999rpx;
|
||||||
|
|
||||||
/* 间距 */
|
/* 间距 - 4pt 基准 */
|
||||||
--space-xs: 8rpx;
|
--space-1: 4rpx;
|
||||||
--space-sm: 16rpx;
|
--space-2: 8rpx;
|
||||||
--space-md: 24rpx;
|
--space-3: 12rpx;
|
||||||
--space-lg: 32rpx;
|
--space-4: 16rpx;
|
||||||
--space-xl: 40rpx;
|
--space-5: 20rpx;
|
||||||
--space-2xl: 48rpx;
|
--space-6: 24rpx;
|
||||||
|
--space-8: 32rpx;
|
||||||
|
--space-10: 40rpx;
|
||||||
|
--space-12: 48rpx;
|
||||||
|
--space-16: 64rpx;
|
||||||
|
|
||||||
/* 过渡 */
|
/* 过渡 */
|
||||||
--transition-fast: 0.2s ease-out;
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
--transition-normal: 0.3s ease-out;
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
--transition-slow: 0.5s ease-out;
|
--duration-fast: 0.15s;
|
||||||
|
--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;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "MiSans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
box-sizing: border-box;
|
||||||
|
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 {
|
||||||
@@ -95,29 +96,31 @@
|
|||||||
|
|
||||||
page {
|
page {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-base);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20rpx;
|
gap: var(--space-4);
|
||||||
padding: 30rpx 20rpx;
|
padding: var(--space-5) var(--space-4);
|
||||||
max-width: 750rpx;
|
max-width: 750rpx;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-surface);
|
||||||
border-radius: var(--radius-medium);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-md);
|
padding: var(--space-5);
|
||||||
border: 1rpx solid var(--border-color);
|
box-shadow: var(--shadow-sm);
|
||||||
transition: all var(--transition-fast);
|
transition: box-shadow var(--duration-normal) var(--ease-out),
|
||||||
|
transform var(--duration-normal) var(--ease-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-elevated {
|
.card-elevated {
|
||||||
box-shadow: var(--shadow-large);
|
box-shadow: var(--shadow-lg);
|
||||||
transform: translateY(-4rpx);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-interactive {
|
.card-interactive {
|
||||||
@@ -126,103 +129,120 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
/* 排版系统 */
|
||||||
|
.text-display {
|
||||||
|
font-size: 48rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 34rpx;
|
line-height: 1.1;
|
||||||
line-height: 1.2;
|
letter-spacing: -0.03em;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.text-title {
|
||||||
|
font-size: 36rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 28rpx;
|
line-height: 1.2;
|
||||||
line-height: 1.3;
|
letter-spacing: -0.02em;
|
||||||
color: var(--text-secondary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.text-heading {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body {
|
||||||
|
font-size: 28rpx;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 28rpx;
|
line-height: 1.5;
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.caption {
|
.text-caption {
|
||||||
font-weight: 500;
|
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.3;
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-xs { margin-top: var(--space-xs); }
|
.text-label {
|
||||||
.mt-sm { margin-top: var(--space-sm); }
|
|
||||||
.mt-md { margin-top: var(--space-md); }
|
|
||||||
.mt-lg { margin-top: var(--space-lg); }
|
|
||||||
.mt-xl { margin-top: var(--space-xl); }
|
|
||||||
.mt-30 { margin-top: 30rpx; }
|
|
||||||
.mt-20 { margin-top: 20rpx; }
|
|
||||||
|
|
||||||
.mb-xs { margin-bottom: var(--space-xs); }
|
|
||||||
.mb-sm { margin-bottom: var(--space-sm); }
|
|
||||||
.mb-md { margin-bottom: var(--space-md); }
|
|
||||||
.mb-lg { margin-bottom: var(--space-lg); }
|
|
||||||
.mb-xl { margin-bottom: var(--space-xl); }
|
|
||||||
|
|
||||||
.p-xs { padding: var(--space-xs); }
|
|
||||||
.p-sm { padding: var(--space-sm); }
|
|
||||||
.p-md { padding: var(--space-md); }
|
|
||||||
.p-lg { padding: var(--space-lg); }
|
|
||||||
.p-xl { padding: var(--space-xl); }
|
|
||||||
|
|
||||||
.flex-row { display: flex; align-items: center; }
|
|
||||||
.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-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 { display: flex; flex-direction: column; }
|
|
||||||
.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-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%; }
|
|
||||||
.h-100 { height: 100%; }
|
|
||||||
|
|
||||||
.interactive {
|
|
||||||
transition: all var(--transition-normal);
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-apple {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-medium);
|
|
||||||
padding: var(--space-sm) var(--space-md);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 28rpx;
|
|
||||||
transition: all var(--transition-normal);
|
|
||||||
cursor: pointer;
|
|
||||||
-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-size: 22rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
letter-spacing: 0.02em;
|
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 {
|
&.tag-success {
|
||||||
background: var(--success-light);
|
background: var(--success-light);
|
||||||
@@ -230,7 +250,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.tag-primary {
|
&.tag-primary {
|
||||||
background: rgba(10, 132, 255, 0.12);
|
background: var(--primary-tint);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,36 +265,207 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-apple {
|
/* 进度条 */
|
||||||
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8rpx;
|
height: 10rpx;
|
||||||
background: var(--gray-200);
|
background: var(--gray-100);
|
||||||
border-radius: var(--radius-small);
|
border-radius: var(--radius-full);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, var(--primary), var(--primary-light));
|
background: var(--primary);
|
||||||
border-radius: var(--radius-small);
|
border-radius: var(--radius-full);
|
||||||
transition: width var(--transition-normal);
|
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;
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&.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 {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-group-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兼容旧类名 */
|
||||||
|
.mt-xs { margin-top: var(--space-2); }
|
||||||
|
.mt-sm { margin-top: var(--space-4); }
|
||||||
|
.mt-md { margin-top: var(--space-6); }
|
||||||
|
.mt-lg { margin-top: var(--space-8); }
|
||||||
|
.mt-xl { margin-top: var(--space-10); }
|
||||||
|
.mt-30 { margin-top: 30rpx; }
|
||||||
|
.mt-20 { margin-top: 20rpx; }
|
||||||
|
|
||||||
|
.mb-xs { margin-bottom: var(--space-2); }
|
||||||
|
.mb-sm { margin-bottom: var(--space-4); }
|
||||||
|
.mb-md { margin-bottom: var(--space-6); }
|
||||||
|
.mb-lg { margin-bottom: var(--space-8); }
|
||||||
|
.mb-xl { margin-bottom: var(--space-10); }
|
||||||
|
|
||||||
|
.p-xs { padding: var(--space-2); }
|
||||||
|
.p-sm { padding: var(--space-4); }
|
||||||
|
.p-md { padding: var(--space-6); }
|
||||||
|
.p-lg { padding: var(--space-8); }
|
||||||
|
.p-xl { padding: var(--space-10); }
|
||||||
|
|
||||||
|
.flex-row-g8 { display: flex; align-items: center; gap: var(--space-2); }
|
||||||
|
.flex-row-g16 { display: flex; align-items: center; gap: var(--space-4); }
|
||||||
|
.flex-row-g20 { display: flex; align-items: center; gap: 20rpx; }
|
||||||
|
.flex-row-g24 { display: flex; align-items: center; gap: var(--space-6); }
|
||||||
|
|
||||||
|
.flex-col-g8 { display: flex; flex-direction: column; gap: var(--space-2); }
|
||||||
|
.flex-col-g16 { display: flex; flex-direction: column; gap: var(--space-4); }
|
||||||
|
.flex-col-g20 { display: flex; flex-direction: column; gap: 20rpx; }
|
||||||
|
.flex-col-g24 { display: flex; flex-direction: column; gap: var(--space-6); }
|
||||||
|
|
||||||
|
/* 背景色 */
|
||||||
|
.bg-primary { background-color: var(--bg-surface); }
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
/* 文字色 */
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
/* 旧标签兼容 */
|
||||||
|
.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 {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 28rpx;
|
||||||
|
transition: all var(--duration-normal) var(--ease-out);
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.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-small);
|
border-radius: var(--radius-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all var(--duration-fast) var(--ease-out);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
padding: 20rpx;
|
padding: var(--space-5);
|
||||||
background: var(--gray-100);
|
background: var(--gray-100);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
@@ -299,56 +490,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
background: var(--gray-300);
|
background: var(--gray-200);
|
||||||
color: var(--text-secondary);
|
color: var(--gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-mini {
|
&.btn-mini {
|
||||||
min-height: 40rpx;
|
min-height: 52rpx;
|
||||||
padding: 0 16rpx;
|
padding: 0 var(--space-4);
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
border-radius: 6rpx;
|
border-radius: var(--radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-large {
|
&.btn-large {
|
||||||
min-height: 64rpx;
|
min-height: 88rpx;
|
||||||
padding: 0 32rpx;
|
padding: 0 var(--space-8);
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
border-radius: var(--radius-medium);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-disabled {
|
&.btn-disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group {
|
/* 旧进度条兼容 */
|
||||||
display: flex;
|
.progress-apple {
|
||||||
gap: 12rpx;
|
width: 100%;
|
||||||
|
height: 10rpx;
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.btn-apple {
|
.progress-fill {
|
||||||
flex: 1;
|
height: 100%;
|
||||||
}
|
background: var(--primary);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
&.btn-group-vertical {
|
transition: width var(--duration-slow) var(--ease-out);
|
||||||
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>
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ RUN npm config set registry https://registry.npmmirror.com
|
|||||||
# 复制 package.json(利用 Docker 缓存)
|
# 复制 package.json(利用 Docker 缓存)
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
|
||||||
# 使用 uvm 自动安装最新兼容的 @dcloudio 包版本
|
|
||||||
RUN npx @dcloudio/uvm@latest
|
|
||||||
|
|
||||||
# 安装所有依赖
|
# 安装所有依赖
|
||||||
RUN npm install --legacy-peer-deps
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
@@ -48,6 +45,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
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
1
MP_verify_yU2Z8mp831jh6QX7.txt
Normal file
1
MP_verify_yU2Z8mp831jh6QX7.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
yU2Z8mp831jh6QX7
|
||||||
@@ -3,10 +3,14 @@ import { APP_TYPE } from '@/utils/env.js';
|
|||||||
|
|
||||||
export const orderApi = {
|
export const orderApi = {
|
||||||
getList(identifier, page, page_size, payment_status) {
|
getList(identifier, page, page_size, payment_status) {
|
||||||
|
const data = { identifier, page, page_size };
|
||||||
|
if (payment_status !== undefined && payment_status !== null) {
|
||||||
|
data.payment_status = payment_status;
|
||||||
|
}
|
||||||
return request({
|
return request({
|
||||||
url: '/api/c/v1/orders',
|
url: '/api/c/v1/orders',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: { identifier, page, page_size, payment_status }
|
data
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -23,5 +27,20 @@ export const orderApi = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { identifier, package_ids, app_type: APP_TYPE }
|
data: { identifier, package_ids, app_type: APP_TYPE }
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pay(order_id, payment_method, password) {
|
||||||
|
const data = {
|
||||||
|
payment_method,
|
||||||
|
app_type: APP_TYPE
|
||||||
|
};
|
||||||
|
if (password) {
|
||||||
|
data.password = password;
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: `/api/c/v1/orders/${order_id}/pay`,
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -34,6 +34,24 @@ export const walletApi = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 新的充值订单接口
|
||||||
|
getRechargeOrders(identifier, page, page_size, status) {
|
||||||
|
return request({
|
||||||
|
url: '/api/c/v1/recharge-orders',
|
||||||
|
method: 'GET',
|
||||||
|
data: { identifier, page, page_size, status }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取充值订单详情
|
||||||
|
getRechargeOrderDetail(identifier, rechargeOrderId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/c/v1/recharge-orders/${rechargeOrderId}`,
|
||||||
|
method: 'GET',
|
||||||
|
data: { identifier }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getTransactions(identifier, page, page_size, params = {}) {
|
getTransactions(identifier, page, page_size, params = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/c/v1/wallet/transactions',
|
url: '/api/c/v1/wallet/transactions',
|
||||||
|
|||||||
@@ -1,48 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card device-status-card">
|
<view class="device-status-card">
|
||||||
<view class="card-header mb-md flex-row-sb">
|
<view class="card-header">
|
||||||
<view class="title">设备信息</view>
|
<text class="text-heading">设备信息</text>
|
||||||
<view class="tag-apple tag-success">{{ getOperator(deviceInfo.currentIccid) }}</view>
|
<view class="operator-badge">
|
||||||
</view>
|
<text class="operator-name">中国电信</text>
|
||||||
<view class="device-info flex-col-g16">
|
|
||||||
<view class="info-row flex-row-sb">
|
|
||||||
<view class="info-label">
|
|
||||||
<view class="subtitle">当前卡: {{ deviceInfo.currentIccid }}</view>
|
|
||||||
</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="info-row flex-row-sb">
|
</view>
|
||||||
<view class="info-label">
|
|
||||||
<view class="subtitle">到期时间:{{ deviceInfo.expireDate }}</view>
|
<view class="status-row">
|
||||||
</view>
|
<view class="status-item">
|
||||||
<view class="info-values flex-row-g20">
|
<text class="status-label">当前卡</text>
|
||||||
<view class="tag-apple tag-primary">{{ deviceInfo.statusStr }}</view>
|
<text class="status-value iccid">{{ deviceInfo.currentIccid }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-item">
|
||||||
|
<text class="status-label">实名状态</text>
|
||||||
|
<view class="auth-badge" :class="isRealName ? 'auth-done' : 'auth-pending'" @tap="$emit('authentication')">
|
||||||
|
<text>{{ isRealName ? '已实名' : '未实名' }}</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="device-metrics flex-row-sb mt-md">
|
|
||||||
<view class="metric-item flex-col-center">
|
<view class="status-row">
|
||||||
<view class="caption mb-xs">信号强度</view>
|
<view class="status-item full-width">
|
||||||
<view class="metric-value">{{ getSignalText(deviceInfo.rssi) }}</view>
|
<text class="status-label">到期时间</text>
|
||||||
|
<text class="status-value expire">{{ deviceInfo.expireDate }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="metric-item flex-col-center">
|
</view>
|
||||||
<view class="caption mb-xs">设备电量</view>
|
|
||||||
<view class="metric-value">{{ deviceInfo.battery }} %</view>
|
<view class="metrics-grid">
|
||||||
|
<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-item flex-col-center">
|
<view class="metric-card">
|
||||||
<view class="caption mb-xs">连接数量</view>
|
<view class="metric-icon battery">
|
||||||
<view class="metric-value">{{ deviceInfo.connCnt }} 台</view>
|
<text class="metric-icon-text">🔋</text>
|
||||||
|
</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>
|
||||||
defineProps({
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
deviceInfo: { type: Object, default: () => ({}) },
|
deviceInfo: { type: Object, default: () => ({}) },
|
||||||
isRealName: { type: Boolean, default: false },
|
isRealName: { type: Boolean, default: false },
|
||||||
opratorList: { type: Array, default: () => [] }
|
opratorList: { type: Array, default: () => [] }
|
||||||
@@ -50,27 +66,152 @@
|
|||||||
|
|
||||||
defineEmits(['authentication']);
|
defineEmits(['authentication']);
|
||||||
|
|
||||||
const getOperator = (iccid) => {
|
|
||||||
return '中国电信';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSignalText = (rssi) => rssi || '强';
|
const getSignalText = (rssi) => rssi || '强';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.device-status-card {
|
.device-status-card {
|
||||||
.info-row {
|
background: var(--bg-surface);
|
||||||
padding: var(--space-sm) 0;
|
border-radius: var(--radius-xl);
|
||||||
border-bottom: 1rpx solid var(--gray-200);
|
padding: var(--space-5);
|
||||||
&:last-child { border-bottom: none; }
|
box-shadow: var(--shadow-sm);
|
||||||
.info-label { min-width: 200rpx; }
|
}
|
||||||
.info-values { flex: 1; justify-content: flex-end; }
|
|
||||||
}
|
.card-header {
|
||||||
.device-metrics {
|
display: flex;
|
||||||
background: var(--gray-100);
|
align-items: center;
|
||||||
border-radius: var(--radius-small);
|
justify-content: space-between;
|
||||||
padding: 20rpx;
|
padding-bottom: var(--space-4);
|
||||||
.metric-item { flex: 1; }
|
border-bottom: 1rpx solid var(--gray-100);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator-badge {
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
background: var(--primary-tint);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator-name {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
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 {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="floating-service-card" @click="handleClick">
|
<view class="service-card" @click="handleClick">
|
||||||
<view class="service-left">
|
<view class="service-inner">
|
||||||
<image class="service-icon" src="/static/link.png" mode="aspectFit"></image>
|
<view class="service-left">
|
||||||
<view class="service-content">
|
<view class="service-icon-wrap">
|
||||||
<text class="service-title">需要帮助?</text>
|
<image class="service-icon" src="/static/link.png" mode="aspectFit"></image>
|
||||||
<text class="service-desc">专属客服在线解答</text>
|
</view>
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -20,56 +24,71 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.floating-service-card {
|
.service-card {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20rpx;
|
bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
|
||||||
left: 20rpx;
|
left: var(--space-4);
|
||||||
right: 20rpx;
|
right: var(--space-4);
|
||||||
height: 120rpx;
|
z-index: 999;
|
||||||
background: #fff;
|
}
|
||||||
border-radius: 60rpx;
|
|
||||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
.service-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 24rpx 0 24rpx;
|
padding: var(--space-4) var(--space-5);
|
||||||
z-index: 1000;
|
background: var(--bg-surface);
|
||||||
|
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: 20rpx;
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.service-icon {
|
.service-icon-wrap {
|
||||||
width: 60rpx;
|
width: 80rpx;
|
||||||
height: 60rpx;
|
height: 80rpx;
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--primary-tint);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
.service-content {
|
.service-icon {
|
||||||
display: flex;
|
width: 44rpx;
|
||||||
flex-direction: column;
|
height: 44rpx;
|
||||||
gap: 4rpx;
|
}
|
||||||
|
|
||||||
.service-title {
|
.service-content {
|
||||||
font-size: 28rpx;
|
display: flex;
|
||||||
font-weight: 600;
|
flex-direction: column;
|
||||||
color: var(--text-primary);
|
gap: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-desc {
|
.service-title {
|
||||||
font-size: 22rpx;
|
font-size: 28rpx;
|
||||||
color: var(--text-tertiary);
|
font-weight: 600;
|
||||||
}
|
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: 16rpx 32rpx;
|
padding: var(--space-3) var(--space-5);
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
background: var(--primary);
|
||||||
color: #fff;
|
color: var(--text-inverse);
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 40rpx;
|
border-radius: var(--radius-full);
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,91 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card function-card">
|
<view class="card function-card">
|
||||||
<view class="card-header">
|
<view class="card-header mb-5">
|
||||||
<view class="title">功能菜单</view>
|
<text class="text-heading">功能服务</text>
|
||||||
<view class="header-actions">
|
|
||||||
<button class="btn-apple btn-primary btn-mini" @tap="$emit('sync')">运营商数据同步</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="function-grid">
|
<view class="function-grid">
|
||||||
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'authentication')" role="button" tabindex="0">
|
<view
|
||||||
<view class="function-icon">
|
v-for="item in displayFunctions"
|
||||||
<image src="/static/authentication.png" mode="aspectFit" alt="实名认证"></image>
|
:key="item.key"
|
||||||
|
class="function-item interactive"
|
||||||
|
: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>
|
||||||
<view :class="['function-name', realNameStatus === '已实名' ? 'text-primary' : '']">
|
<text class="function-name" :class="{ 'text-primary-color': item.highlight }">{{ item.name }}</text>
|
||||||
{{ realNameStatus || '实名认证' }}
|
|
||||||
</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" v-if="isDevice" @tap="$emit('enter', 'asset-package')" 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', 'bind')" role="button" tabindex="0">
|
|
||||||
<view class="function-icon">
|
|
||||||
<image src="/static/bind-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', '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" v-if="isDevice" @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>
|
||||||
|
|
||||||
@@ -94,50 +25,114 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { computed } from 'vue';
|
||||||
realNameStatus: { type: String, default: '' },
|
|
||||||
alreadyBindPhone: { type: Boolean, default: false },
|
const props = defineProps({
|
||||||
isDevice: { type: Boolean, default: true }
|
realNameStatus: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
alreadyBindPhone: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isDevice: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['enter', 'sync']);
|
const emit = defineEmits(['enter', 'sync']);
|
||||||
|
|
||||||
|
const functions = computed(() => {
|
||||||
|
const list = [
|
||||||
|
{ key: 'authentication', name: props.realNameStatus || '实名认证', icon: '/static/authentication.png', action: 'authentication', highlight: props.realNameStatus === '已实名' },
|
||||||
|
{ key: 'sync', name: '运营数据同步', icon: '/static/data.png', action: 'sync' },
|
||||||
|
{ 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.function-card {
|
.function-card {
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
padding-bottom: var(--space-4);
|
||||||
justify-content: space-between;
|
border-bottom: 1rpx solid var(--gray-100);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.text-primary { color: var(--primary) !important; }
|
|
||||||
|
.function-grid {
|
||||||
|
display: grid;
|
||||||
|
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>
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card traffic-card">
|
<view class="traffic-card">
|
||||||
<view class="card-header flex-row-sb mb-lg">
|
<view class="card-header">
|
||||||
<view class="title">本月流量</view>
|
<text class="text-heading">本月流量</text>
|
||||||
<view class="usage-percent">
|
<view class="usage-indicator">
|
||||||
<text class="title">{{ usedFlowPercent }}</text>
|
<text class="usage-value">{{ usedFlowPercent }}</text>
|
||||||
<text class="caption">%</text>
|
<text class="usage-unit">%</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="progress-section mb-lg">
|
|
||||||
<view class="progress-apple">
|
<view class="progress-section">
|
||||||
<view class="progress-fill" :style="{width: usedFlowPercent + '%'}"></view>
|
<view class="progress-track">
|
||||||
|
<view class="progress-fill" :style="{ width: usedFlowPercent + '%' }"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="traffic-stats flex-row-sb">
|
|
||||||
<view class="stat-item flex-col-center">
|
<view class="traffic-stats">
|
||||||
<view class="caption mb-xs">已使用</view>
|
<view class="stat-item">
|
||||||
<view class="stat-value-container">
|
<text class="stat-label">已使用</text>
|
||||||
<text class="stat-value">{{ formatDataSize(deviceInfo.totalBytesCnt) }}</text>
|
<view class="stat-value-wrap">
|
||||||
<text class="stat-unit">{{ isDevice ? 'GB' : 'MB' }}</text>
|
<text class="stat-value">{{ usedValue }}</text>
|
||||||
|
<text class="stat-unit">{{ usedUnit }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-item flex-col-center">
|
<view class="stat-divider"></view>
|
||||||
<view class="caption mb-xs">总流量</view>
|
<view class="stat-item">
|
||||||
<view class="stat-value-container">
|
<text class="stat-label">总流量</text>
|
||||||
<text class="stat-value">{{ formatDataSize(deviceInfo.flowSize) }}</text>
|
<view class="stat-value-wrap">
|
||||||
<text class="stat-unit">{{ isDevice ? 'GB' : 'MB' }}</text>
|
<text class="stat-value">{{ totalValue }}</text>
|
||||||
|
<text class="stat-unit">{{ totalUnit }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-item flex-col-center">
|
<view class="stat-divider"></view>
|
||||||
<view class="caption mb-xs">剩余</view>
|
<view class="stat-item">
|
||||||
<view class="stat-value-container">
|
<text class="stat-label">剩余</text>
|
||||||
<text class="stat-value">{{ formatDataSize(deviceInfo.flowSize - deviceInfo.totalBytesCnt) }}</text>
|
<view class="stat-value-wrap">
|
||||||
<text class="stat-unit">{{ isDevice ? 'GB' : 'MB' }}</text>
|
<text class="stat-value remain">{{ remainValue }}</text>
|
||||||
|
<text class="stat-unit">{{ remainUnit }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -39,42 +43,210 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { assetApi } from '@/api/index.js';
|
||||||
|
import { useUserStore } from '@/store/index.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
deviceInfo: { type: Object, default: () => ({}) },
|
identifier: { type: String, default: '' }
|
||||||
isDevice: { type: Boolean, default: true }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const usedFlowPercent = computed(() => {
|
const emit = defineEmits(['packageLoaded']);
|
||||||
if (props.deviceInfo.flowSize && props.deviceInfo.totalBytesCnt) {
|
|
||||||
return (props.deviceInfo.totalBytesCnt / props.deviceInfo.flowSize * 100).toFixed(2);
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 流量数据
|
||||||
|
const usedMB = ref(0);
|
||||||
|
const totalMB = ref(0);
|
||||||
|
const remainMB = ref(0);
|
||||||
|
|
||||||
|
// 格式化流量数据(MB 转 GB)
|
||||||
|
const formatTraffic = (mb) => {
|
||||||
|
if (!mb || mb === 0) {
|
||||||
|
return { value: '0.00', unit: 'MB' };
|
||||||
}
|
}
|
||||||
return '0';
|
|
||||||
|
if (mb >= 1024) {
|
||||||
|
return {
|
||||||
|
value: (mb / 1024).toFixed(2),
|
||||||
|
unit: 'GB'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: mb.toFixed(2),
|
||||||
|
unit: 'MB'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 已使用流量
|
||||||
|
const usedValue = computed(() => formatTraffic(usedMB.value).value);
|
||||||
|
const usedUnit = computed(() => formatTraffic(usedMB.value).unit);
|
||||||
|
|
||||||
|
// 总流量
|
||||||
|
const totalValue = computed(() => formatTraffic(totalMB.value).value);
|
||||||
|
const totalUnit = computed(() => formatTraffic(totalMB.value).unit);
|
||||||
|
|
||||||
|
// 剩余流量
|
||||||
|
const remainValue = computed(() => formatTraffic(remainMB.value).value);
|
||||||
|
const remainUnit = computed(() => formatTraffic(remainMB.value).unit);
|
||||||
|
|
||||||
|
// 使用百分比
|
||||||
|
const usedFlowPercent = computed(() => {
|
||||||
|
if (totalMB.value && usedMB.value) {
|
||||||
|
const percent = (usedMB.value / totalMB.value * 100);
|
||||||
|
return Math.min(percent, 100).toFixed(1);
|
||||||
|
}
|
||||||
|
return '0.0';
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDataSize = (size) => {
|
// 加载套餐流量数据
|
||||||
if (!size) return '0';
|
const loadPackageData = async () => {
|
||||||
const num = parseFloat(size);
|
const identifier = props.identifier || userStore.state.identifier;
|
||||||
if (num < 0.01) return '0';
|
if (!identifier) return;
|
||||||
return num.toFixed(2);
|
|
||||||
|
try {
|
||||||
|
const data = await assetApi.getPackageHistory(identifier, 1, 10, { status: 1 });
|
||||||
|
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
// 只有一个生效中的套餐
|
||||||
|
const activePackage = data.items[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 {
|
||||||
|
// 未启用虚流量,使用真流量
|
||||||
|
usedMB.value = activePackage.data_usage_mb || 0;
|
||||||
|
totalMB.value = activePackage.data_limit_mb || 0;
|
||||||
|
remainMB.value = totalMB.value - usedMB.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将套餐信息(包括到期时间)传递给父组件
|
||||||
|
emit('packageLoaded', activePackage);
|
||||||
|
} else {
|
||||||
|
// 没有生效中的套餐
|
||||||
|
usedMB.value = 0;
|
||||||
|
totalMB.value = 0;
|
||||||
|
remainMB.value = 0;
|
||||||
|
emit('packageLoaded', null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载套餐流量数据失败', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听 identifier 变化
|
||||||
|
watch(() => props.identifier || userStore.state.identifier, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
loadPackageData();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.traffic-card {
|
.traffic-card {
|
||||||
.usage-percent {
|
background: var(--bg-surface);
|
||||||
display: flex;
|
border-radius: var(--radius-xl);
|
||||||
align-items: baseline;
|
padding: var(--space-5);
|
||||||
gap: 4rpx;
|
box-shadow: var(--shadow-sm);
|
||||||
.title { font-size: 35rpx; font-weight: 700; margin-right: 5rpx; }
|
}
|
||||||
}
|
|
||||||
.traffic-stats {
|
.card-header {
|
||||||
background: var(--gray-100);
|
display: flex;
|
||||||
border-radius: var(--radius-small);
|
align-items: center;
|
||||||
padding: 20rpx;
|
justify-content: space-between;
|
||||||
margin-top: 16rpx;
|
margin-bottom: var(--space-5);
|
||||||
.stat-item { flex: 1; }
|
}
|
||||||
|
|
||||||
|
.usage-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-value {
|
||||||
|
font-size: 48rpx;
|
||||||
|
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>
|
||||||
@@ -1,41 +1,166 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card user-info-card interactive">
|
<view class="user-info-card">
|
||||||
<view class="flex-row-g20">
|
<view class="card-main">
|
||||||
<view class="user-avatar">
|
<view class="user-avatar">
|
||||||
<image src="https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500" mode="aspectFill" alt="用户头像" />
|
<image :src="avatarUrl" mode="aspectFill" alt="用户头像" />
|
||||||
</view>
|
</view>
|
||||||
<view class="user-details flex-col-g8">
|
<view class="user-details">
|
||||||
<view class="flex-row-g20">
|
<view class="user-id">
|
||||||
<view class="title">{{ currentCardNo }}</view>
|
<text class="device-id">{{ currentCardNo || '-' }}</text>
|
||||||
|
<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 class="tag-apple" :class="onlineStatus === '在线' ? 'tag-success' : 'tag-warning'">
|
</view>
|
||||||
{{ onlineStatus }}
|
<view class="card-footer" v-if="!isDevice">
|
||||||
|
<view class="network-badge" :class="networkStatus == 1 ? 'network-ok' : 'network-stop'">
|
||||||
|
{{ networkStatus == 1 ? '正常' : '停机' }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { computed } from 'vue';
|
||||||
|
import { useUserStore } from '@/store/index.js';
|
||||||
|
|
||||||
|
const props = 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: '离线' },
|
||||||
|
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 avatarUrl = computed(() => {
|
||||||
|
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 {
|
||||||
color: var(--text-primary);
|
background: var(--bg-surface);
|
||||||
.user-avatar {
|
border-radius: var(--radius-xl);
|
||||||
width: 80rpx;
|
padding: var(--space-5);
|
||||||
height: 80rpx;
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: var(--radius-medium);
|
}
|
||||||
overflow: hidden;
|
|
||||||
border: 2rpx solid var(--border-light);
|
.card-main {
|
||||||
image { width: 100%; height: 100%; object-fit: cover; }
|
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; }
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
@@ -1,25 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card wifi-card">
|
<view class="wifi-card">
|
||||||
<view class="card-header flex-row-sb">
|
<view class="card-header">
|
||||||
<view class="title">WiFi配置</view>
|
<text class="text-heading">WiFi配置</text>
|
||||||
<view class="btn-group">
|
<button class="btn btn-mini btn-primary" @tap="$emit('modify')">修改</button>
|
||||||
<button class="btn-apple btn-primary btn-mini" @tap="$emit('modify')">修改</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="wifi-info flex-col-g16 mt-30">
|
<view class="wifi-list">
|
||||||
<view class="wifi-item flex-row-sb">
|
<view class="wifi-item">
|
||||||
<view class="wifi-details flex-col-g8">
|
<view class="wifi-icon-wrap">
|
||||||
<view class="caption">网络名称</view>
|
<text class="wifi-icon-text">📶</text>
|
||||||
<view class="subtitle">{{ deviceInfo.ssidName }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<button class="btn-apple btn-primary btn-mini" @tap="$emit('copy', deviceInfo.ssidName)">复制</button>
|
<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 flex-row-sb">
|
<view class="wifi-item">
|
||||||
<view class="wifi-details flex-col-g8">
|
<view class="wifi-icon-wrap">
|
||||||
<view class="caption">连接密码</view>
|
<text class="wifi-icon-text">🔑</text>
|
||||||
<view class="subtitle">{{ deviceInfo.ssidPwd }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<button class="btn-apple btn-primary btn-mini" @tap="$emit('copy', deviceInfo.ssidPwd)">复制</button>
|
<view class="wifi-content">
|
||||||
|
<text class="wifi-label">连接密码</text>
|
||||||
|
<text class="wifi-value">{{ deviceInfo.ssidPwd }}</text>
|
||||||
|
</view>
|
||||||
|
<button class="btn-copy" @tap="$emit('copy', deviceInfo.ssidPwd)">复制</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -35,13 +39,86 @@
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wifi-card {
|
.wifi-card {
|
||||||
.wifi-item {
|
background: var(--bg-surface);
|
||||||
padding: var(--space-md);
|
border-radius: var(--radius-xl);
|
||||||
background: var(--gray-100);
|
padding: var(--space-5);
|
||||||
border-radius: var(--radius-medium);
|
box-shadow: var(--shadow-sm);
|
||||||
margin-bottom: var(--space-sm);
|
}
|
||||||
&:last-child { margin-bottom: 0; }
|
|
||||||
.wifi-details { flex: 1; }
|
.card-header {
|
||||||
|
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>
|
</style>
|
||||||
@@ -408,6 +408,7 @@ GET /api/c/v1/asset/package-history?identifier=1234567890&page=1&page_size=10
|
|||||||
"package_usage_id": 5001,
|
"package_usage_id": 5001,
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
|
"enable_virtual_data": true,
|
||||||
"status_name": "生效中",
|
"status_name": "生效中",
|
||||||
"usage_type": "single_card",
|
"usage_type": "single_card",
|
||||||
"virtual_limit_mb": 20480,
|
"virtual_limit_mb": 20480,
|
||||||
@@ -448,7 +449,11 @@ data.items[]:
|
|||||||
- virtual_ratio:虚流量比例(real/virtual)
|
- virtual_ratio:虚流量比例(real/virtual)
|
||||||
- virtual_remain_mb:剩余虚流量(MB),按virtual_ratio换算
|
- virtual_remain_mb:剩余虚流量(MB),按virtual_ratio换算
|
||||||
- virtual_used_mb:已用虚流量(MB),按virtual_ratio换算
|
- virtual_used_mb:已用虚流量(MB),按virtual_ratio换算
|
||||||
|
- enable_virtual_data:是否启用虚流量
|
||||||
|
* 如果启用了虚流量那么 本月流量展示的就是 套餐虚流量总量 剩余虚流量 已用虚流量 只是值用这些,已使用,总流量,剩余不要改
|
||||||
|
* 如果没有启用那就是 套餐真流量总量 已用真流量 剩余真流量(自己计算) 然后如果大于或者等于1024MB 转换成 GB
|
||||||
|
* 本月流量哪里需要调用这个接口,带上参数status: 1:生效中 只会有一个生效的
|
||||||
|
*
|
||||||
## 3.3 资产可购套餐列表
|
## 3.3 资产可购套餐列表
|
||||||
|
|
||||||
URL:
|
URL:
|
||||||
|
|||||||
626
docs/微信支付集成文档.md
Normal file
626
docs/微信支付集成文档.md
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
# 微信小程序支付集成文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档总结了套餐订购模块中的微信支付完整实现流程,可复用到其他项目中。
|
||||||
|
|
||||||
|
## 一、核心文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
miniprogram/
|
||||||
|
├── utils/
|
||||||
|
│ └── payment.ts # 微信支付工具类
|
||||||
|
├── api/
|
||||||
|
│ └── order.ts # 订单API接口
|
||||||
|
├── types/
|
||||||
|
│ └── api.ts # API类型定义
|
||||||
|
└── pages/
|
||||||
|
└── package/
|
||||||
|
└── package.ts # 套餐订购页面(业务实现)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、支付流程
|
||||||
|
|
||||||
|
### 2.1 标准套餐支付流程(三步走)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 创建订单 → 2. 获取支付参数 → 3. 调起微信支付
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第一步:创建订单
|
||||||
|
|
||||||
|
**接口:** `POST /api/c/v1/orders/create`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
app_type: 'miniapp', // 应用类型:miniapp(小程序) | official_account(公众号)
|
||||||
|
identifier: string, // 资产标识符(设备ID/ICCID等)
|
||||||
|
package_ids: number[] // 套餐ID列表
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
order_type: 'package' | 'recharge', // 订单类型
|
||||||
|
order: {
|
||||||
|
order_id: number, // 订单ID(用于后续支付)
|
||||||
|
order_no: string,
|
||||||
|
payment_status: 1, // 1:待支付
|
||||||
|
total_amount: number, // 总金额(分)
|
||||||
|
created_at: string
|
||||||
|
},
|
||||||
|
pay_config?: {...} // 强充订单才返回(见2.2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第二步:获取支付参数
|
||||||
|
|
||||||
|
**接口:** `POST /api/c/v1/orders/:id/pay`
|
||||||
|
|
||||||
|
**请求参数:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
payment_method: 'wechat', // 支付方式:wechat | wallet
|
||||||
|
app_type: 'miniapp' // 应用类型(微信支付必传)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
payment_method: 'wechat',
|
||||||
|
pay_config: {
|
||||||
|
app_id: string, // 微信AppID
|
||||||
|
timestamp: string, // 时间戳
|
||||||
|
nonce_str: string, // 随机字符串
|
||||||
|
package: string, // 预支付ID,格式:prepay_id=wx...
|
||||||
|
sign_type: string, // 签名类型(RSA/MD5/HMAC-SHA256)
|
||||||
|
pay_sign: string // 支付签名
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第三步:调起微信支付
|
||||||
|
|
||||||
|
使用 `wx.requestPayment()` 调起支付:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wx.requestPayment({
|
||||||
|
timeStamp: pay_config.timestamp,
|
||||||
|
nonceStr: pay_config.nonce_str,
|
||||||
|
package: pay_config.package,
|
||||||
|
signType: pay_config.sign_type,
|
||||||
|
paySign: pay_config.pay_sign,
|
||||||
|
success: (res) => {
|
||||||
|
// 支付成功
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
// 支付失败或取消
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 强充订单支付流程(一步走)
|
||||||
|
|
||||||
|
当订单返回 `order_type === 'recharge'` 且有 `pay_config` 时,表示强充订单:
|
||||||
|
|
||||||
|
```
|
||||||
|
创建订单(直接返回支付参数) → 调起微信支付
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点:**
|
||||||
|
- 创建订单时已返回完整 `pay_config`
|
||||||
|
- 无需再调用 `/orders/:id/pay` 接口
|
||||||
|
- 直接使用返回的 `pay_config` 调起支付
|
||||||
|
|
||||||
|
**判断逻辑:**
|
||||||
|
```typescript
|
||||||
|
if (orderResult.order_type === 'recharge' && orderResult.pay_config) {
|
||||||
|
// 强充订单,直接支付
|
||||||
|
await WxPayment.payWithCallback(orderResult.pay_config, callbacks);
|
||||||
|
} else {
|
||||||
|
// 普通订单,需要获取支付参数
|
||||||
|
const payResult = await OrderApi.payOrder(orderId, {...});
|
||||||
|
await WxPayment.payWithCallback(payResult.pay_config, callbacks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、核心代码实现
|
||||||
|
|
||||||
|
### 3.1 微信支付工具类(payment.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 微信支付工具类
|
||||||
|
*/
|
||||||
|
import { Logger } from './logger';
|
||||||
|
|
||||||
|
/** 微信支付配置 */
|
||||||
|
export interface WxPayConfig {
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
package: string;
|
||||||
|
signType: 'RSA' | 'MD5' | 'HMAC-SHA256';
|
||||||
|
paySign: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支付结果 */
|
||||||
|
export interface PaymentResult {
|
||||||
|
success: boolean;
|
||||||
|
errMsg?: string;
|
||||||
|
userCancelled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WxPayment {
|
||||||
|
/**
|
||||||
|
* 发起微信支付
|
||||||
|
* @param payConfig 后端返回的支付配置
|
||||||
|
* @returns 支付结果
|
||||||
|
*/
|
||||||
|
static async pay(payConfig: {
|
||||||
|
app_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
nonce_str: string;
|
||||||
|
package: string;
|
||||||
|
sign_type: string;
|
||||||
|
pay_sign: string;
|
||||||
|
}): Promise<PaymentResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 1. 验证 package 参数格式
|
||||||
|
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
|
||||||
|
Logger.error('微信支付参数错误:package 格式不正确', {
|
||||||
|
package: payConfig.package,
|
||||||
|
expected: 'prepay_id=wx...'
|
||||||
|
});
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
errMsg: '支付参数获取失败,请稍后重试'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info('发起微信支付', {
|
||||||
|
...payConfig,
|
||||||
|
pay_sign: payConfig.pay_sign.substring(0, 10) + '...' // 脱敏
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 调用微信支付API
|
||||||
|
wx.requestPayment({
|
||||||
|
timeStamp: payConfig.timestamp,
|
||||||
|
nonceStr: payConfig.nonce_str,
|
||||||
|
package: payConfig.package,
|
||||||
|
signType: payConfig.sign_type as any,
|
||||||
|
paySign: payConfig.pay_sign,
|
||||||
|
success: (res) => {
|
||||||
|
Logger.info('微信支付成功', res);
|
||||||
|
resolve({ success: true });
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
Logger.error('微信支付失败', err);
|
||||||
|
|
||||||
|
// 判断是否为用户取消
|
||||||
|
const userCancelled = err.errMsg?.includes('cancel') ||
|
||||||
|
err.errMsg?.includes('用户取消');
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
errMsg: err.errMsg,
|
||||||
|
userCancelled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付并使用回调处理结果
|
||||||
|
* @param payConfig 支付配置
|
||||||
|
* @param callbacks 回调函数
|
||||||
|
*/
|
||||||
|
static async payWithCallback(
|
||||||
|
payConfig: {
|
||||||
|
app_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
nonce_str: string;
|
||||||
|
package: string;
|
||||||
|
sign_type: string;
|
||||||
|
pay_sign: string;
|
||||||
|
},
|
||||||
|
callbacks?: {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onFail?: (errMsg: string) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
): Promise<PaymentResult> {
|
||||||
|
const result = await this.pay(payConfig);
|
||||||
|
|
||||||
|
// 根据结果调用相应回调
|
||||||
|
if (result.success) {
|
||||||
|
callbacks?.onSuccess?.();
|
||||||
|
} else if (result.userCancelled) {
|
||||||
|
callbacks?.onCancel?.();
|
||||||
|
} else {
|
||||||
|
callbacks?.onFail?.(result.errMsg || '支付失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 订单API封装(order.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Request } from '../utils/request';
|
||||||
|
import type {
|
||||||
|
CreateOrderRequest,
|
||||||
|
CreateOrderResponse,
|
||||||
|
PayOrderRequest,
|
||||||
|
PayOrderResponse
|
||||||
|
} from '../types/api';
|
||||||
|
|
||||||
|
export class OrderApi {
|
||||||
|
/**
|
||||||
|
* 创建订单
|
||||||
|
* POST /api/c/v1/orders/create
|
||||||
|
*/
|
||||||
|
static async createOrder(data: CreateOrderRequest): Promise<CreateOrderResponse> {
|
||||||
|
return Request.post<CreateOrderResponse>(
|
||||||
|
'/api/c/v1/orders/create',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单支付
|
||||||
|
* POST /api/c/v1/orders/:id/pay
|
||||||
|
*/
|
||||||
|
static async payOrder(orderId: number, data: PayOrderRequest): Promise<PayOrderResponse> {
|
||||||
|
return Request.post<PayOrderResponse>(
|
||||||
|
`/api/c/v1/orders/${orderId}/pay`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 业务层实现示例(package.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { OrderApi } from '../../api/order';
|
||||||
|
import { WxPayment } from '../../utils/payment';
|
||||||
|
import { Logger } from '../../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单并支付(微信支付)
|
||||||
|
*/
|
||||||
|
async createOrderAndPay() {
|
||||||
|
try {
|
||||||
|
const selectedPackage = this.data.packages[this.data.selectedIdx];
|
||||||
|
|
||||||
|
// 第一步:创建订单
|
||||||
|
wx.showLoading({ title: '创建订单...', mask: true });
|
||||||
|
|
||||||
|
const orderResult = await OrderApi.createOrder({
|
||||||
|
app_type: 'miniapp',
|
||||||
|
identifier: this.getIdentifier(),
|
||||||
|
package_ids: [selectedPackage.package_id]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否为强充订单
|
||||||
|
if (orderResult.order_type === 'recharge' && orderResult.pay_config) {
|
||||||
|
// 强充订单:直接使用返回的 pay_config 支付
|
||||||
|
|
||||||
|
// 验证 pay_config 有效性
|
||||||
|
if (!orderResult.pay_config.package) {
|
||||||
|
Logger.error('强充订单支付配置无效', {
|
||||||
|
recharge_id: orderResult.recharge?.recharge_id
|
||||||
|
});
|
||||||
|
wx.hideLoading();
|
||||||
|
wx.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wx.hideLoading();
|
||||||
|
this.closeModal();
|
||||||
|
|
||||||
|
// 调起支付
|
||||||
|
await WxPayment.payWithCallback(
|
||||||
|
orderResult.pay_config,
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
this.handlePaySuccess();
|
||||||
|
},
|
||||||
|
onFail: (errMsg) => {
|
||||||
|
wx.showToast({ title: '支付失败', icon: 'none' });
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
wx.showToast({ title: '已取消支付', icon: 'none' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通套餐订单:需要获取支付参数
|
||||||
|
const orderId = orderResult.order.order_id;
|
||||||
|
|
||||||
|
// 第二步:获取支付参数
|
||||||
|
wx.showLoading({ title: '准备支付...', mask: true });
|
||||||
|
|
||||||
|
const payResult = await OrderApi.payOrder(orderId, {
|
||||||
|
payment_method: 'wechat',
|
||||||
|
app_type: 'miniapp'
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.info('订单支付接口返回:', {
|
||||||
|
order_id: orderId,
|
||||||
|
has_pay_config: !!payResult.pay_config
|
||||||
|
});
|
||||||
|
|
||||||
|
wx.hideLoading();
|
||||||
|
this.closeModal();
|
||||||
|
|
||||||
|
// 验证 pay_config
|
||||||
|
if (!payResult.pay_config || !payResult.pay_config.package) {
|
||||||
|
Logger.error('订单支付接口返回配置无效', {
|
||||||
|
order_id: orderId
|
||||||
|
});
|
||||||
|
wx.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三步:调起微信支付
|
||||||
|
await WxPayment.payWithCallback(
|
||||||
|
payResult.pay_config,
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
this.handlePaySuccess();
|
||||||
|
},
|
||||||
|
onFail: (errMsg) => {
|
||||||
|
wx.showToast({ title: '支付失败', icon: 'none' });
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
wx.showToast({ title: '已取消支付', icon: 'none' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
wx.hideLoading();
|
||||||
|
this.closeModal();
|
||||||
|
|
||||||
|
Logger.error('创建订单或支付失败:', error);
|
||||||
|
|
||||||
|
// 特殊错误处理:订单正在创建中
|
||||||
|
if (error.code === 1008) {
|
||||||
|
wx.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
|
||||||
|
confirmText: '去支付',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
wx.navigateTo({ url: '/pages/orders/orders' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他错误提示
|
||||||
|
const errorMsg = error.message || error.msg || '操作失败,请稍后重试';
|
||||||
|
wx.showToast({ title: errorMsg, icon: 'none' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付成功处理
|
||||||
|
*/
|
||||||
|
handlePaySuccess() {
|
||||||
|
const pkg = this.data.packages[this.data.selectedIdx];
|
||||||
|
this.setData({ selectedIdx: -1 });
|
||||||
|
wx.showToast({
|
||||||
|
title: `「${pkg.name}」支付成功`,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
// 可选:刷新数据、跳转页面等
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、类型定义(api.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** 创建订单 - 请求 */
|
||||||
|
export interface CreateOrderRequest {
|
||||||
|
app_type?: 'official_account' | 'miniapp'; // 应用类型(强充必传)
|
||||||
|
identifier: string; // 资产标识符
|
||||||
|
package_ids: number[]; // 套餐ID列表
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建订单 - 响应 */
|
||||||
|
export interface CreateOrderResponse {
|
||||||
|
order_type: 'package' | 'recharge'; // 订单类型
|
||||||
|
order: {
|
||||||
|
order_id: number;
|
||||||
|
order_no: string;
|
||||||
|
payment_status: 1 | 2 | 3 | 4; // 1:待支付 2:已支付 3:已取消 4:已退款
|
||||||
|
total_amount: number; // 总金额(分)
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
pay_config?: { // 强充订单才返回
|
||||||
|
app_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
nonce_str: string;
|
||||||
|
package: string; // prepay_id=wx...
|
||||||
|
sign_type: string;
|
||||||
|
pay_sign: string;
|
||||||
|
} | null;
|
||||||
|
recharge?: { // 充值订单信息
|
||||||
|
recharge_id: number;
|
||||||
|
recharge_no: string;
|
||||||
|
amount: number;
|
||||||
|
status: 0 | 1 | 2; // 0:待支付 1:已支付 2:已关闭
|
||||||
|
auto_purchase_status: string;
|
||||||
|
};
|
||||||
|
linked_package_info?: { // 强充关联套餐信息
|
||||||
|
package_names: string[];
|
||||||
|
total_package_amount: number;
|
||||||
|
wallet_credit: number;
|
||||||
|
force_recharge_amount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单支付 - 请求 */
|
||||||
|
export interface PayOrderRequest {
|
||||||
|
payment_method: 'wallet' | 'wechat'; // 支付方式
|
||||||
|
app_type?: 'official_account' | 'miniapp'; // 微信支付必传
|
||||||
|
password?: string; // 钱包支付必传
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单支付 - 响应 */
|
||||||
|
export interface PayOrderResponse {
|
||||||
|
payment_method: 'wallet' | 'wechat';
|
||||||
|
pay_config?: { // 微信支付时返回
|
||||||
|
app_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
nonce_str: string;
|
||||||
|
package: string;
|
||||||
|
sign_type: string;
|
||||||
|
pay_sign: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、关键要点
|
||||||
|
|
||||||
|
### 5.1 支付参数验证
|
||||||
|
|
||||||
|
**必须验证 `package` 参数格式:**
|
||||||
|
```typescript
|
||||||
|
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
|
||||||
|
// 参数无效,停止支付
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 支付结果判断
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 成功
|
||||||
|
success: (res) => { /* 支付成功 */ }
|
||||||
|
|
||||||
|
// 失败/取消
|
||||||
|
fail: (err) => {
|
||||||
|
// 判断是否用户取消
|
||||||
|
const userCancelled = err.errMsg?.includes('cancel') ||
|
||||||
|
err.errMsg?.includes('用户取消');
|
||||||
|
|
||||||
|
if (userCancelled) {
|
||||||
|
// 用户主动取消
|
||||||
|
} else {
|
||||||
|
// 支付失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 错误处理
|
||||||
|
|
||||||
|
1. **参数验证失败** - 提示用户并停止支付
|
||||||
|
2. **用户取消支付** - 友好提示,不作为错误
|
||||||
|
3. **支付失败** - 提示错误信息
|
||||||
|
4. **订单创建中(1008)** - 引导用户到订单页
|
||||||
|
|
||||||
|
### 5.4 日志记录
|
||||||
|
|
||||||
|
关键节点必须记录日志:
|
||||||
|
- 支付参数(脱敏)
|
||||||
|
- 支付成功/失败
|
||||||
|
- 参数验证失败
|
||||||
|
- 异常情况
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Logger.info('发起微信支付', {
|
||||||
|
...payConfig,
|
||||||
|
pay_sign: payConfig.pay_sign.substring(0, 10) + '...' // 脱敏
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 Loading状态管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建订单前
|
||||||
|
wx.showLoading({ title: '创建订单...', mask: true });
|
||||||
|
|
||||||
|
// 获取支付参数前
|
||||||
|
wx.showLoading({ title: '准备支付...', mask: true });
|
||||||
|
|
||||||
|
// 调起支付前
|
||||||
|
wx.hideLoading(); // 必须关闭,否则遮挡支付界面
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么有两种支付流程?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- **标准流程(三步)**:普通套餐订购
|
||||||
|
- **快捷流程(一步)**:强充订单,后端已预创建支付参数
|
||||||
|
|
||||||
|
### Q2: package 参数为什么要验证?
|
||||||
|
|
||||||
|
**A:** 后端可能返回空值或格式错误,直接传给微信会导致支付崩溃。必须验证格式为 `prepay_id=wx...`
|
||||||
|
|
||||||
|
### Q3: 如何区分用户取消和支付失败?
|
||||||
|
|
||||||
|
**A:** 检查 `errMsg` 是否包含 `'cancel'` 或 `'用户取消'` 关键字。
|
||||||
|
|
||||||
|
### Q4: 为什么调起支付前要 hideLoading?
|
||||||
|
|
||||||
|
**A:** Loading 遮罩会挡住微信支付界面,导致用户无法操作。
|
||||||
|
|
||||||
|
## 七、快速集成清单
|
||||||
|
|
||||||
|
要在新项目中集成微信支付,需要:
|
||||||
|
|
||||||
|
- [ ] 复制 `utils/payment.ts` 工具类
|
||||||
|
- [ ] 创建订单API接口(`createOrder`、`payOrder`)
|
||||||
|
- [ ] 定义类型文件(`api.ts`)
|
||||||
|
- [ ] 实现业务逻辑(参考 `createOrderAndPay`)
|
||||||
|
- [ ] 添加 Logger 日志工具(可选)
|
||||||
|
- [ ] 处理支付回调(成功、失败、取消)
|
||||||
|
- [ ] 添加错误处理逻辑
|
||||||
|
|
||||||
|
## 八、注意事项
|
||||||
|
|
||||||
|
1. **安全性**
|
||||||
|
- 日志中敏感信息(pay_sign)要脱敏
|
||||||
|
- 永远不要在前端生成支付签名
|
||||||
|
- 所有支付参数由后端返回
|
||||||
|
|
||||||
|
2. **用户体验**
|
||||||
|
- 支付过程中显示 Loading 提示
|
||||||
|
- 调起支付前必须关闭 Loading
|
||||||
|
- 区分取消和失败,给出不同提示
|
||||||
|
- 支付失败时保留订单,允许用户重试
|
||||||
|
|
||||||
|
3. **异常处理**
|
||||||
|
- 参数验证失败时友好提示
|
||||||
|
- 网络错误时允许重试
|
||||||
|
- 订单创建中(1008)引导到订单页
|
||||||
|
- 所有异常都要记录日志
|
||||||
|
|
||||||
|
4. **测试要点**
|
||||||
|
- 正常支付流程
|
||||||
|
- 用户取消支付
|
||||||
|
- 网络异常
|
||||||
|
- 参数异常
|
||||||
|
- 强充订单流程
|
||||||
|
- 普通订单流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本:** v1.0
|
||||||
|
**更新日期:** 2026-04-14
|
||||||
|
**适用范围:** 微信小程序支付
|
||||||
80
docs/浅色深色.md
Normal file
80
docs/浅色深色.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
声明 color-scheme
|
||||||
|
有两种方式:
|
||||||
|
|
||||||
|
meta: 在head中声明<meta name="color-scheme" content="light dark">,声明当前页面支持 light 和 dark 两种模式,系统切换到深色模式时,浏览器默认样式也会切换到深色;
|
||||||
|
css:下面的 css 同样可以实现上面 meta 声明的效果
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
注意:此声明并非为页面做自动适配,只影响浏览器默认样式
|
||||||
|
|
||||||
|
更多信息可查阅 W3C 文档 《CSS Color Adjustment Module Level 1》
|
||||||
|
|
||||||
|
通过 CSS 媒体查询
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
颜色较多的情况,建议使用CSS变量对颜色值进行管理
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--nav-bg-color: #F7F7F7;
|
||||||
|
--content-bg-color: #FFFFFF;
|
||||||
|
--font-color: rgba(0,0,0,.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--nav-bg-color: #2F2F2F;
|
||||||
|
--content-bg-color: #2C2C2C;
|
||||||
|
--font-color: rgba(255, 255, 255, .8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color: var(--font-color)
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: var(--nav-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background-color: var(--content-bg-color);
|
||||||
|
}
|
||||||
|
图片适配
|
||||||
|
利用picture+source标签,设置不同模式下的图片 url。
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<!-- 深色模式下的图片 -->
|
||||||
|
<source srcset="dark.jpg" media="(prefers-color-scheme: dark)" />
|
||||||
|
<!-- 默认模式下的图片 -->
|
||||||
|
<img src="light.jpg"/>
|
||||||
|
</picture>
|
||||||
|
JavaScript中判断当前模式&监听模式变化
|
||||||
|
利用的是matchMedia方法,具体用法参考以下例子:
|
||||||
|
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
function darkModeHandler() {
|
||||||
|
if (mediaQuery.matches) {
|
||||||
|
console.log('现在是深色模式')
|
||||||
|
} else {
|
||||||
|
console.log('现在是浅色模式')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断当前模式
|
||||||
|
darkModeHandler()
|
||||||
|
// 监听模式变化
|
||||||
|
mediaQuery.addListener(darkModeHandler)
|
||||||
@@ -43,7 +43,9 @@
|
|||||||
/* ios打包配置 */
|
/* ios打包配置 */
|
||||||
"ios" : {},
|
"ios" : {},
|
||||||
/* SDK配置 */
|
/* SDK配置 */
|
||||||
"sdkConfigs" : {}
|
"sdkConfigs" : {
|
||||||
|
"payment" : {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* 快应用特有相关 */
|
/* 快应用特有相关 */
|
||||||
|
|||||||
6970
package-lock.json
generated
6970
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,19 +1,22 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:h5": "uni",
|
"dev:h5": "uni",
|
||||||
"build:h5": "uni build -p h5"
|
"build:h5": "UNI_INPUT_DIR=. uni build -p h5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dcloudio/types": "*",
|
"@dcloudio/types": "3.4.19",
|
||||||
"@dcloudio/vite-plugin-uni": "*",
|
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
|
||||||
|
"less": "^4.0.0",
|
||||||
"sass": "^1.63.2",
|
"sass": "^1.63.2",
|
||||||
"sass-loader": "^10.4.1",
|
"vite": "5.2.8"
|
||||||
"vite": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "*",
|
"@dcloudio/uni-app": "3.0.0-4080720251210001",
|
||||||
"@dcloudio/uni-components": "*",
|
"@dcloudio/uni-components": "3.0.0-4080720251210001",
|
||||||
"@dcloudio/uni-h5": "*",
|
"@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"
|
"uview-plus": "^3.6.29"
|
||||||
|
|||||||
@@ -47,7 +47,8 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/bind/bind",
|
"path": "pages/bind/bind",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "绑定手机号"
|
"navigationBarTitleText": "绑定手机号",
|
||||||
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,16 +47,16 @@
|
|||||||
<view class="flow-title">流量信息</view>
|
<view class="flow-title">流量信息</view>
|
||||||
<view class="flow-stats">
|
<view class="flow-stats">
|
||||||
<view class="flow-item">
|
<view class="flow-item">
|
||||||
<view class="flow-label">已用真流量</view>
|
<view class="flow-label">已使用</view>
|
||||||
<view class="flow-value">{{ formatMB(item.data_usage_mb) }}</view>
|
<view class="flow-value">{{ getUsedFlow(item) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="flow-item">
|
<view class="flow-item">
|
||||||
<view class="flow-label">总量真流量</view>
|
<view class="flow-label">总流量</view>
|
||||||
<view class="flow-value">{{ formatMB(item.data_limit_mb) }}</view>
|
<view class="flow-value">{{ getTotalFlow(item) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="flow-item">
|
<view class="flow-item">
|
||||||
<view class="flow-label">剩余虚流量</view>
|
<view class="flow-label">剩余</view>
|
||||||
<view class="flow-value">{{ formatMB(item.virtual_remain_mb) }}</view>
|
<view class="flow-value">{{ getRemainFlow(item) }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="progress-section">
|
<view class="progress-section">
|
||||||
@@ -101,16 +101,63 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatMB = (mb) => {
|
const formatMB = (mb) => {
|
||||||
if (!mb) return '0';
|
if (!mb && mb !== 0) return '0 MB';
|
||||||
if (mb >= 1024) {
|
if (mb >= 1024) {
|
||||||
return (mb / 1024).toFixed(2) + ' GB';
|
return (mb / 1024).toFixed(2) + ' GB';
|
||||||
}
|
}
|
||||||
return mb + ' MB';
|
return mb.toFixed(2) + ' MB';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取已使用流量
|
||||||
|
const getUsedFlow = (item) => {
|
||||||
|
if (item.enable_virtual_data) {
|
||||||
|
// 启用虚流量,显示已用虚流量
|
||||||
|
return formatMB(item.virtual_used_mb || 0);
|
||||||
|
} else {
|
||||||
|
// 未启用虚流量,显示已用真流量
|
||||||
|
return formatMB(item.data_usage_mb || 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取总流量
|
||||||
|
const getTotalFlow = (item) => {
|
||||||
|
if (item.enable_virtual_data) {
|
||||||
|
// 启用虚流量,显示虚流量总量
|
||||||
|
return formatMB(item.virtual_limit_mb || 0);
|
||||||
|
} else {
|
||||||
|
// 未启用虚流量,显示真流量总量
|
||||||
|
return formatMB(item.data_limit_mb || 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取剩余流量
|
||||||
|
const getRemainFlow = (item) => {
|
||||||
|
if (item.enable_virtual_data) {
|
||||||
|
// 启用虚流量,显示剩余虚流量
|
||||||
|
return formatMB(item.virtual_remain_mb || 0);
|
||||||
|
} else {
|
||||||
|
// 未启用虚流量,计算剩余真流量
|
||||||
|
const remain = (item.data_limit_mb || 0) - (item.data_usage_mb || 0);
|
||||||
|
return formatMB(remain);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取使用百分比
|
||||||
const getUsagePercent = (item) => {
|
const getUsagePercent = (item) => {
|
||||||
if (!item.data_limit_mb) return 0;
|
let used, total;
|
||||||
return Math.min((item.data_usage_mb / item.data_limit_mb) * 100, 100).toFixed(2);
|
|
||||||
|
if (item.enable_virtual_data) {
|
||||||
|
// 启用虚流量
|
||||||
|
used = item.virtual_used_mb || 0;
|
||||||
|
total = item.virtual_limit_mb || 0;
|
||||||
|
} else {
|
||||||
|
// 未启用虚流量
|
||||||
|
used = item.data_usage_mb || 0;
|
||||||
|
total = item.data_limit_mb || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!total) return 0;
|
||||||
|
return Math.min((used / total) * 100, 100).toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||||
|
<view class="empty-icon">📇</view>
|
||||||
|
<view class="empty-title">认证列表为空</view>
|
||||||
|
<view class="empty-desc">当前账号下暂无可实名的卡片</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="card" v-for="item in list" :key="item.iccid">
|
<view class="card" v-for="item in list" :key="item.iccid">
|
||||||
<view class="flex-row-g20">
|
<view class="flex-row-g20">
|
||||||
<view class="logo">
|
<view class="logo">
|
||||||
@@ -32,12 +38,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { assetApi, realnameApi } from '@/api/index.js';
|
import { assetApi, deviceApi, realnameApi } from '@/api/index.js';
|
||||||
import { useUserStore } from '@/store/index.js';
|
import { useUserStore } from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
let list = reactive([]);
|
let list = reactive([]);
|
||||||
|
let loading = ref(false);
|
||||||
let showIccidModal = ref(false);
|
let showIccidModal = ref(false);
|
||||||
let currentModalIccid = ref('');
|
let currentModalIccid = ref('');
|
||||||
let currentCard = ref(null);
|
let currentCard = ref(null);
|
||||||
@@ -54,18 +61,43 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadCards = async () => {
|
const loadCards = async () => {
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await assetApi.getInfo(userStore.state.identifier);
|
const assetData = await assetApi.getInfo(userStore.state.identifier);
|
||||||
if (data.cards && data.cards.length > 0) {
|
|
||||||
list.splice(0, list.length, ...data.cards.map(card => ({
|
// 判断是否为设备
|
||||||
iccid: card.iccid,
|
if (assetData.asset_type === 'device') {
|
||||||
category: card.carrier_name.includes('电信') ? '124' : card.carrier_name.includes('联通') ? '125' : '126',
|
// 是设备,调用设备卡列表接口
|
||||||
isRealName: card.real_name_status === 1
|
const data = await deviceApi.getCards(userStore.state.identifier);
|
||||||
})));
|
if (data.cards && data.cards.length > 0) {
|
||||||
|
list.splice(0, list.length, ...data.cards.map(card => ({
|
||||||
|
iccid: card.iccid,
|
||||||
|
category: getCarrierCategory(card.carrier_name),
|
||||||
|
isRealName: card.real_name_status === 1
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不是设备(单卡),直接使用 asset info 数据
|
||||||
|
if (assetData.identifier) {
|
||||||
|
list.splice(0, list.length, {
|
||||||
|
iccid: assetData.identifier,
|
||||||
|
category: getCarrierCategory(assetData.carrier_name),
|
||||||
|
isRealName: assetData.real_name_status === 1
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载卡列表失败', e);
|
console.error('加载卡列表失败', e);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCarrierCategory = (carrierName) => {
|
||||||
|
if (!carrierName) return '126';
|
||||||
|
if (carrierName.includes('电信')) return '124';
|
||||||
|
if (carrierName.includes('联通')) return '125';
|
||||||
|
if (carrierName.includes('移动')) return '126';
|
||||||
|
return '126';
|
||||||
};
|
};
|
||||||
|
|
||||||
const toReal = async (card) => {
|
const toReal = async (card) => {
|
||||||
@@ -200,4 +232,16 @@
|
|||||||
border-radius: var(--radius-small);
|
border-radius: var(--radius-small);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 40rpx;
|
||||||
|
min-height: 400rpx;
|
||||||
|
.empty-icon { font-size: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
|
||||||
|
.empty-title { font-size: 32rpx; font-weight: 600; color: var(--text-primary); margin-bottom: 16rpx; }
|
||||||
|
.empty-desc { font-size: 26rpx; color: var(--text-tertiary); text-align: center; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,29 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="page">
|
||||||
<view class="card flex-col-g20">
|
<!-- 自定义导航栏 -->
|
||||||
<view class="flex-row-g20">
|
<view v-if="!fromLogin" class="custom-navbar">
|
||||||
<label for="">手机号:</label>
|
<view class="navbar-back" @tap="goBack">
|
||||||
<up-input placeholder="请输入绑定的手机号" border="surround" v-model="bind.phone" />
|
<text class="back-text">🔙</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="navbar-title">绑定手机号</view>
|
||||||
|
<view class="navbar-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="flex-row-g20">
|
<view class="container" :style="{ paddingTop: fromLogin ? '0' : '0' }">
|
||||||
<label for="">验证码:</label>
|
<view class="card flex-col-g20">
|
||||||
<up-input placeholder="验证码" v-model="bind.code">
|
<view class="flex-row-g20">
|
||||||
<template #suffix>
|
<label for="">手机号:</label>
|
||||||
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0"></up-button>
|
<up-input placeholder="请输入绑定的手机号" border="surround" v-model="bind.phone" />
|
||||||
</template>
|
</view>
|
||||||
</up-input>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="btn">
|
<view class="flex-row-g20">
|
||||||
<up-button type="primary" @tap="bindPhone">绑定手机号</up-button>
|
<label for="">验证码:</label>
|
||||||
|
<up-input placeholder="验证码" v-model="bind.code">
|
||||||
|
<template #suffix>
|
||||||
|
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0"></up-button>
|
||||||
|
</template>
|
||||||
|
</up-input>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="btn">
|
||||||
|
<up-button type="primary" @tap="bindPhone">绑定手机号</up-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 如果从登录页面跳转过来,显示退出按钮 -->
|
||||||
|
<view class="btn" v-if="fromLogin">
|
||||||
|
<up-button type="error" @tap="logout">退出</up-button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { authApi } from '@/api/index.js';
|
import { authApi } from '@/api/index.js';
|
||||||
|
|
||||||
let bind = reactive({
|
let bind = reactive({
|
||||||
@@ -33,7 +49,16 @@
|
|||||||
|
|
||||||
let cooldown = ref(0);
|
let cooldown = ref(0);
|
||||||
let codeText = ref('获取验证码');
|
let codeText = ref('获取验证码');
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
let fromLogin = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取页面参数,判断是否从登录页面跳转过来
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const currentPage = pages[pages.length - 1];
|
||||||
|
const options = currentPage.options;
|
||||||
|
fromLogin.value = options.fromLogin === 'true';
|
||||||
|
});
|
||||||
|
|
||||||
const getCode = async () => {
|
const getCode = async () => {
|
||||||
if (!bind.phone) {
|
if (!bind.phone) {
|
||||||
@@ -77,13 +102,89 @@ let timer = null;
|
|||||||
await authApi.bindPhone(bind.phone, bind.code);
|
await authApi.bindPhone(bind.phone, bind.code);
|
||||||
uni.showToast({ title: '绑定成功', icon: 'success' });
|
uni.showToast({ title: '绑定成功', icon: 'success' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.navigateBack();
|
if (fromLogin.value) {
|
||||||
|
// 从登录页面过来的,绑定成功后跳转到首页
|
||||||
|
uni.redirectTo({ url: '/pages/index/index' });
|
||||||
|
} else {
|
||||||
|
// 从其他页面过来的,返回上一页
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('绑定手机号失败', e);
|
console.error('绑定手机号失败', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.clearStorageSync();
|
||||||
|
uni.reLaunch({ url: '/pages/login/login' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1rpx solid var(--border-light);
|
||||||
|
padding: 0 24rpx;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.navbar-back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-placeholder {
|
||||||
|
width: 100rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<view class="card">
|
<view class="card">
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<view class="label">原手机号</view>
|
<view class="label">原手机号</view>
|
||||||
<up-input placeholder="请输入原手机号" border="surround" v-model="form.oldPhone" />
|
<up-input placeholder="请输入原手机号" border="surround" v-model="form.oldPhone" disabled/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
import { authApi } from '@/api/index.js';
|
import { authApi } from '@/api/index.js';
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -47,6 +47,17 @@
|
|||||||
newCode: ''
|
newCode: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取 URL 参数中的原手机号
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const currentPage = pages[pages.length - 1];
|
||||||
|
const options = currentPage.options;
|
||||||
|
|
||||||
|
if (options.oldPhone) {
|
||||||
|
form.oldPhone = options.oldPhone;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let oldCooldown = ref(0);
|
let oldCooldown = ref(0);
|
||||||
let newCooldown = ref(0);
|
let newCooldown = ref(0);
|
||||||
let oldCodeText = ref('获取验证码');
|
let oldCodeText = ref('获取验证码');
|
||||||
|
|||||||
@@ -8,34 +8,34 @@
|
|||||||
|
|
||||||
<view v-else-if="exchangeData" class="card exchange-card">
|
<view v-else-if="exchangeData" class="card exchange-card">
|
||||||
<view class="exchange-header flex-row-sb">
|
<view class="exchange-header flex-row-sb">
|
||||||
<view class="exchange-no">{{ exchangeData.exchange_no }}</view>
|
<view class="exchange-no">{{ exchangeData?.exchange_no }}</view>
|
||||||
<view class="tag-apple" :class="getStatusClass(exchangeData.status)">{{ exchangeData.status_text }}</view>
|
<view class="tag-apple" :class="getStatusClass(exchangeData?.status)">{{ exchangeData?.status_text }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="exchange-info">
|
<view class="exchange-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">{{ exchangeData.exchange_reason }}</view>
|
<view class="info-value">{{ exchangeData?.exchange_reason }}</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">{{ formatTime(exchangeData.created_at) }}</view>
|
<view class="info-value">{{ formatTime(exchangeData?.created_at) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_name">
|
<view class="info-row flex-row-sb" v-if="exchangeData?.recipient_name">
|
||||||
<view class="info-label">收件人</view>
|
<view class="info-label">收件人</view>
|
||||||
<view class="info-value">{{ exchangeData.recipient_name }}</view>
|
<view class="info-value">{{ exchangeData?.recipient_name }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_phone">
|
<view class="info-row flex-row-sb" v-if="exchangeData?.recipient_phone">
|
||||||
<view class="info-label">联系电话</view>
|
<view class="info-label">联系电话</view>
|
||||||
<view class="info-value">{{ exchangeData.recipient_phone }}</view>
|
<view class="info-value">{{ exchangeData?.recipient_phone }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_address">
|
<view class="info-row flex-row-sb" v-if="exchangeData?.recipient_address">
|
||||||
<view class="info-label">收货地址</view>
|
<view class="info-label">收货地址</view>
|
||||||
<view class="info-value address-value">{{ exchangeData.recipient_address }}</view>
|
<view class="info-value address-value">{{ exchangeData?.recipient_address }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="exchange-actions" v-if="exchangeData.status === 1">
|
<view class="exchange-actions" v-if="exchangeData?.status === 1">
|
||||||
<button class="btn-apple btn-primary" @tap="openPopup">填写信息</button>
|
<button class="btn-apple btn-primary" @tap="openPopup">填写信息</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
let exchangeData = reactive(null);
|
let exchangeData = ref(null);
|
||||||
let loading = ref(false);
|
let loading = ref(false);
|
||||||
let showPopup = ref(false);
|
let showPopup = ref(false);
|
||||||
let form = reactive({
|
let form = reactive({
|
||||||
@@ -100,11 +100,11 @@
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await exchangeApi.getPending(userStore.state.identifier);
|
const data = await exchangeApi.getPending(userStore.state.identifier);
|
||||||
if (data) {
|
// 接口返回的是单个对象,不是数组
|
||||||
exchangeData.splice(0, exchangeData.length, data);
|
exchangeData.value = data || null;
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载换货记录失败', e);
|
console.error('加载换货记录失败', e);
|
||||||
|
exchangeData.value = null;
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
@@ -125,6 +125,14 @@
|
|||||||
uni.showToast({ title: '请输入收件人电话', icon: 'none' });
|
uni.showToast({ title: '请输入收件人电话', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 手机号格式校验
|
||||||
|
const phoneReg = /^1[3-9]\d{9}$/;
|
||||||
|
if (!phoneReg.test(form.recipient_phone)) {
|
||||||
|
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!form.recipient_address) {
|
if (!form.recipient_address) {
|
||||||
uni.showToast({ title: '请输入收货地址', icon: 'none' });
|
uni.showToast({ title: '请输入收货地址', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
@@ -132,7 +140,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await exchangeApi.submitShippingInfo(
|
await exchangeApi.submitShippingInfo(
|
||||||
exchangeData.id,
|
exchangeData.value.id,
|
||||||
form.recipient_name,
|
form.recipient_name,
|
||||||
form.recipient_phone,
|
form.recipient_phone,
|
||||||
form.recipient_address
|
form.recipient_address
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<UserInfoCard :currentCardNo="currentCardNo" :deviceInfo="deviceInfo" :onlineStatus="onlineStatus" />
|
<UserInfoCard :currentCardNo="currentCardNo" :deviceInfo="deviceInfo" :onlineStatus="onlineStatus"
|
||||||
|
: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')" />
|
@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" /> -->
|
||||||
|
|
||||||
<TrafficCard :deviceInfo="deviceInfo" :isDevice="userInfo.isDevice" />
|
<TrafficCard :identifier="currentCardNo" @packageLoaded="handlePackageLoaded" />
|
||||||
|
|
||||||
<WhitelistCard v-if="!userInfo.isDevice" :whitelistData="whitelistData" @refresh="refreshWhitelist"
|
<!-- <WhitelistCard v-if="!userInfo.isDevice" :whitelistData="whitelistData" @refresh="refreshWhitelist"
|
||||||
@add="showAddWhitelistDialog" @showSms="showSmsCodeDialog" />
|
@add="showAddWhitelistDialog" @showSms="showSmsCodeDialog" /> -->
|
||||||
|
|
||||||
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi" @copy="copy" />
|
<WifiCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" @modify="modifyWifi" @copy="copy" />
|
||||||
|
|
||||||
@@ -120,8 +121,13 @@
|
|||||||
import WifiCard from '@/components/WifiCard.vue';
|
import WifiCard from '@/components/WifiCard.vue';
|
||||||
import FunctionCard from '@/components/FunctionCard.vue';
|
import FunctionCard from '@/components/FunctionCard.vue';
|
||||||
import FloatingButton from '@/components/FloatingButton.vue';
|
import FloatingButton from '@/components/FloatingButton.vue';
|
||||||
import { assetApi, deviceApi } from '@/api/index.js';
|
import {
|
||||||
import { useUserStore } from '@/store/index.js';
|
assetApi,
|
||||||
|
deviceApi
|
||||||
|
} from '@/api/index.js';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -137,29 +143,35 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let deviceInfo = reactive({
|
let deviceInfo = reactive({
|
||||||
battery: 100,
|
battery: 0,
|
||||||
connect: 0,
|
connect: 0,
|
||||||
statusStr: '正常',
|
network_status: 1,
|
||||||
expireDate: '',
|
status: 1,
|
||||||
currentIccid: '',
|
expireDate: '-',
|
||||||
category: '',
|
currentIccid: '-',
|
||||||
phone: '',
|
category: '-',
|
||||||
|
phone: '-',
|
||||||
flowSize: 0,
|
flowSize: 0,
|
||||||
totalBytesCnt: 0,
|
totalBytesCnt: 0,
|
||||||
ssidName: '',
|
ssidName: '-',
|
||||||
ssidPwd: '',
|
ssidPwd: '-',
|
||||||
rssi: '强',
|
rssi: '-',
|
||||||
onlineStatus: '0',
|
onlineStatus: '0',
|
||||||
connCnt: 0,
|
connCnt: 0,
|
||||||
run_time: 0,
|
run_time: 0,
|
||||||
last_online_time: '',
|
last_online_time: '-',
|
||||||
lan_ip: '',
|
lan_ip: '-',
|
||||||
kf_url: '',
|
kf_url: '-',
|
||||||
mchList: []
|
mchList: [],
|
||||||
|
imei: '-',
|
||||||
|
max_clients: 1,
|
||||||
|
asset_type: 'device',
|
||||||
|
bound_phone: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
let isRealName = ref(false);
|
let isRealName = ref(false);
|
||||||
let alreadyBindPhone = ref(false);
|
let alreadyBindPhone = ref(false);
|
||||||
|
let boundPhone = ref('');
|
||||||
let realNameStatus = ref('');
|
let realNameStatus = ref('');
|
||||||
let wifi_info = reactive({
|
let wifi_info = reactive({
|
||||||
ssid: '',
|
ssid: '',
|
||||||
@@ -200,39 +212,96 @@
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await assetApi.getInfo(identifier);
|
const data = await assetApi.getInfo(identifier);
|
||||||
|
|
||||||
|
// 基本信息
|
||||||
userInfo.isDevice = data.asset_type === 'device';
|
userInfo.isDevice = data.asset_type === 'device';
|
||||||
currentCardNo.value = data.identifier;
|
currentCardNo.value = data.identifier || '-';
|
||||||
deviceInfo.statusStr = data.status === 1 ? '正常' : '禁用';
|
deviceInfo.network_status = data.network_status;
|
||||||
|
deviceInfo.status = data.status;
|
||||||
|
deviceInfo.imei = data.imei || '-';
|
||||||
|
deviceInfo.asset_type = data.asset_type || 'device';
|
||||||
|
deviceInfo.bound_phone = data.bound_phone || '';
|
||||||
isRealName.value = data.real_name_status === 1;
|
isRealName.value = data.real_name_status === 1;
|
||||||
realNameStatus.value = data.real_name_status === 1 ? '已实名' : '未实名';
|
realNameStatus.value = data.real_name_status === 1 ? '已实名' : '未实名';
|
||||||
deviceInfo.flowSize = data.package_remain_mb || 0;
|
boundPhone.value = data.bound_phone || '';
|
||||||
deviceInfo.totalBytesCnt = data.package_total_mb || 0;
|
alreadyBindPhone.value = !!data.bound_phone;
|
||||||
deviceInfo.currentIccid = data.identifier;
|
|
||||||
|
|
||||||
|
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
|
||||||
|
|
||||||
|
// 当前卡信息
|
||||||
|
if (data.cards && data.cards.length > 0) {
|
||||||
|
// 找到当前卡
|
||||||
|
const currentCard = data.cards.find(card => card.is_current) || data.cards[0];
|
||||||
|
deviceInfo.currentIccid = currentCard.iccid || '-';
|
||||||
|
|
||||||
|
// 卡列表
|
||||||
|
deviceInfo.mchList = data.cards.map(card => ({
|
||||||
|
iccidMark: card.iccid || '-',
|
||||||
|
category: card.slot_position,
|
||||||
|
is_current: card.is_current
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
deviceInfo.currentIccid = '-';
|
||||||
|
deviceInfo.mchList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备实时信息
|
||||||
if (data.device_realtime) {
|
if (data.device_realtime) {
|
||||||
const rt = data.device_realtime;
|
const rt = data.device_realtime;
|
||||||
deviceInfo.onlineStatus = rt.online_status === 1 ? '1' : '0';
|
deviceInfo.onlineStatus = rt.online_status === 1 ? '1' : '0';
|
||||||
deviceInfo.battery = rt.battery_level || 100;
|
deviceInfo.battery = rt.battery_level ?? 0;
|
||||||
deviceInfo.ssidName = rt.ssid || '';
|
deviceInfo.ssidName = rt.ssid || '-';
|
||||||
deviceInfo.ssidPwd = rt.wifi_password || '';
|
deviceInfo.ssidPwd = rt.wifi_password || '-';
|
||||||
deviceInfo.lan_ip = rt.lan_ip || '';
|
deviceInfo.lan_ip = rt.lan_ip || '-';
|
||||||
deviceInfo.connCnt = rt.client_number || 0;
|
deviceInfo.connCnt = rt.client_number ?? 0;
|
||||||
|
deviceInfo.max_clients = rt.max_clients ?? 1;
|
||||||
deviceInfo.run_time = rt.run_time || 0;
|
deviceInfo.run_time = rt.run_time || 0;
|
||||||
deviceInfo.rssi = rt.rssi || '强';
|
deviceInfo.rssi = calculateSignalStrength(rt.rsrp, rt.rsrq, rt.rssi);
|
||||||
|
} else {
|
||||||
|
// 没有实时信息时设置默认值
|
||||||
|
deviceInfo.onlineStatus = '0';
|
||||||
|
deviceInfo.battery = 0;
|
||||||
|
deviceInfo.ssidName = '-';
|
||||||
|
deviceInfo.ssidPwd = '-';
|
||||||
|
deviceInfo.lan_ip = '-';
|
||||||
|
deviceInfo.connCnt = 0;
|
||||||
|
deviceInfo.max_clients = 1;
|
||||||
|
deviceInfo.run_time = 0;
|
||||||
|
deviceInfo.rssi = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.cards && data.cards.length > 0) {
|
// 到期时间由 TrafficCard 组件获取套餐信息时一并返回
|
||||||
deviceInfo.mchList = data.cards.map(card => ({
|
|
||||||
iccidMark: card.iccid,
|
|
||||||
category: card.slot_position
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载资产信息失败', e);
|
console.error('加载资产信息失败', e);
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算信号强度
|
||||||
|
const calculateSignalStrength = (rsrp, rsrq, rssi) => {
|
||||||
|
// 简单的信号强度计算逻辑
|
||||||
|
if (!rsrp && !rsrq && !rssi) return '-';
|
||||||
|
|
||||||
|
const rsrpNum = parseInt(rsrp);
|
||||||
|
if (isNaN(rsrpNum)) return '-';
|
||||||
|
|
||||||
|
if (rsrpNum >= -80) return '强';
|
||||||
|
if (rsrpNum >= -95) return '中';
|
||||||
|
return '弱';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理套餐加载完成事件(从 TrafficCard 组件传递过来)
|
||||||
|
const handlePackageLoaded = (activePackage) => {
|
||||||
|
if (activePackage && activePackage.expires_at) {
|
||||||
|
// 格式化到期时间
|
||||||
|
const date = new Date(activePackage.expires_at);
|
||||||
|
deviceInfo.expireDate =
|
||||||
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
deviceInfo.expireDate = '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const modifyWifi = () => {
|
const modifyWifi = () => {
|
||||||
wifi_info.ssid = deviceInfo.ssidName;
|
wifi_info.ssid = deviceInfo.ssidName;
|
||||||
wifi_info.pwd = deviceInfo.ssidPwd;
|
wifi_info.pwd = deviceInfo.ssidPwd;
|
||||||
@@ -249,26 +318,112 @@
|
|||||||
);
|
);
|
||||||
deviceInfo.ssidName = wifi_info.ssid;
|
deviceInfo.ssidName = wifi_info.ssid;
|
||||||
deviceInfo.ssidPwd = wifi_info.pwd;
|
deviceInfo.ssidPwd = wifi_info.pwd;
|
||||||
uni.showToast({ title: '修改成功', icon: 'success' });
|
uni.showToast({
|
||||||
|
title: '修改成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('修改WiFi失败', e);
|
console.error('修改WiFi失败', e);
|
||||||
}
|
}
|
||||||
showModifyWifi.value = false;
|
showModifyWifi.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectKF = () => uni.showToast({
|
const connectKF = () => {
|
||||||
title: '正在跳转客服',
|
const kfUrl =
|
||||||
icon: 'none'
|
'https://work.weixin.qq.com/kfid/kfc0fd79f27686fb65e?enc_scene=ENCfR9AVui6UfvvxySGVuvaG&scene_param=commonlink';
|
||||||
});
|
|
||||||
const enterBack = () => uni.showToast({
|
// #ifdef H5
|
||||||
title: '正在跳转后台',
|
window.location.href = kfUrl;
|
||||||
icon: 'none'
|
// #endif
|
||||||
});
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序跳转到企业微信客服
|
||||||
|
wx.openCustomerServiceChat({
|
||||||
|
extInfo: {
|
||||||
|
url: kfUrl
|
||||||
|
},
|
||||||
|
corpId: 'ww4e3b7f9f7c8a9b0c',
|
||||||
|
success: () => {
|
||||||
|
console.log('打开客服会话成功');
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('打开客服会话失败', err);
|
||||||
|
// 失败时使用网页跳转
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/webview/webview?url=${encodeURIComponent(kfUrl)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5 || MP-WEIXIN
|
||||||
|
// 其他平台,复制链接
|
||||||
|
uni.showModal({
|
||||||
|
title: '联系客服',
|
||||||
|
content: '是否复制客服链接?',
|
||||||
|
confirmText: '复制',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: kfUrl,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '客服链接已复制',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
};
|
||||||
|
|
||||||
|
const enterBack = () => {
|
||||||
|
if (!deviceInfo.lan_ip || deviceInfo.lan_ip === '-') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无后台地址',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到设备后台管理地址
|
||||||
|
const url = deviceInfo.lan_ip.startsWith('http') ? deviceInfo.lan_ip : `http://${deviceInfo.lan_ip}`;
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
window.location.href = url;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.showModal({
|
||||||
|
title: '后台地址',
|
||||||
|
content: url,
|
||||||
|
confirmText: '复制',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '地址已复制',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
};
|
||||||
|
|
||||||
const YesRestart = async () => {
|
const YesRestart = async () => {
|
||||||
try {
|
try {
|
||||||
await deviceApi.reboot(userStore.state.identifier);
|
await deviceApi.reboot(userStore.state.identifier);
|
||||||
uni.showToast({ title: '重启指令已发送', icon: 'success' });
|
uni.showToast({
|
||||||
|
title: '重启指令已发送',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('重启设备失败', e);
|
console.error('重启设备失败', e);
|
||||||
}
|
}
|
||||||
@@ -288,7 +443,10 @@
|
|||||||
const YesRecover = async () => {
|
const YesRecover = async () => {
|
||||||
try {
|
try {
|
||||||
await deviceApi.factoryReset(userStore.state.identifier);
|
await deviceApi.factoryReset(userStore.state.identifier);
|
||||||
uni.showToast({ title: '恢复出厂设置指令已发送', icon: 'success' });
|
uni.showToast({
|
||||||
|
title: '恢复出厂设置指令已发送',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('恢复出厂设置失败', e);
|
console.error('恢复出厂设置失败', e);
|
||||||
}
|
}
|
||||||
@@ -425,8 +583,62 @@
|
|||||||
showSmsCode.value = false;
|
showSmsCode.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSync = () => {
|
const onSync = async () => {
|
||||||
uni.showToast({ title: '同步成功', icon: 'success' });
|
const identifier = userStore.state.identifier;
|
||||||
|
if (!identifier) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请先登录',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '同步中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
@@ -460,8 +672,16 @@
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'change-phone':
|
case 'change-phone':
|
||||||
|
if (!boundPhone.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请先去绑定手机号',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/change-phone/change-phone'
|
url: `/pages/change-phone/change-phone?oldPhone=${boundPhone.value}`
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'device-exchange':
|
case 'device-exchange':
|
||||||
@@ -500,16 +720,19 @@
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wifi-popup {
|
.wifi-popup {
|
||||||
width: 600rpx;
|
width: 600rpx;
|
||||||
padding: 30rpx;
|
padding: var(--space-6);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 32rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-spacer {
|
.bottom-spacer {
|
||||||
height: 120rpx;
|
height: 140rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,14 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="login-page">
|
||||||
<view class="card">
|
<view class="overlay"></view>
|
||||||
<view class="title-login">
|
<view class="animated-bg">
|
||||||
登录
|
<view class="circle circle-1"></view>
|
||||||
|
<view class="circle circle-2"></view>
|
||||||
|
<view class="circle circle-3"></view>
|
||||||
|
<view class="circle circle-4"></view>
|
||||||
|
<view class="circle circle-5"></view>
|
||||||
|
<view class="shape shape-triangle"></view>
|
||||||
|
<view class="shape shape-square"></view>
|
||||||
|
<view class="shape shape-pentagon"></view>
|
||||||
|
<view class="shape shape-hexagon"></view>
|
||||||
|
</view>
|
||||||
|
<view class="content-wrapper">
|
||||||
|
<view class="content">
|
||||||
|
<view class="header">
|
||||||
|
<view class="main-title">登录</view>
|
||||||
|
<view class="sub-title">请输入标识进行登录</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-section">
|
||||||
|
<view class="input-wrap" :class="{ focus: inputFocus }">
|
||||||
|
<input
|
||||||
|
v-model="identifier"
|
||||||
|
class="input"
|
||||||
|
placeholder="请输入标识"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
@focus="inputFocus = true"
|
||||||
|
@blur="inputFocus = false"
|
||||||
|
@input="showError = false"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text v-if="showError" class="error">请输入标识</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="input">
|
|
||||||
<up-input class="mt-30" v-model="identifier" placeholder="请输入SN/IMEI/虚拟号/ICCID/MSISDN"></up-input>
|
<view class="btn-section">
|
||||||
|
<button
|
||||||
|
class="btn-login"
|
||||||
|
:class="{ disabled: !identifier || loading || !agreed }"
|
||||||
|
:disabled="!identifier || loading || !agreed"
|
||||||
|
@tap="handleLogin"
|
||||||
|
>
|
||||||
|
<view v-if="loading" class="loading">
|
||||||
|
<view class="dot"/>
|
||||||
|
<view class="dot"/>
|
||||||
|
<view class="dot"/>
|
||||||
|
</view>
|
||||||
|
<text v-else>登录</text>
|
||||||
|
</button>
|
||||||
|
</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 class="button">
|
|
||||||
<up-button class="mt-30 btn-apple btn-primary" type="primary" :loading="loading" @click="login">立即登录</up-button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -18,14 +67,15 @@
|
|||||||
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 } from '@/utils/env.js';
|
import { APP_ID, USE_WECHAT_AUTH } from '@/utils/env.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const identifier = ref('');
|
const identifier = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const agreed = ref(true);
|
||||||
const isWechat = /micromessenger/i.test(navigator.userAgent);
|
const inputFocus = ref(false);
|
||||||
|
const showError = ref(false);
|
||||||
|
|
||||||
const getCode = () => {
|
const getCode = () => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -37,28 +87,40 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const redirectToWxAuth = (assetToken) => {
|
const redirectToWxAuth = (assetToken) => {
|
||||||
const redirectUri = encodeURIComponent(window.location.href.split('?')[0]);
|
sessionStorage.setItem('assetToken', assetToken);
|
||||||
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${assetToken}#wechat_redirect`;
|
const redirectUri = encodeURIComponent('https://cmp-c.boss160.cn/');
|
||||||
|
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 login = async () => {
|
const handleLogin = () => {
|
||||||
if (!identifier.value) {
|
if (!identifier.value) {
|
||||||
uni.showToast({ title: '请输入SN/IMEI/虚拟号/ICCID/MSISDN', icon: 'none' });
|
showError.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
doLogin();
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLogin = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const verifyData = await authApi.verifyAsset(identifier.value);
|
const verifyData = await authApi.verifyAsset(identifier.value);
|
||||||
userStore.setAssetToken(verifyData.asset_token);
|
userStore.setAssetToken(verifyData.asset_token);
|
||||||
userStore.setIdentifier(identifier.value);
|
userStore.setIdentifier(identifier.value);
|
||||||
|
|
||||||
// if (!isWechat) {
|
if (!USE_WECHAT_AUTH) {
|
||||||
// uni.showToast({ title: '请在微信中打开', icon: 'none' });
|
const loginData = await authApi.wechatLogin(verifyData.asset_token, 'mock_code');
|
||||||
// loading.value = false;
|
userStore.setToken(loginData.token);
|
||||||
// return;
|
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) {
|
||||||
@@ -68,8 +130,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleWechatCallback = async () => {
|
const handleWechatCallback = async () => {
|
||||||
|
if (!USE_WECHAT_AUTH) return;
|
||||||
|
|
||||||
const code = getCode();
|
const code = getCode();
|
||||||
const assetToken = userStore.state.assetToken;
|
const assetToken = sessionStorage.getItem('assetToken');
|
||||||
|
|
||||||
if (!code || !assetToken) return;
|
if (!code || !assetToken) return;
|
||||||
|
|
||||||
@@ -79,9 +143,48 @@
|
|||||||
try {
|
try {
|
||||||
const loginData = await authApi.wechatLogin(assetToken, code);
|
const loginData = await authApi.wechatLogin(assetToken, code);
|
||||||
userStore.setToken(loginData.token);
|
userStore.setToken(loginData.token);
|
||||||
|
|
||||||
uni.setStorageSync('identifier', userStore.state.identifier);
|
uni.setStorageSync('identifier', userStore.state.identifier);
|
||||||
uni.redirectTo({ url: '/pages/index/index' });
|
|
||||||
|
// H5 环境获取微信用户信息
|
||||||
|
// #ifdef H5
|
||||||
|
try {
|
||||||
|
// 通过 uni.getUserProfile 获取用户信息(需要用户点击授权)
|
||||||
|
// 注意:这个方法在微信公众号 H5 中不可用,需要通过后端接口获取
|
||||||
|
// 这里暂时保存默认信息,实际应该由后端返回用户信息
|
||||||
|
if (loginData.user_info) {
|
||||||
|
userStore.setUserInfo({
|
||||||
|
avatar: loginData.user_info.avatar || '',
|
||||||
|
nickname: loginData.user_info.nickname || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('获取用户信息失败', err);
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 小程序环境可以直接获取用户信息
|
||||||
|
try {
|
||||||
|
const { userInfo } = await uni.getUserProfile({
|
||||||
|
desc: '用于完善用户资料'
|
||||||
|
});
|
||||||
|
userStore.setUserInfo({
|
||||||
|
avatar: userInfo.avatarUrl || '',
|
||||||
|
nickname: userInfo.nickName || ''
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log('用户取消授权');
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 判断是否需要绑定手机号
|
||||||
|
if (loginData.need_bind_phone) {
|
||||||
|
// 需要绑定手机号,跳转到绑定页面
|
||||||
|
uni.redirectTo({ url: '/pages/bind/bind?fromLogin=true' });
|
||||||
|
} else {
|
||||||
|
// 不需要绑定,直接进入首页
|
||||||
|
uni.redirectTo({ url: '/pages/index/index' });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('微信登录失败', e);
|
console.error('微信登录失败', e);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -94,23 +197,380 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.login-page {
|
||||||
padding-top: 30vh;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(135deg, #005cbf 0%, #007aff 50%, #4da6ff 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.title-login {
|
.overlay {
|
||||||
color: var(--text-primary);
|
position: absolute;
|
||||||
font-size: 24px;
|
top: 0;
|
||||||
text-align: center;
|
left: 0;
|
||||||
margin: 20px 0;
|
right: 0;
|
||||||
font-weight: bold;
|
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% {
|
||||||
.input {
|
transform: translate(100rpx, -80rpx) scale(1.2);
|
||||||
margin: 70rpx 0 50rpx 0;
|
opacity: 0.5;
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-bottom: 40rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
position: relative;
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
|
text-shadow: 0 2rpx 8rpx rgba(64, 40, 128, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.5;
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
|
text-shadow: 0 1rpx 4rpx rgba(64, 40, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 32rpx 40rpx;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(64, 40, 128, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrap.focus {
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(64, 40, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #1A1A1A;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #C4C4C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #EF4444;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-section {
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
height: 108rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login.disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:active:not(.disabled) {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 10rpx;
|
||||||
|
height: 10rpx;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: load 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(1) { animation-delay: -0.32s; }
|
||||||
|
.dot:nth-child(2) { animation-delay: -0.16s; }
|
||||||
|
|
||||||
|
@keyframes load {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="wallet-card">
|
<view class="wallet-card">
|
||||||
<view class="balance-section">
|
<view class="balance-section">
|
||||||
<view class="balance-label">账户余额(元)</view>
|
<view class="balance-header">
|
||||||
|
<view class="balance-label">账户余额(元)</view>
|
||||||
|
<view class="recharge-btn-wrapper">
|
||||||
|
<button class="btn-recharge" @tap="showRechargeModal = true">充值</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view class="balance-amount">{{ formatMoney(walletDetail.balance) }}</view>
|
<view class="balance-amount">{{ formatMoney(walletDetail.balance) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="wallet-footer">
|
<view class="wallet-footer">
|
||||||
@@ -13,7 +18,7 @@
|
|||||||
<view class="footer-divider"></view>
|
<view class="footer-divider"></view>
|
||||||
<view class="footer-item">
|
<view class="footer-item">
|
||||||
<view class="footer-label">更新时间</view>
|
<view class="footer-label">更新时间</view>
|
||||||
<view class="footer-value">{{ walletDetail.updated_at }}</view>
|
<view class="footer-value">{{ formatDateTime(walletDetail.updated_at) }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -22,26 +27,28 @@
|
|||||||
<view class="tab-header">
|
<view class="tab-header">
|
||||||
<view class="tab-indicator" :style="{ left: currentTab === 0 ? '8rpx' : '50%' }"></view>
|
<view class="tab-indicator" :style="{ left: currentTab === 0 ? '8rpx' : '50%' }"></view>
|
||||||
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="onTabTap(0)">
|
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="onTabTap(0)">
|
||||||
<text>充值记录</text>
|
<text>充值订单</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="tab-item" :class="{ active: currentTab === 1 }" @tap="onTabTap(1)">
|
<view class="tab-item" :class="{ active: currentTab === 1 }" @tap="onTabTap(1)">
|
||||||
<text>钱包流水</text>
|
<text>钱包流水</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<swiper class="tab-swiper" :current="currentTab" @change="onSwiperChange" :circular="false" :acceleration="true">
|
<swiper class="tab-swiper" :current="currentTab" @change="onSwiperChange" :circular="false"
|
||||||
|
: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">
|
||||||
<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>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else>
|
<view v-else>
|
||||||
<view class="card record-card" v-for="(item, index) in rechargeList" :key="index">
|
<view class="card record-card" v-for="(item, index) in rechargeList" :key="index">
|
||||||
<view class="record-header flex-row-sb">
|
<view class="record-header flex-row-sb">
|
||||||
<view class="record-no">{{ item.recharge_no }}</view>
|
<view class="record-no">{{ item.recharge_order_no || item.recharge_no }}</view>
|
||||||
<view class="tag-apple" :class="getRechargeStatusClass(item.status)">{{ item.status_name }}</view>
|
<view class="tag-apple" :class="getRechargeStatusClass(item.status)">
|
||||||
|
{{ item.status_name }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-info">
|
<view class="record-info">
|
||||||
<view class="info-row flex-row-sb">
|
<view class="info-row flex-row-sb">
|
||||||
@@ -54,13 +61,17 @@
|
|||||||
</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.auto_purchase_status === '1' ? '已开启' : '未开启' }}</view>
|
<view class="info-value">
|
||||||
|
{{ getAutoPurchaseStatusText(item.auto_purchase_status) }}</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>
|
</view>
|
||||||
|
<view v-if="item.status === 0" class="record-actions">
|
||||||
|
<button class="btn-pay" @tap="handleRechargePayment(item)">立即支付</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="load-more" v-if="rechargeList.length > 0">
|
<view class="load-more" v-if="rechargeList.length > 0">
|
||||||
@@ -83,7 +94,8 @@
|
|||||||
<view class="card record-card" v-for="(item, index) in transactionList" :key="index">
|
<view class="card record-card" v-for="(item, index) in transactionList" :key="index">
|
||||||
<view class="record-header flex-row-sb">
|
<view class="record-header flex-row-sb">
|
||||||
<view class="record-type">{{ item.type_name }}</view>
|
<view class="record-type">{{ item.type_name }}</view>
|
||||||
<view class="record-amount" :class="item.amount >= 0 ? 'text-success' : 'text-danger'">
|
<view class="record-amount"
|
||||||
|
:class="item.amount >= 0 ? 'text-success' : 'text-danger'">
|
||||||
{{ item.amount >= 0 ? '+' : '' }}{{ formatMoney(item.amount) }}
|
{{ item.amount >= 0 ? '+' : '' }}{{ formatMoney(item.amount) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -116,14 +128,55 @@
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
|
|
||||||
|
<!-- 充值弹窗 -->
|
||||||
|
<view>
|
||||||
|
<up-popup :show="showRechargeModal" mode="center" @close="showRechargeModal=false">
|
||||||
|
<view class="recharge-popup">
|
||||||
|
<view class="popup-header">
|
||||||
|
<view class="popup-title">账户充值</view>
|
||||||
|
<view class="popup-close" @tap="showRechargeModal=false">×</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="recharge-amounts">
|
||||||
|
<view class="amount-item" v-for="item in rechargeAmounts" :key="item.value"
|
||||||
|
:class="{ active: selectedAmount === item.value }" @tap="selectAmount(item.value)">
|
||||||
|
<text class="amount-value">¥{{ formatDisplayMoney(item.label) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="amount-item custom" :class="{ active: isCustomAmount }"
|
||||||
|
@tap="selectCustomAmount">
|
||||||
|
<text class="amount-value">自定义</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="custom-input" v-if="isCustomAmount">
|
||||||
|
<view class="input-label">请输入充值金额(元)</view>
|
||||||
|
<up-input v-model="customAmount" type="number" placeholder="最低0.01元" border="surround" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="popup-footer">
|
||||||
|
<button class="btn-apple btn-secondary" @tap="showRechargeModal=false">取消</button>
|
||||||
|
<button class="btn-apple btn-primary" @tap="confirmRecharge">确认充值</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</up-popup>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import {
|
||||||
import { walletApi } from '@/api/index.js';
|
ref,
|
||||||
import { useUserStore } from '@/store/index.js';
|
reactive,
|
||||||
|
onMounted
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
walletApi
|
||||||
|
} from '@/api/index.js';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -142,6 +195,23 @@
|
|||||||
return (amount / 100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
return (amount / 100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDisplayMoney = (amount) => {
|
||||||
|
if (!amount && amount !== 0) return '0';
|
||||||
|
// 直接格式化显示的数字(已经是元),添加千分号
|
||||||
|
return parseFloat(amount).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateStr) => {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
};
|
||||||
|
|
||||||
let walletDetail = reactive({
|
let walletDetail = reactive({
|
||||||
balance: 0,
|
balance: 0,
|
||||||
frozen_balance: 0,
|
frozen_balance: 0,
|
||||||
@@ -160,15 +230,236 @@
|
|||||||
let transactionPage = ref(1);
|
let transactionPage = ref(1);
|
||||||
const transactionPageSize = 5;
|
const transactionPageSize = 5;
|
||||||
|
|
||||||
|
let showRechargeModal = ref(false);
|
||||||
|
let selectedAmount = ref(null);
|
||||||
|
let isCustomAmount = ref(false);
|
||||||
|
let customAmount = ref('');
|
||||||
|
const rechargeAmounts = [{
|
||||||
|
value: 1000,
|
||||||
|
label: '10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2000,
|
||||||
|
label: '20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 5000,
|
||||||
|
label: '50'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 10000,
|
||||||
|
label: '100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 20000,
|
||||||
|
label: '200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 50000,
|
||||||
|
label: '500'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectAmount = (value) => {
|
||||||
|
selectedAmount.value = value;
|
||||||
|
isCustomAmount.value = false;
|
||||||
|
customAmount.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCustomAmount = () => {
|
||||||
|
isCustomAmount.value = true;
|
||||||
|
selectedAmount.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRecharge = async () => {
|
||||||
|
let amount = 0;
|
||||||
|
|
||||||
|
if (isCustomAmount.value) {
|
||||||
|
// 自定义金额
|
||||||
|
const customAmountNum = parseFloat(customAmount.value);
|
||||||
|
if (!customAmountNum || customAmountNum < 0.01) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请输入正确的充值金额',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
amount = Math.round(customAmountNum * 100); // 转换为分
|
||||||
|
} else if (selectedAmount.value) {
|
||||||
|
// 预设金额
|
||||||
|
amount = selectedAmount.value;
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请选择充值金额',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '处理中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 第一步:充值前校验
|
||||||
|
const checkData = await walletApi.rechargeCheck(userStore.state.identifier);
|
||||||
|
|
||||||
|
// 如果需要强制充值且金额小于强制充值金额
|
||||||
|
if (checkData.need_force_recharge && amount < checkData.force_recharge_amount) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: `${checkData.message || '当前需充值最低金额'} ¥${(checkData.force_recharge_amount / 100).toFixed(2)}`,
|
||||||
|
showCancel: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查金额范围
|
||||||
|
if (amount < checkData.min_amount || amount > checkData.max_amount) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({
|
||||||
|
title: `充值金额范围:¥${(checkData.min_amount / 100).toFixed(2)} - ¥${(checkData.max_amount / 100).toFixed(2)}`,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2500
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:创建充值订单
|
||||||
|
const rechargeData = await walletApi.recharge(
|
||||||
|
userStore.state.identifier,
|
||||||
|
amount,
|
||||||
|
'wechat'
|
||||||
|
);
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
showRechargeModal.value = false;
|
||||||
|
|
||||||
|
// 验证支付配置
|
||||||
|
if (!rechargeData.pay_config || !rechargeData.pay_config.package) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付参数获取失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三步:调起微信支付
|
||||||
|
// #ifdef H5
|
||||||
|
if (typeof WeixinJSBridge === 'undefined') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请在微信中打开',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||||
|
appId: rechargeData.pay_config.app_id,
|
||||||
|
timeStamp: rechargeData.pay_config.timestamp,
|
||||||
|
nonceStr: rechargeData.pay_config.nonce_str,
|
||||||
|
package: rechargeData.pay_config.package,
|
||||||
|
signType: rechargeData.pay_config.sign_type,
|
||||||
|
paySign: rechargeData.pay_config.pay_sign
|
||||||
|
}, function(res) {
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
console.log('支付成功', res);
|
||||||
|
uni.showToast({
|
||||||
|
title: '充值成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
loadWalletDetail();
|
||||||
|
rechargePage.value = 1;
|
||||||
|
rechargeNoMore.value = false;
|
||||||
|
loadRechargeList();
|
||||||
|
}, 1500);
|
||||||
|
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('支付失败', res);
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.requestPayment({
|
||||||
|
timeStamp: rechargeData.pay_config.timestamp,
|
||||||
|
nonceStr: rechargeData.pay_config.nonce_str,
|
||||||
|
package: rechargeData.pay_config.package,
|
||||||
|
signType: rechargeData.pay_config.sign_type,
|
||||||
|
paySign: rechargeData.pay_config.pay_sign,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('支付成功', res);
|
||||||
|
uni.showToast({
|
||||||
|
title: '充值成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
loadWalletDetail();
|
||||||
|
rechargePage.value = 1;
|
||||||
|
rechargeNoMore.value = false;
|
||||||
|
loadRechargeList();
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('支付失败', err);
|
||||||
|
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error('充值失败', e);
|
||||||
|
const errorMsg = e.msg || e.message || '充值失败,请稍后重试';
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getRechargeStatusClass = (status) => {
|
const getRechargeStatusClass = (status) => {
|
||||||
const classMap = {
|
const classMap = {
|
||||||
'0': 'tag-warning',
|
0: 'tag-warning', // 待支付
|
||||||
'1': 'tag-success',
|
1: 'tag-success', // 已支付
|
||||||
'2': 'tag-danger'
|
2: 'tag-danger', // 已关闭
|
||||||
|
3: 'tag-danger' // 已退款
|
||||||
};
|
};
|
||||||
return classMap[status] || '';
|
return classMap[status] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAutoPurchaseStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': '处理中',
|
||||||
|
'success': '已完成',
|
||||||
|
'failed': '失败',
|
||||||
|
'1': '已开启', // 兼容旧版
|
||||||
|
'0': '未开启' // 兼容旧版
|
||||||
|
};
|
||||||
|
return statusMap[status] || '未开启';
|
||||||
|
};
|
||||||
|
|
||||||
const loadWalletDetail = async () => {
|
const loadWalletDetail = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await walletApi.getDetail(userStore.state.identifier);
|
const data = await walletApi.getDetail(userStore.state.identifier);
|
||||||
@@ -185,16 +476,49 @@
|
|||||||
rechargeLoading.value = true;
|
rechargeLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await walletApi.getRecharges(
|
// 使用新的充值订单接口
|
||||||
|
const data = await walletApi.getRechargeOrders(
|
||||||
userStore.state.identifier,
|
userStore.state.identifier,
|
||||||
rechargePage.value,
|
rechargePage.value,
|
||||||
rechargePageSize
|
rechargePageSize
|
||||||
);
|
);
|
||||||
|
|
||||||
const newData = (data.items || []).map(item => ({
|
const newData = (data.items || []).map(item => {
|
||||||
...item,
|
// 格式化时间
|
||||||
status_name: item.status === 1 ? '已支付' : item.status === 0 ? '待支付' : '已关闭'
|
let formattedCreatedAt = item.created_at;
|
||||||
}));
|
if (formattedCreatedAt && formattedCreatedAt.includes('T')) {
|
||||||
|
formattedCreatedAt = formattedCreatedAt.replace('T', ' ').slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端已返回 status_name,优先使用
|
||||||
|
if (item.status_name) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
created_at: formattedCreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 兼容旧版本,手动映射
|
||||||
|
let statusName = '未知';
|
||||||
|
switch (item.status) {
|
||||||
|
case 0:
|
||||||
|
statusName = '待支付';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
statusName = '已支付';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
statusName = '已关闭';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
statusName = '已退款';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
status_name: statusName,
|
||||||
|
created_at: formattedCreatedAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
rechargeList.push(...newData);
|
rechargeList.push(...newData);
|
||||||
@@ -213,6 +537,120 @@
|
|||||||
rechargeLoading.value = false;
|
rechargeLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRechargePayment = async (rechargeOrder) => {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '处理中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用订单金额重新调用充值接口获取支付配置
|
||||||
|
const rechargeData = await walletApi.recharge(
|
||||||
|
userStore.state.identifier,
|
||||||
|
rechargeOrder.amount,
|
||||||
|
'wechat'
|
||||||
|
);
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
// 验证支付配置
|
||||||
|
if (!rechargeData.pay_config || !rechargeData.pay_config.package) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付参数获取失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调起微信支付
|
||||||
|
// #ifdef H5
|
||||||
|
if (typeof WeixinJSBridge === 'undefined') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请在微信中打开',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||||
|
appId: rechargeData.pay_config.app_id,
|
||||||
|
timeStamp: rechargeData.pay_config.timestamp,
|
||||||
|
nonceStr: rechargeData.pay_config.nonce_str,
|
||||||
|
package: rechargeData.pay_config.package,
|
||||||
|
signType: rechargeData.pay_config.sign_type,
|
||||||
|
paySign: rechargeData.pay_config.pay_sign
|
||||||
|
}, function(res) {
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '充值成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
loadWalletDetail();
|
||||||
|
rechargePage.value = 1;
|
||||||
|
rechargeNoMore.value = false;
|
||||||
|
loadRechargeList();
|
||||||
|
}, 1500);
|
||||||
|
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.requestPayment({
|
||||||
|
timeStamp: rechargeData.pay_config.timestamp,
|
||||||
|
nonceStr: rechargeData.pay_config.nonce_str,
|
||||||
|
package: rechargeData.pay_config.package,
|
||||||
|
signType: rechargeData.pay_config.sign_type,
|
||||||
|
paySign: rechargeData.pay_config.pay_sign,
|
||||||
|
success: (res) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '充值成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
loadWalletDetail();
|
||||||
|
rechargePage.value = 1;
|
||||||
|
rechargeNoMore.value = false;
|
||||||
|
loadRechargeList();
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error('获取支付配置失败', e);
|
||||||
|
const errorMsg = e.msg || e.message || '支付失败,请稍后重试';
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadTransactionList = async (append = false) => {
|
const loadTransactionList = async (append = false) => {
|
||||||
if (transactionLoading.value || transactionNoMore.value) return;
|
if (transactionLoading.value || transactionNoMore.value) return;
|
||||||
transactionLoading.value = true;
|
transactionLoading.value = true;
|
||||||
@@ -224,10 +662,19 @@
|
|||||||
transactionPageSize
|
transactionPageSize
|
||||||
);
|
);
|
||||||
|
|
||||||
const newData = (data.items || []).map(item => ({
|
const newData = (data.items || []).map(item => {
|
||||||
...item,
|
// 格式化时间
|
||||||
type_name: item.type === 'recharge' ? '充值' : item.type === 'purchase' ? '消费' : '其他'
|
let formattedCreatedAt = item.created_at;
|
||||||
}));
|
if (formattedCreatedAt && formattedCreatedAt.includes('T')) {
|
||||||
|
formattedCreatedAt = formattedCreatedAt.replace('T', ' ').slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
type_name: item.type === 'recharge' ? '充值' : item.type === 'purchase' ? '消费' : '其他',
|
||||||
|
created_at: formattedCreatedAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
transactionList.push(...newData);
|
transactionList.push(...newData);
|
||||||
@@ -284,8 +731,41 @@
|
|||||||
|
|
||||||
.balance-section {
|
.balance-section {
|
||||||
margin-bottom: 28rpx;
|
margin-bottom: 28rpx;
|
||||||
.balance-label { font-size: 24rpx; opacity: 0.75; margin-bottom: 8rpx; }
|
|
||||||
.balance-amount { font-size: 72rpx; font-weight: 700; letter-spacing: -2rpx; }
|
.balance-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
.balance-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-btn-wrapper {
|
||||||
|
.btn-recharge {
|
||||||
|
background: #fff;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
font-size: 72rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -2rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-footer {
|
.wallet-footer {
|
||||||
@@ -293,11 +773,27 @@
|
|||||||
align-items: center;
|
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 { width: 1rpx; height: 40rpx; background: rgba(255, 255, 255, 0.3); margin: 0 32rpx; }
|
|
||||||
|
.footer-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
margin: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-item {
|
.footer-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.footer-label { font-size: 22rpx; opacity: 0.7; margin-bottom: 6rpx; }
|
|
||||||
.footer-value { font-size: 26rpx; font-weight: 600; }
|
.footer-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +804,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.tab-header {
|
.tab-header {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -315,6 +812,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
padding: 8rpx;
|
padding: 8rpx;
|
||||||
|
|
||||||
.tab-indicator {
|
.tab-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8rpx;
|
top: 8rpx;
|
||||||
@@ -325,6 +823,7 @@
|
|||||||
transition: left 0.3s ease;
|
transition: left 0.3s ease;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -337,6 +836,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -363,17 +863,68 @@
|
|||||||
|
|
||||||
.record-card {
|
.record-card {
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
.record-header {
|
.record-header {
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
.record-no { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
|
||||||
.record-type { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
.record-no {
|
||||||
.record-amount { font-size: 32rpx; font-weight: 700; }
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-type {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-amount {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-info {
|
.record-info {
|
||||||
.info-row {
|
.info-row {
|
||||||
padding: 10rpx 0;
|
padding: 10rpx 0;
|
||||||
.info-label { font-size: 24rpx; color: var(--text-tertiary); }
|
|
||||||
.info-value { font-size: 24rpx; color: var(--text-primary); max-width: 380rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.info-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--text-primary);
|
||||||
|
max-width: 380rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-actions {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.btn-pay {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
width:100%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,8 +935,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 120rpx 40rpx;
|
padding: 120rpx 40rpx;
|
||||||
.empty-icon { font-size: 100rpx; margin-bottom: 24rpx; opacity: 0.5; }
|
|
||||||
.empty-title { font-size: 28rpx; font-weight: 600; color: var(--text-secondary); }
|
.empty-icon {
|
||||||
|
font-size: 100rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
@@ -394,4 +955,110 @@
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recharge-popup {
|
||||||
|
width: 600rpx;
|
||||||
|
max-width: 90vw;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-large);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid var(--border-light);
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-amounts {
|
||||||
|
padding: 30rpx;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.amount-item {
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2rpx solid var(--border-light);
|
||||||
|
border-radius: var(--radius-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(0, 122, 255, 0.05);
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input {
|
||||||
|
padding: 0 30rpx 30rpx;
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-top: 1rpx solid var(--border-light);
|
||||||
|
|
||||||
|
.btn-apple {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-medium);
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.btn-secondary {
|
||||||
|
background: var(--gray-200);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,50 +1,83 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<view class="filter-tabs">
|
||||||
|
<view v-for="(item, index) in filterOptions" :key="index" class="tab-item"
|
||||||
|
:class="{ active: filterIndex === index }" @tap="onFilterChange(index)">
|
||||||
|
{{ item.label }}
|
||||||
|
<view v-if="filterIndex === index" class="tab-line"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="orderList.length === 0 && !loading" class="empty-state">
|
<view v-if="orderList.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>
|
||||||
|
|
||||||
<view v-else class="card order-card" v-for="(item, index) in orderList" :key="index">
|
<scroll-view v-else scroll-y class="order-scroll" @scrolltolower="loadMore">
|
||||||
<view class="order-header flex-row-sb">
|
<view class="order-card" v-for="(item, index) in orderList" :key="index">
|
||||||
<view class="order-no">{{ item.order_no }}</view>
|
<view class="card-header">
|
||||||
<view class="tag-apple" :class="getStatusClass(item.payment_status)">{{ item.payment_status_name }}</view>
|
<view class="header-left">
|
||||||
</view>
|
<view class="header-row">
|
||||||
|
<text class="field-tag">订单号</text>
|
||||||
|
<text class="order-no">{{ item.order_no }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-badge" :class="getStatusClass(item.payment_status)">
|
||||||
|
{{ item.payment_status_name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="order-info">
|
<view class="card-body">
|
||||||
<view class="info-row flex-row-sb">
|
<view class="info-grid">
|
||||||
<view class="info-label">下单时间</view>
|
<view class="info-item">
|
||||||
<view class="info-value">{{ item.created_at }}</view>
|
<view class="info-label">订单时间</view>
|
||||||
|
<view class="info-value">{{ item.created_at }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">订单金额</view>
|
||||||
|
<view class="info-value amount">¥{{ formatMoney(item.total_amount) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="divider"></view>
|
||||||
|
|
||||||
|
<view class="package-section">
|
||||||
|
<view class="section-label">套餐名称</view>
|
||||||
|
<view class="package-list">
|
||||||
|
<view class="package-item" v-for="(pkgName, pIndex) in item.package_names" :key="pIndex">
|
||||||
|
<text class="package-name">{{ pkgName }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="item.payment_status === 1" class="card-footer">
|
||||||
|
<button class="btn-pay" @tap="handleOrderPayment(item)">立即支付</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="divider"></view>
|
<view class="load-more" v-if="orderList.length > 0">
|
||||||
|
<text v-if="loading">加载中...</text>
|
||||||
<view class="package-list">
|
<text v-else-if="noMore">没有更多了</text>
|
||||||
<view class="package-item" v-for="(pkgName, pIndex) in item.package_names" :key="pIndex">
|
<text v-else>上拉加载更多</text>
|
||||||
<view class="package-name">{{ pkgName }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
<view class="order-footer flex-row-sb">
|
|
||||||
<view class="total-label">合计</view>
|
|
||||||
<view class="total-amount">¥{{ formatMoney(item.total_amount) }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="load-more" v-if="orderList.length > 0">
|
|
||||||
<text v-if="loading">加载中...</text>
|
|
||||||
<text v-else-if="noMore">没有更多了</text>
|
|
||||||
<text v-else @tap="loadMore">点击加载更多</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import {
|
||||||
import { orderApi } from '@/api/index.js';
|
ref,
|
||||||
import { useUserStore } from '@/store/index.js';
|
reactive,
|
||||||
|
onMounted
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
orderApi
|
||||||
|
} from '@/api/index.js';
|
||||||
|
import {
|
||||||
|
useUserStore
|
||||||
|
} from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
@@ -53,6 +86,28 @@
|
|||||||
let noMore = ref(false);
|
let noMore = ref(false);
|
||||||
let page = ref(1);
|
let page = ref(1);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
let filterIndex = ref(0);
|
||||||
|
let filterOptions = [{
|
||||||
|
label: '全部',
|
||||||
|
value: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '待支付',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已支付',
|
||||||
|
value: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已取消',
|
||||||
|
value: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已退款',
|
||||||
|
value: 4
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const formatMoney = (amount) => {
|
const formatMoney = (amount) => {
|
||||||
if (!amount && amount !== 0) return '0.00';
|
if (!amount && amount !== 0) return '0.00';
|
||||||
@@ -61,27 +116,40 @@
|
|||||||
|
|
||||||
const getStatusClass = (status) => {
|
const getStatusClass = (status) => {
|
||||||
const classMap = {
|
const classMap = {
|
||||||
'0': 'tag-warning',
|
1: 'tag-warning',
|
||||||
'1': 'tag-success',
|
2: 'tag-success',
|
||||||
'2': 'tag-danger'
|
3: 'tag-secondary',
|
||||||
|
4: 'tag-info'
|
||||||
};
|
};
|
||||||
return classMap[status] || '';
|
return classMap[status] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFilterChange = (index) => {
|
||||||
|
filterIndex.value = index;
|
||||||
|
page.value = 1;
|
||||||
|
noMore.value = false;
|
||||||
|
loadOrderList();
|
||||||
|
};
|
||||||
|
|
||||||
const loadOrderList = async (append = false) => {
|
const loadOrderList = async (append = false) => {
|
||||||
if (loading.value || noMore.value) return;
|
if (loading.value || noMore.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
|
const paymentStatus = filterOptions[filterIndex.value].value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await orderApi.getList(
|
const data = await orderApi.getList(
|
||||||
userStore.state.identifier,
|
userStore.state.identifier,
|
||||||
page.value,
|
page.value,
|
||||||
pageSize
|
pageSize,
|
||||||
|
paymentStatus
|
||||||
);
|
);
|
||||||
|
|
||||||
const newData = (data.items || []).map(item => ({
|
const newData = (data.items || []).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
payment_status_name: item.payment_status === 1 ? '已支付' : item.payment_status === 0 ? '待支付' : '已取消',
|
payment_status_name: item.payment_status === 1 ? '待支付' : item.payment_status === 2 ?
|
||||||
|
'已支付' : item.payment_status === 3 ? '已取消' : item.payment_status === 4 ? '已退款' :
|
||||||
|
'未知',
|
||||||
created_at: item.created_at ? item.created_at.split('T').join(' ').slice(0, 19) : ''
|
created_at: item.created_at ? item.created_at.split('T').join(' ').slice(0, 19) : ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -108,6 +176,111 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOrderPayment = async (order) => {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '处理中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payData = await orderApi.pay(order.order_id, 'wechat');
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (!payData.pay_config || !payData.pay_config.package) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付参数获取失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (typeof WeixinJSBridge === 'undefined') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请在微信中打开',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||||
|
appId: payData.pay_config.app_id,
|
||||||
|
timeStamp: payData.pay_config.timestamp,
|
||||||
|
nonceStr: payData.pay_config.nonce_str,
|
||||||
|
package: payData.pay_config.package,
|
||||||
|
signType: payData.pay_config.sign_type,
|
||||||
|
paySign: payData.pay_config.pay_sign
|
||||||
|
}, function(res) {
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
page.value = 1;
|
||||||
|
noMore.value = false;
|
||||||
|
loadOrderList();
|
||||||
|
}, 1500);
|
||||||
|
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.requestPayment({
|
||||||
|
timeStamp: payData.pay_config.timestamp,
|
||||||
|
nonceStr: payData.pay_config.nonce_str,
|
||||||
|
package: payData.pay_config.package,
|
||||||
|
signType: payData.pay_config.sign_type,
|
||||||
|
paySign: payData.pay_config.pay_sign,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
page.value = 1;
|
||||||
|
noMore.value = false;
|
||||||
|
loadOrderList();
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消支付',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error('支付失败', e);
|
||||||
|
const errorMsg = e.msg || e.message || '支付失败,请稍后重试';
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadOrderList();
|
loadOrderList();
|
||||||
});
|
});
|
||||||
@@ -115,62 +288,239 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.container {
|
||||||
.empty-state {
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
background: #f5f6f7;
|
||||||
justify-content: center;
|
}
|
||||||
padding: 120rpx 40rpx;
|
|
||||||
min-height: 400rpx;
|
|
||||||
.empty-icon { font-size: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
|
|
||||||
.empty-title { font-size: 32rpx; font-weight: 600; color: var(--text-primary); margin-bottom: 16rpx; }
|
|
||||||
.empty-desc { font-size: 26rpx; color: var(--text-tertiary); text-align: center; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card {
|
.filter-tabs {
|
||||||
margin-bottom: var(--space-md);
|
display: flex;
|
||||||
.order-header {
|
background: #fff;
|
||||||
margin-bottom: var(--space-md);
|
position: sticky;
|
||||||
.order-no { font-size: 28rpx; font-weight: 600; color: var(--text-primary); }
|
top: 0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
padding: 28rpx 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-info {
|
.tab-line {
|
||||||
.info-row {
|
position: absolute;
|
||||||
padding: var(--space-xs) 0;
|
bottom: 0;
|
||||||
.info-label { font-size: 24rpx; color: var(--text-tertiary); }
|
left: 50%;
|
||||||
.info-value { font-size: 24rpx; color: var(--text-primary); }
|
transform: translateX(-50%);
|
||||||
|
width: 48rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 200rpx 40rpx;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 28rpx 28rpx 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #999;
|
||||||
|
background: #f5f6f7;
|
||||||
|
padding: 2rpx 10rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
.info-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.amount {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
height: 1rpx;
|
height: 1rpx;
|
||||||
background: var(--gray-200);
|
background: #f5f5f5;
|
||||||
margin: var(--space-md) 0;
|
margin: 24rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-list {
|
.package-section {
|
||||||
.package-item {
|
.section-label {
|
||||||
padding: var(--space-sm) 0;
|
font-size: 22rpx;
|
||||||
border-bottom: 1rpx solid var(--gray-100);
|
color: #999;
|
||||||
&:last-child { border-bottom: none; }
|
margin-bottom: 16rpx;
|
||||||
.package-name { font-size: 26rpx; color: var(--text-primary); }
|
}
|
||||||
|
|
||||||
|
.package-list {
|
||||||
|
.package-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-footer {
|
.card-footer {
|
||||||
margin-top: var(--space-md);
|
margin-top: 24rpx;
|
||||||
padding-top: var(--space-md);
|
padding-top: 24rpx;
|
||||||
border-top: 1rpx solid var(--gray-200);
|
border-top: 1rpx solid #f0f0f0;
|
||||||
.total-label { font-size: 26rpx; color: var(--text-secondary); }
|
display: flex;
|
||||||
.total-amount { font-size: 32rpx; font-weight: 700; color: var(--danger); }
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.btn-pay {
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 16rpx 40rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
width:100%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.load-more {
|
.status-badge {
|
||||||
text-align: center;
|
padding: 8rpx 20rpx;
|
||||||
padding: var(--space-lg);
|
border-radius: 30rpx;
|
||||||
color: var(--text-tertiary);
|
font-size: 22rpx;
|
||||||
font-size: 24rpx;
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.tag-warning {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-success {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-info {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -9,33 +9,77 @@
|
|||||||
<view class="card package-card" v-for="item in packageList" :key="item.package_id">
|
<view class="card package-card" v-for="item in packageList" :key="item.package_id">
|
||||||
<view class="package-header flex-row-sb">
|
<view class="package-header flex-row-sb">
|
||||||
<view class="package-name">{{ item.package_name }}</view>
|
<view class="package-name">{{ item.package_name }}</view>
|
||||||
<view class="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
|
<view class="header-right">
|
||||||
{{ item.is_addon ? '加油包' : '正式套餐' }}
|
<view class="validity-tag">有效期{{ item.validity_days }}天</view>
|
||||||
|
<view class="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
|
||||||
|
{{ item.is_addon ? '加油包' : '正式套餐' }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="package-price">¥{{ formatMoney(item.retail_price) }}</view>
|
<view class="package-data">{{ formatData(item.data_allowance, item.data_unit) }}</view>
|
||||||
<view class="package-desc">{{ item.description }}</view>
|
<view class="package-desc">{{ item.description }}</view>
|
||||||
<view class="package-info">
|
<view class="package-footer flex-row-sb">
|
||||||
<view class="info-item">流量: {{ formatData(item.data_allowance, item.data_unit) }}</view>
|
<view class="package-price">¥{{ formatMoney(item.retail_price) }}</view>
|
||||||
<view class="info-item">有效: {{ item.validity_days }}天</view>
|
<view class="btn">
|
||||||
</view>
|
<up-button type="primary" @click="buyPackage(item)">立即订购</up-button>
|
||||||
<view class="btn">
|
</view>
|
||||||
<up-button type="primary" @click="buyPackage(item)">立即订购</up-button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<up-modal title="确认购买" :show="showModal" showCancelButton @confirm="confirmBuy" @cancel="showModal=false">
|
<!-- 支付方式选择弹窗 -->
|
||||||
<view style="padding: 40rpx; text-align: center;">
|
<up-popup :show="showModal" mode="center" @close="showModal=false">
|
||||||
<view>套餐名称:{{ currentPackage?.package_name }}</view>
|
<view class="payment-popup">
|
||||||
<view class="mt-20">价格:¥{{ formatMoney(currentPackage?.retail_price) }}</view>
|
<view class="popup-header">
|
||||||
|
<view class="popup-title">选择支付方式</view>
|
||||||
|
<view class="popup-close" @tap="showModal=false">×</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="package-summary">
|
||||||
|
<view class="summary-name">{{ currentPackage?.package_name }}</view>
|
||||||
|
<view class="summary-price">¥{{ formatMoney(currentPackage?.retail_price) }}</view>
|
||||||
|
<view class="summary-details">
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">套餐流量</text>
|
||||||
|
<text class="detail-value">{{ formatData(currentPackage?.data_allowance, currentPackage?.data_unit) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-divider"></view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">有效期</text>
|
||||||
|
<text class="detail-value">{{ currentPackage?.validity_days }}天</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="payment-methods">
|
||||||
|
<view class="method-item" :class="{ active: paymentMethod === 'wechat' }" @tap="selectPaymentMethod('wechat')">
|
||||||
|
<view class="method-left">
|
||||||
|
<image class="method-icon" src="/static/wechat.png" mode="aspectFit"></image>
|
||||||
|
<text class="method-name">微信支付</text>
|
||||||
|
</view>
|
||||||
|
<view class="method-radio" :class="{ checked: paymentMethod === 'wechat' }"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="method-item" :class="{ active: paymentMethod === 'wallet' }" @tap="selectPaymentMethod('wallet')">
|
||||||
|
<view class="method-left">
|
||||||
|
<image class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
|
||||||
|
<text class="method-name">账户余额(¥{{ formatMoney(walletBalance) }})</text>
|
||||||
|
</view>
|
||||||
|
<view class="method-radio" :class="{ checked: paymentMethod === 'wallet' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="popup-footer">
|
||||||
|
<button class="btn-apple btn-secondary" @tap="showModal=false">取消</button>
|
||||||
|
<button class="btn-apple btn-primary" @tap="confirmPay">确认支付</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</up-modal>
|
</up-popup>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { assetApi, orderApi } from '@/api/index.js';
|
import { assetApi, orderApi, walletApi } from '@/api/index.js';
|
||||||
import { useUserStore } from '@/store/index.js';
|
import { useUserStore } from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -44,6 +88,8 @@
|
|||||||
let currentPackage = ref(null);
|
let currentPackage = ref(null);
|
||||||
let packageList = reactive([]);
|
let packageList = reactive([]);
|
||||||
let loading = ref(false);
|
let loading = ref(false);
|
||||||
|
let paymentMethod = ref('wechat');
|
||||||
|
let walletBalance = ref(0);
|
||||||
|
|
||||||
const formatMoney = (amount) => {
|
const formatMoney = (amount) => {
|
||||||
if (!amount && amount !== 0) return '0.00';
|
if (!amount && amount !== 0) return '0.00';
|
||||||
@@ -68,23 +114,230 @@
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadWalletBalance = async () => {
|
||||||
|
try {
|
||||||
|
const data = await walletApi.getDetail(userStore.state.identifier);
|
||||||
|
walletBalance.value = data.balance || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载钱包余额失败', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const buyPackage = (item) => {
|
const buyPackage = (item) => {
|
||||||
currentPackage.value = item;
|
currentPackage.value = item;
|
||||||
|
paymentMethod.value = 'wechat';
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmBuy = async () => {
|
const selectPaymentMethod = (method) => {
|
||||||
|
paymentMethod.value = method;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmPay = async () => {
|
||||||
|
if (!currentPackage.value) return;
|
||||||
|
|
||||||
|
uni.showLoading({ title: '创建订单...', mask: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await orderApi.create(userStore.state.identifier, [currentPackage.value.package_id]);
|
// 第一步:创建订单
|
||||||
uni.showToast({ title: '购买成功', icon: 'success' });
|
const orderResult = await orderApi.create(
|
||||||
|
userStore.state.identifier,
|
||||||
|
[currentPackage.value.package_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 判断是否为强充订单
|
||||||
|
if (orderResult.order_type === 'recharge' && orderResult.recharge) {
|
||||||
|
const recharge = orderResult.recharge;
|
||||||
|
|
||||||
|
// 检查充值订单状态
|
||||||
|
if (recharge.status === 2 || recharge.status === 3) {
|
||||||
|
// 已关闭或已退款的订单
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: `充值订单${recharge.status_name},请重新下单`,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查充值订单是否已存在(待支付状态但无支付配置)
|
||||||
|
if (recharge.status === 1 && (!orderResult.pay_config || !orderResult.pay_config.package)) {
|
||||||
|
// 待支付订单已存在,引导用户跳转到订单页面
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '检测到您有待支付的充值订单,是否前往我的钱包查看?',
|
||||||
|
confirmText: '去查看',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/my-wallet/my-wallet' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有支付配置(新订单应该有)
|
||||||
|
if (!orderResult.pay_config || !orderResult.pay_config.package) {
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取强充信息
|
||||||
|
const forceRechargeAmount = orderResult.linked_package_info?.force_recharge_amount || recharge.amount;
|
||||||
|
const packageNames = orderResult.linked_package_info?.package_names || [];
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
|
||||||
|
// 提示用户需要强充
|
||||||
|
const packageNameStr = packageNames.length > 0 ? packageNames.join('、') : '套餐';
|
||||||
|
const message = `购买${packageNameStr}需要先充值 ¥${formatMoney(forceRechargeAmount)},充值成功后将自动购买套餐`;
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '需要充值',
|
||||||
|
content: message,
|
||||||
|
confirmText: '去充值',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 调起微信支付
|
||||||
|
await handleWechatPay(orderResult.pay_config, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有订单信息(非强充订单)
|
||||||
|
if (!orderResult || !orderResult.order) {
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
uni.showToast({ title: '订单创建失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderId = orderResult.order.order_id;
|
||||||
|
|
||||||
|
// 第二步:获取支付参数
|
||||||
|
uni.showLoading({ title: '准备支付...', mask: true });
|
||||||
|
|
||||||
|
const payResult = await orderApi.pay(orderId, paymentMethod.value);
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
|
||||||
|
// 处理不同支付方式
|
||||||
|
if (paymentMethod.value === 'wechat') {
|
||||||
|
// 微信支付
|
||||||
|
if (!payResult.pay_config || !payResult.pay_config.package) {
|
||||||
|
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleWechatPay(payResult.pay_config);
|
||||||
|
} else if (paymentMethod.value === 'wallet') {
|
||||||
|
// 钱包支付成功
|
||||||
|
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
loadWalletBalance();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('购买套餐失败', e);
|
uni.hideLoading();
|
||||||
|
showModal.value = false;
|
||||||
|
console.error('创建订单或支付失败', e);
|
||||||
|
|
||||||
|
// 特殊错误处理:订单正在创建中
|
||||||
|
if (e.code === 1008) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
|
||||||
|
confirmText: '去支付',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/order-list/order-list' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他错误提示
|
||||||
|
const errorMsg = e.msg || e.message || '操作失败,请稍后重试';
|
||||||
|
uni.showToast({ title: errorMsg, icon: 'none' });
|
||||||
}
|
}
|
||||||
showModal.value = false;
|
};
|
||||||
|
|
||||||
|
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
|
||||||
|
// 验证参数
|
||||||
|
if (!payConfig.package || !payConfig.package.startsWith('prepay_id=')) {
|
||||||
|
uni.showToast({ title: '支付参数无效', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #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(() => {
|
||||||
loadPackages();
|
loadPackages();
|
||||||
|
loadWalletBalance();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -107,25 +360,226 @@
|
|||||||
.package-header {
|
.package-header {
|
||||||
margin-bottom: var(--space-sm);
|
margin-bottom: var(--space-sm);
|
||||||
.package-name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); }
|
.package-name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); }
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
.validity-tag {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.package-price {
|
.package-data {
|
||||||
font-size: 40rpx;
|
font-size: 48rpx;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
color: var(--warning);
|
color: var(--primary);
|
||||||
margin-bottom: var(--space-sm);
|
margin-bottom: var(--space-sm);
|
||||||
|
letter-spacing: -1rpx;
|
||||||
}
|
}
|
||||||
.package-desc {
|
.package-desc {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
.package-info {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-lg);
|
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
.info-item { font-size: 24rpx; color: var(--text-tertiary); }
|
|
||||||
}
|
}
|
||||||
.btn { margin-top: var(--space-sm); }
|
.package-footer {
|
||||||
|
align-items: center;
|
||||||
|
.package-price {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-popup {
|
||||||
|
width: 600rpx;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-large);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid var(--border-light);
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-summary {
|
||||||
|
padding: 40rpx 30rpx 30rpx;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 122, 255, 0.05) 0%, rgba(0, 122, 255, 0.02) 100%);
|
||||||
|
border-bottom: 1rpx solid var(--border-light);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.summary-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-price {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
letter-spacing: -1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid rgba(0, 122, 255, 0.1);
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: var(--border-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods {
|
||||||
|
padding: 30rpx;
|
||||||
|
|
||||||
|
.method-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
border: 2rpx solid var(--border-light);
|
||||||
|
border-radius: var(--radius-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(0, 122, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.method-icon {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-radio {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border: 2rpx solid var(--gray-400);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--primary);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-top: 1rpx solid var(--border-light);
|
||||||
|
|
||||||
|
.btn-apple {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-medium);
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.btn-secondary {
|
||||||
|
background: var(--gray-200);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<view v-if="mchList.length === 0 && !loading" class="empty-state">
|
||||||
|
<view class="empty-icon">📡</view>
|
||||||
|
<view class="empty-title">运营商列表为空</view>
|
||||||
|
<view class="empty-desc">当前设备暂无可切换的运营商</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="card" v-for="item in mchList" :key="item.iccid">
|
<view class="card" v-for="item in mchList" :key="item.iccid">
|
||||||
<view class="flex-row-sb mt-30">
|
<view class="flex-row-sb mt-30">
|
||||||
<view class="flex-row-g20">
|
<view class="flex-row-g20">
|
||||||
@@ -14,9 +20,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex-row-g20">
|
<view class="flex-row-g20">
|
||||||
<view class="operator">
|
|
||||||
<up-tag type="primary" size="mini" v-if="item.is_current">当前使用</up-tag>
|
|
||||||
</view>
|
|
||||||
<view class="operator">
|
<view class="operator">
|
||||||
<up-tag :type="item.real_name_status === 1 ? 'primary' : 'success'" size="mini">{{ item.real_name_status === 1 ? '已实名' : '未实名' }}</up-tag>
|
<up-tag :type="item.real_name_status === 1 ? 'primary' : 'success'" size="mini">{{ item.real_name_status === 1 ? '已实名' : '未实名' }}</up-tag>
|
||||||
</view>
|
</view>
|
||||||
@@ -25,7 +28,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="btn flex-row-g20 mt-30">
|
<view class="btn flex-row-g20 mt-30">
|
||||||
<up-button class="btn-apple btn-success" v-if="!item.is_current" type="success" @tap="switchOperator(item)" :loading="switching">
|
<up-button class="btn-apple btn-primary" v-if="item.is_current" type="primary" disabled>
|
||||||
|
当前使用
|
||||||
|
</up-button>
|
||||||
|
<up-button class="btn-apple btn-success" v-else type="success" @tap="switchOperator(item)" :loading="switching">
|
||||||
切换此运营商
|
切换此运营商
|
||||||
</up-button>
|
</up-button>
|
||||||
</view>
|
</view>
|
||||||
@@ -35,12 +41,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
import { deviceApi, assetApi } from '@/api/index.js';
|
import { assetApi, deviceApi } from '@/api/index.js';
|
||||||
import { useUserStore } from '@/store/index.js';
|
import { useUserStore } from '@/store/index.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
let mchList = reactive([]);
|
let mchList = reactive([]);
|
||||||
|
let loading = ref(false);
|
||||||
let switching = ref(false);
|
let switching = ref(false);
|
||||||
|
|
||||||
let opratorList = reactive([
|
let opratorList = reactive([
|
||||||
@@ -55,17 +62,49 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadCards = async () => {
|
const loadCards = async () => {
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await assetApi.getInfo(userStore.state.identifier);
|
const assetData = await assetApi.getInfo(userStore.state.identifier);
|
||||||
if (data.cards && data.cards.length > 0) {
|
|
||||||
mchList.splice(0, mchList.length, ...data.cards.map(card => ({
|
// 判断是否为设备
|
||||||
...card,
|
if (assetData.asset_type === 'device') {
|
||||||
category: card.carrier_name.includes('电信') ? '124' : card.carrier_name.includes('联通') ? '125' : '126'
|
// 是设备,调用设备卡列表接口
|
||||||
})));
|
const data = await deviceApi.getCards(userStore.state.identifier);
|
||||||
|
if (data.cards && data.cards.length > 0) {
|
||||||
|
mchList.splice(0, mchList.length, ...data.cards.map(card => ({
|
||||||
|
iccid: card.iccid,
|
||||||
|
carrier_name: card.carrier_name,
|
||||||
|
is_current: card.is_active,
|
||||||
|
real_name_status: card.real_name_status,
|
||||||
|
slot_position: card.slot_position,
|
||||||
|
category: getCarrierCategory(card.carrier_name)
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不是设备(单卡),直接使用 asset info 数据,只有一个卡
|
||||||
|
if (assetData.identifier) {
|
||||||
|
mchList.splice(0, mchList.length, {
|
||||||
|
iccid: assetData.identifier,
|
||||||
|
carrier_name: assetData.carrier_name,
|
||||||
|
is_current: true,
|
||||||
|
real_name_status: assetData.real_name_status,
|
||||||
|
slot_position: 1,
|
||||||
|
category: getCarrierCategory(assetData.carrier_name)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载卡列表失败', e);
|
console.error('加载卡列表失败', e);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCarrierCategory = (carrierName) => {
|
||||||
|
if (!carrierName) return '126';
|
||||||
|
if (carrierName.includes('电信')) return '124';
|
||||||
|
if (carrierName.includes('联通')) return '125';
|
||||||
|
if (carrierName.includes('移动')) return '126';
|
||||||
|
return '126';
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchOperator = async (item) => {
|
const switchOperator = async (item) => {
|
||||||
@@ -118,4 +157,16 @@
|
|||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 40rpx;
|
||||||
|
min-height: 400rpx;
|
||||||
|
.empty-icon { font-size: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
|
||||||
|
.empty-title { font-size: 32rpx; font-weight: 600; color: var(--text-primary); margin-bottom: 16rpx; }
|
||||||
|
.empty-desc { font-size: 26rpx; color: var(--text-tertiary); text-align: center; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
4757
pnpm-lock.yaml
generated
Normal file
4757
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/data.png
Normal file
BIN
static/data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
static/wechat.png
Normal file
BIN
static/wechat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -5,7 +5,11 @@ const state = reactive({
|
|||||||
assetToken: '',
|
assetToken: '',
|
||||||
identifier: uni.getStorageSync('identifier') || '',
|
identifier: uni.getStorageSync('identifier') || '',
|
||||||
isDevice: false,
|
isDevice: false,
|
||||||
realNameStatus: 0
|
realNameStatus: 0,
|
||||||
|
userInfo: uni.getStorageSync('userInfo') || {
|
||||||
|
avatar: '',
|
||||||
|
nickname: ''
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useUserStore = () => {
|
export const useUserStore = () => {
|
||||||
@@ -31,11 +35,18 @@ export const useUserStore = () => {
|
|||||||
state.realNameStatus = status;
|
state.realNameStatus = status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setUserInfo = (userInfo) => {
|
||||||
|
state.userInfo = userInfo;
|
||||||
|
uni.setStorageSync('userInfo', userInfo);
|
||||||
|
};
|
||||||
|
|
||||||
const clearUser = () => {
|
const clearUser = () => {
|
||||||
state.token = '';
|
state.token = '';
|
||||||
state.assetToken = '';
|
state.assetToken = '';
|
||||||
|
state.userInfo = { avatar: '', nickname: '' };
|
||||||
uni.removeStorageSync('token');
|
uni.removeStorageSync('token');
|
||||||
uni.removeStorageSync('identifier');
|
uni.removeStorageSync('identifier');
|
||||||
|
uni.removeStorageSync('userInfo');
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -45,6 +56,7 @@ export const useUserStore = () => {
|
|||||||
setIdentifier,
|
setIdentifier,
|
||||||
setIsDevice,
|
setIsDevice,
|
||||||
setRealNameStatus,
|
setRealNameStatus,
|
||||||
|
setUserInfo,
|
||||||
clearUser
|
clearUser
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
2
uni.scss
2
uni.scss
@@ -74,3 +74,5 @@ $uni-color-subtitle: #555555; // 二级标题颜色
|
|||||||
$uni-font-size-subtitle:26px;
|
$uni-font-size-subtitle:26px;
|
||||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||||
$uni-font-size-paragraph:15px;
|
$uni-font-size-paragraph:15px;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const BASE_URL = 'https://cmp-api.boss160.cn';
|
export const BASE_URL = 'https://cmp-api.boss160.cn';
|
||||||
|
|
||||||
export const APP_TYPE = 'miniapp';
|
export const APP_TYPE = 'official_account';
|
||||||
|
|
||||||
export const APP_ID = 'wxea8c599fe100ce8a';
|
export const APP_ID = 'wx404dc822ce15c5f0';
|
||||||
|
|
||||||
|
export const USE_WECHAT_AUTH = true;
|
||||||
Reference in New Issue
Block a user