Compare commits

..

1 Commits

Author SHA1 Message Date
c341ef1aac feat: 重构移动端 H5 页面视觉设计
基于移动优先原则优化布局、排版、颜色和间距,提升设计感和专业度。

- 重构全局设计系统:颜色、阴影、间距、圆角、排版
- 优化 UserInfoCard:更大的头像和状态徽章设计
- 优化 DeviceStatusCard:信息层级和指标网格
- 优化 TrafficCard:进度条和数字排版
- 优化 FunctionCard:4列布局和触摸反馈
- 优化 WifiCard:列表设计和信息层次
- 优化 FloatingButton:去除渐变,纯色设计

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 18:18:25 +08:00
37 changed files with 6745 additions and 1632 deletions

View File

@@ -2,7 +2,13 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git add:*)", "Bash(git add:*)",
"Bash(npm list:*)" "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": []

View File

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

30
.impeccable.md Normal file
View 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.

578
App.vue
View File

@@ -5,86 +5,89 @@
</template> </template>
<style lang="scss"> <style lang="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: #F8F8F8;
--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 {
@@ -93,36 +96,31 @@
page { page {
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-secondary); background-color: var(--bg-base);
// min-height: 100vh; 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;
width: 100%;
box-sizing: border-box;
}
.pd-20{
padding: 20rpx;
} }
.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 {
@@ -131,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);
@@ -235,7 +250,7 @@
} }
&.tag-primary { &.tag-primary {
background: rgba(10, 132, 255, 0.12); background: var(--primary-tint);
color: var(--primary); color: var(--primary);
} }
@@ -250,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);
@@ -304,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>

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,55 @@
<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">{{ deviceInfo.carrier_name || '-' }}</view> <view class="operator-badge">
<text class="operator-name">中国电信</text>
</view> </view>
<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>
<view class="info-values flex-row-g20" @tap="$emit('authentication')">
<view class="tag-apple" :class="isRealName ? 'tag-success' : 'tag-warning'"> <view class="status-row">
{{ isRealName ? "已实名" : "未实名" }} <view class="status-item">
<text class="status-label">当前卡</text>
<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="info-row flex-row-sb">
<view class="info-label"> <view class="status-row">
<view class="subtitle" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</view> <view class="status-item full-width">
<view class="subtitle" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</view> <text class="status-label">到期时间</text>
</view> <text class="status-value expire">{{ deviceInfo.expireDate }}</text>
<view class="info-values flex-row-g20">
<view class="tag-apple tag-primary">{{ statusText }}</view>
</view> </view>
</view> </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> </view>
<view class="device-metrics flex-row-sb mt-md"> <text class="metric-label">信号强度</text>
<view class="metric-item flex-col-center"> <text class="metric-value">{{ getSignalText(deviceInfo.rssi) }}</text>
<view class="caption mb-xs">信号强度</view>
<view class="metric-value">{{ getSignalText(deviceInfo.rssi) }}</view>
</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.battery }} %</view> <text class="metric-icon-text">🔋</text>
</view> </view>
<view class="metric-item flex-col-center"> <text class="metric-label">设备电量</text>
<view class="caption mb-xs">连接数量</view> <text class="metric-value">{{ deviceInfo.battery }}%</text>
<view class="metric-value">{{ deviceInfo.connCnt }}/{{ deviceInfo.max_clients }}</view> </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>
@@ -48,39 +61,157 @@
const props = defineProps({ const props = defineProps({
deviceInfo: { type: Object, default: () => ({}) }, deviceInfo: { type: Object, default: () => ({}) },
isRealName: { type: Boolean, default: false }, isRealName: { type: Boolean, default: false },
isDevice: { type: Boolean, default: true },
opratorList: { type: Array, default: () => [] } opratorList: { type: Array, default: () => [] }
}); });
defineEmits(['authentication']); defineEmits(['authentication']);
const getSignalText = (rssi) => rssi || '强'; const getSignalText = (rssi) => rssi || '强';
const statusText = computed(() => {
const statusMap = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
};
return statusMap[props.deviceInfo.status] || '-';
});
</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; }
} }
.device-metrics {
background: var(--gray-100); .card-header {
border-radius: var(--radius-small); display: flex;
padding: 20rpx; align-items: center;
.metric-item { flex: 1; } justify-content: space-between;
padding-bottom: var(--space-4);
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>

View File

@@ -1,7 +1,10 @@
<template> <template>
<view class="floating-service-card" @click="handleClick"> <view class="service-card" @click="handleClick">
<view class="service-inner">
<view class="service-left"> <view class="service-left">
<view class="service-icon-wrap">
<image class="service-icon" src="/static/link.png" mode="aspectFit"></image> <image class="service-icon" src="/static/link.png" mode="aspectFit"></image>
</view>
<view class="service-content"> <view class="service-content">
<text class="service-title">需要帮助</text> <text class="service-title">需要帮助</text>
<text class="service-desc">专属客服在线解答</text> <text class="service-desc">专属客服在线解答</text>
@@ -9,6 +12,7 @@
</view> </view>
<view class="service-btn">立即联系</view> <view class="service-btn">立即联系</view>
</view> </view>
</view>
</template> </template>
<script setup> <script setup>
@@ -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-wrap {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-tint);
border-radius: var(--radius-md);
}
.service-icon { .service-icon {
width: 60rpx; width: 44rpx;
height: 60rpx; height: 44rpx;
} }
.service-content { .service-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4rpx; gap: 2rpx;
}
.service-title { .service-title {
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
line-height: 1.3;
} }
.service-desc { .service-desc {
font-size: 22rpx; font-size: 24rpx;
color: var(--text-tertiary); 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>

View File

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

View File

@@ -1,36 +1,40 @@
<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-track">
<view class="progress-fill" :style="{ width: usedFlowPercent + '%' }"></view> <view class="progress-fill" :style="{ width: usedFlowPercent + '%' }"></view>
</view> </view>
</view> </view>
<view class="traffic-stats flex-row-sb">
<view class="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>
<view class="stat-value-wrap">
<text class="stat-value">{{ usedValue }}</text> <text class="stat-value">{{ usedValue }}</text>
<text class="stat-unit">{{ usedUnit }}</text> <text class="stat-unit">{{ usedUnit }}</text>
</view> </view>
</view> </view>
<view class="stat-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>
<view class="stat-value-wrap">
<text class="stat-value">{{ totalValue }}</text> <text class="stat-value">{{ totalValue }}</text>
<text class="stat-unit">{{ totalUnit }}</text> <text class="stat-unit">{{ totalUnit }}</text>
</view> </view>
</view> </view>
<view class="stat-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">{{ remainValue }}</text> <view class="stat-value-wrap">
<text class="stat-value remain">{{ remainValue }}</text>
<text class="stat-unit">{{ remainUnit }}</text> <text class="stat-unit">{{ remainUnit }}</text>
</view> </view>
</view> </view>
@@ -91,9 +95,9 @@
const usedFlowPercent = computed(() => { const usedFlowPercent = computed(() => {
if (totalMB.value && usedMB.value) { if (totalMB.value && usedMB.value) {
const percent = (usedMB.value / totalMB.value * 100); const percent = (usedMB.value / totalMB.value * 100);
return Math.min(percent, 100).toFixed(2); return Math.min(percent, 100).toFixed(1);
} }
return '0.00'; return '0.0';
}); });
// 加载套餐流量数据 // 加载套餐流量数据
@@ -109,12 +113,16 @@
const activePackage = data.items[0]; const activePackage = data.items[0];
if (activePackage.enable_virtual_data) { if (activePackage.enable_virtual_data) {
// 启用虚流量
usedMB.value = activePackage.virtual_used_mb || 0; usedMB.value = activePackage.virtual_used_mb || 0;
totalMB.value = activePackage.virtual_limit_mb || 0;
remainMB.value = activePackage.virtual_remain_mb || 0;
} else { } else {
// 未启用虚流量,使用真流量
usedMB.value = activePackage.data_usage_mb || 0; usedMB.value = activePackage.data_usage_mb || 0;
}
totalMB.value = activePackage.data_limit_mb || 0; totalMB.value = activePackage.data_limit_mb || 0;
remainMB.value = totalMB.value - usedMB.value; remainMB.value = totalMB.value - usedMB.value;
}
// 将套餐信息(包括到期时间)传递给父组件 // 将套餐信息(包括到期时间)传递给父组件
emit('packageLoaded', activePackage); emit('packageLoaded', activePackage);
@@ -140,18 +148,105 @@ if (activePackage.enable_virtual_data) {
<style scoped lang="scss"> <style scoped lang="scss">
.traffic-card { .traffic-card {
.usage-percent { background: var(--bg-surface);
border-radius: var(--radius-xl);
padding: var(--space-5);
box-shadow: var(--shadow-sm);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-5);
}
.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; display: flex;
align-items: baseline; align-items: baseline;
gap: 4rpx; gap: 4rpx;
.title { font-size: 35rpx; font-weight: 700; margin-right: 5rpx; }
} }
.traffic-stats {
background: var(--gray-100); .stat-value {
border-radius: var(--radius-small); font-size: 32rpx;
padding: 20rpx; font-weight: 700;
margin-top: 16rpx; color: var(--text-primary);
.stat-item { flex: 1; } line-height: 1.1;
&.remain {
color: var(--success);
} }
} }
.stat-unit {
font-size: 22rpx;
font-weight: 500;
color: var(--text-tertiary);
}
</style> </style>

View File

@@ -1,19 +1,25 @@
<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="avatarUrl" 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="caption">套餐到期时间{{ deviceInfo.expireDate || '-' }}</view>
</view> </view>
<view v-if="isDevice" class="tag-apple" :class="onlineStatus === '在线' ? 'tag-success' : 'tag-warning'"> <view class="user-meta">
{{ onlineStatus }} <text class="meta-text" v-if="isDevice">IMEI{{ deviceInfo.imei || '-' }}</text>
<text class="meta-text" v-else>手机号{{ deviceInfo.bound_phone || '-' }}</text>
</view> </view>
<view v-else class="tag-apple" :class="networkStatus == 1 ? 'tag-success' : 'tag-warning'"> </view>
</view>
<view class="card-footer" v-if="!isDevice">
<view class="network-badge" :class="networkStatus == 1 ? 'network-ok' : 'network-stop'">
{{ networkStatus == 1 ? '正常' : '停机' }} {{ networkStatus == 1 ? '正常' : '停机' }}
</view> </view>
</view> </view>
@@ -24,36 +30,137 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useUserStore } from '@/store/index.js'; import { useUserStore } from '@/store/index.js';
const userStore = useUserStore(); const props = defineProps({
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, default: '离线' }, networkStatus: { type: [String, Number], default: '离线' },
isDevice: { type: Boolean, default: true } isDevice: { type: Boolean, default: true }
}); });
// 默认头像 const userStore = useUserStore();
const defaultAvatar = 'https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500'; const defaultAvatar = 'https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500';
// 用户头像,优先使用 store 中的头像,否则使用默认头像
const avatarUrl = computed(() => { const avatarUrl = computed(() => {
return userStore.state.userInfo.avatar || defaultAvatar; return userStore.state.userInfo.avatar || defaultAvatar;
}); });
const isOnline = computed(() => {
return props.onlineStatus === '在线';
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.user-info-card { .user-info-card {
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);
image { width: 100%; height: 100%; object-fit: cover; }
} }
.user-details { flex: 1; }
.card-main {
display: flex;
align-items: center;
gap: var(--space-4);
}
.user-avatar {
width: 96rpx;
height: 96rpx;
border-radius: var(--radius-lg);
overflow: hidden;
border: 4rpx solid var(--gray-100);
flex-shrink: 0;
image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user-details {
flex: 1;
min-width: 0;
}
.user-id {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.device-id {
font-size: 32rpx;
font-weight: 700;
color: var(--text-primary);
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>

View File

@@ -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 class="wifi-list">
<view class="wifi-item">
<view class="wifi-icon-wrap">
<text class="wifi-icon-text">📶</text>
</view> </view>
<view class="wifi-info flex-col-g16 mt-30"> <view class="wifi-content">
<view class="wifi-item flex-row-sb"> <text class="wifi-label">网络名称</text>
<view class="wifi-details flex-col-g8"> <text class="wifi-value">{{ deviceInfo.ssidName }}</text>
<view class="caption">网络名称</view>
<view class="subtitle">{{ deviceInfo.ssidName }}</view>
</view> </view>
<button class="btn-apple btn-primary btn-mini" @tap="$emit('copy', deviceInfo.ssidName)">复制</button> <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,17 +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; }
}
} }
.btn-group{ .card-header {
padding-right: var(--space-md); 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>

View File

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

View File

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

15
main.js
View File

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

View File

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

110
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4757
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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