fix: ui
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 1m33s

This commit is contained in:
sexygoat
2026-04-25 14:28:40 +08:00
parent 91d7d4c21f
commit 809a3cf0f8
6 changed files with 128 additions and 1210 deletions

View File

@@ -1,7 +1,7 @@
/**
* 认证模块 API
*/
import { get, post } from '@/utils/request'
import { get, post, put } from '@/utils/request'
/**
* 登录请求参数
@@ -104,8 +104,5 @@ export function refreshToken(refresh_token: string) {
* PUT /api/auth/password
*/
export function changePassword(data: ChangePasswordParams) {
return post<void>('/api/auth/password', {
data,
method: 'PUT',
})
return put<void>('/api/auth/password', { data })
}

View File

@@ -11,7 +11,7 @@
{
"path": "pages/agent-system/home/index",
"style": {
"navigationBarTitleText": "卡管H5",
"navigationBarTitleText": "代理企业端H5",
"navigationStyle": "custom"
}
},

View File

@@ -112,12 +112,13 @@ const cardHasMore = ref(true)
const deviceHasMore = ref(true)
const pageSize = 20
// 搜索和筛选
const iccidKeyword = ref('')
const virtualNoKeyword = ref('')
const searchType = ref<'iccid' | 'virtual'>('iccid')
const searchKeyword = ref('')
const carrierFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
const showCarrierPicker = ref(false)
const showStatusPicker = ref(false)
const showSearchTypePicker = ref(false)
// 运营商列表
const carrierList = ref<CarrierInfo[]>([])
// 运营商选择器选项
@@ -133,6 +134,11 @@ const statusOptions = [
{ label: '已激活', value: 3 },
{ label: '已停用', value: 4 }
]
// 搜索类型选项
const searchTypeOptions = [
{ label: 'ICCID', value: 'iccid' },
{ label: '虚拟号', value: 'virtual' }
]
// 合并后的资产列表
const assetList = computed<AssetItem[]>(() => {
@@ -209,11 +215,12 @@ async function loadCards(isRefresh = false) {
if (statusFilter.value !== undefined) {
params.status = statusFilter.value
}
if (iccidKeyword.value) {
params.iccid = iccidKeyword.value
}
if (virtualNoKeyword.value) {
params.virtual_no = virtualNoKeyword.value
if (searchKeyword.value) {
if (searchType.value === 'iccid') {
params.iccid = searchKeyword.value
} else {
params.virtual_no = searchKeyword.value
}
}
let cardsData: any
@@ -269,8 +276,8 @@ async function loadDevices(isRefresh = false) {
page_size: pageSize,
}
if (virtualNoKeyword.value) {
params.virtual_no = virtualNoKeyword.value
if (searchKeyword.value) {
params.virtual_no = searchKeyword.value
}
if (statusFilter.value !== undefined) {
params.status = statusFilter.value
@@ -351,6 +358,11 @@ function onStatusConfirm(option: { label: string, value: any }) {
refreshList()
}
// 选择搜索类型
function onSearchTypeConfirm(option: { label: string, value: any }) {
searchType.value = option.value
}
// 获取当前选中的运营商名称
const selectedCarrierName = computed(() => {
if (carrierFilter.value === undefined) return '运营商'
@@ -385,8 +397,7 @@ function changeTab(tab: 'card' | 'device') {
if (activeTab.value === tab) return
activeTab.value = tab
iccidKeyword.value = ''
virtualNoKeyword.value = ''
searchKeyword.value = ''
carrierFilter.value = undefined
statusFilter.value = undefined
@@ -565,104 +576,98 @@ onMounted(async () => {
</script>
<template>
<view class="bg-page safe-area-bottom min-h-screen">
<!-- 搜索和筛选 -->
<view class="safe-area-top px-4 pt-4 pb-3">
<!-- IoT卡搜索和筛选 -->
<view v-if="activeTab === 'card'">
<!-- ICCID搜索 -->
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
<i class="i-mdi-credit-card text-lg text-[#94a3b8] mr-2" />
<input
v-model="iccidKeyword"
class="flex-1 text-sm text-[#1e293b]"
placeholder="ICCID"
/>
<i
v-if="iccidKeyword"
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
@click="iccidKeyword = ''"
/>
</view>
<!-- 虚拟号搜索 -->
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
<i class="i-mdi-phone text-lg text-[#94a3b8] mr-2" />
<input
v-model="virtualNoKeyword"
class="flex-1 text-sm text-[#1e293b]"
placeholder="虚拟号"
/>
<i
v-if="virtualNoKeyword"
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
@click="virtualNoKeyword = ''"
/>
</view>
<!-- 筛选条件 -->
<view class="flex items-center gap-2 mb-2">
<!-- 运营商 -->
<view class="flex-1 bg-white rounded-full px-3 py-2 flex items-center" @click="showCarrierPicker = true">
<text class="flex-1 text-sm text-[#1e293b]">{{ selectedCarrierName }}</text>
<i class="i-mdi-chevron-down text-lg text-[#94a3b8]" />
<view class="bg-page safe-area-bottom min-h-screen flex flex-col">
<!-- 搜索区域 -->
<view class="bg-white px-4 pt-4 pb-3">
<!-- 搜索框 -->
<view class="relative flex items-center">
<input
v-model="searchKeyword"
class="flex-1 h-10 bg-[#f1f5f9] rounded-full pl-4 pr-12 text-sm text-[#1e293b] placeholder-[#94a3b8]"
:placeholder="searchType === 'iccid' ? '搜索ICCID' : '搜索虚拟号'"
@confirm="refreshList"
/>
<view
class="absolute right-1 flex items-center"
>
<view
v-if="searchKeyword"
class="w-8 h-8 flex items-center justify-center text-[#94a3b8]"
@click="searchKeyword = ''"
>
<i class="i-mdi-close-circle text-lg" />
</view>
<!-- 状态 -->
<view class="flex-1 bg-white rounded-full px-3 py-2 flex items-center" @click="showStatusPicker = true">
<text class="flex-1 text-sm text-[#1e293b]">{{ selectedStatusName }}</text>
<i class="i-mdi-chevron-down text-lg text-[#94a3b8]" />
<view
class="w-8 h-8 flex items-center justify-center text-brand"
@click="refreshList"
>
<i class="i-mdi-magnify text-xl" />
</view>
</view>
<!-- 搜索按钮 -->
<view class="bg-brand rounded-full py-2.5 text-center" @click="refreshList">
<text class="text-sm font-500 text-white">搜索</text>
</view>
</view>
<!-- 设备搜索 -->
<view v-else>
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
<i class="i-mdi-phone text-lg text-[#94a3b8] mr-2" />
<input
v-model="virtualNoKeyword"
class="flex-1 text-sm text-[#1e293b]"
placeholder="虚拟号"
/>
<i
v-if="virtualNoKeyword"
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
@click="virtualNoKeyword = ''"
/>
<!-- 筛选和类型切换 -->
<view class="flex items-center gap-2 mt-3 flex-wrap">
<view
class="flex items-center gap-1 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
@click="showSearchTypePicker = true"
>
<text class="text-12px" :class="searchType !== 'iccid' ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ searchType === 'iccid' ? 'ICCID' : '虚拟号' }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view>
<!-- 搜索按钮 -->
<view class="bg-brand rounded-full py-2.5 text-center" @click="refreshList">
<text class="text-sm font-500 text-white">搜索</text>
<view
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
@click="showCarrierPicker = true"
>
<text class="text-12px truncate" :class="carrierFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedCarrierName }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
</view>
<view
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0"
@click="showStatusPicker = true"
>
<text class="text-12px" :class="statusFilter !== undefined ? 'text-brand font-medium' : 'text-[#64748b]'">
{{ selectedStatusName }}
</text>
<i class="i-mdi-chevron-down text-12px text-[#94a3b8]" />
</view>
<view
v-if="carrierFilter !== undefined || statusFilter !== undefined"
class="flex items-center gap-1 px-2 py-1.5 rounded-full bg-[#ff4d4f]/10 text-[#ff4d4f] flex-shrink-0"
@click="carrierFilter = undefined; statusFilter = undefined; refreshList()"
>
<i class="i-mdi-close text-12px" />
<text class="text-12px font-medium">清除</text>
</view>
</view>
</view>
<!-- 筛选标签 -->
<view class="px-4 pb-3">
<view class="bg-white rounded-full p-1 flex items-center">
<!-- IoT卡 -->
<view
class="flex-1 py-2 rounded-full transition-fast text-center"
:class="activeTab === 'card' ? 'bg-brand' : ''"
@click="changeTab('card')"
>
<text class="text-sm font-500" :class="activeTab === 'card' ? 'text-white' : 'text-[#64748b]'">IoT卡</text>
</view>
<!-- 设备 -->
<view
class="flex-1 py-2 rounded-full transition-fast text-center"
:class="activeTab === 'device' ? 'bg-brand' : ''"
@click="changeTab('device')"
>
<text class="text-sm font-500" :class="activeTab === 'device' ? 'text-white' : 'text-[#64748b]'">设备</text>
<!-- 卡片/设备标签 -->
<view class="bg-white px-4 pb-3">
<view class="border-t border-[#f1f5f9] pt-3">
<view class="bg-[#f1f5f9] rounded-full p-1 flex">
<view
class="flex-1 py-2 rounded-full text-center transition-all"
:class="activeTab === 'card' ? 'bg-white shadow-sm text-[#1e293b] font-medium' : 'text-[#64748b]'"
@click="changeTab('card')"
>
<text class="text-sm">IoT卡</text>
</view>
<view
class="flex-1 py-2 rounded-full text-center transition-all"
:class="activeTab === 'device' ? 'bg-white shadow-sm text-[#1e293b] font-medium' : 'text-[#64748b]'"
@click="changeTab('device')"
>
<text class="text-sm">设备</text>
</view>
</view>
</view>
</view>
@@ -673,8 +678,7 @@ onMounted(async () => {
<!-- 资产列表 -->
<view
v-else
class="overflow-y-auto"
:style="{ height: activeTab === 'card' ? 'calc(100vh - 380px)' : 'calc(100vh - 280px)' }"
class="flex-1 overflow-y-auto min-h-0"
@scroll="onScroll"
>
<view v-if="assetList.length > 0" class="px-4 pb-4">
@@ -730,36 +734,32 @@ onMounted(async () => {
<template v-if="asset.type === 'card'">
<template v-if="(asset.raw as CardInfo).network_status === 0">
<view
class="px-3 py-1 rounded-lg"
style="background: #52c41a; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
@click="handleStartCard(asset, $event)"
>
<text class="text-12px font-500">复机</text>
复机
</view>
</template>
<template v-else-if="(asset.raw as CardInfo).network_status === 1">
<view
class="px-3 py-1 rounded-lg"
style="background: #ff4d4f; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
@click="handleStopCard(asset, $event)"
>
<text class="text-12px font-500">停机</text>
停机
</view>
</template>
<template v-else>
<view
class="px-3 py-1 rounded-lg"
style="background: #ff4d4f; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
@click="handleStopCard(asset, $event)"
>
<text class="text-12px font-500">停机</text>
停机
</view>
<view
class="px-3 py-1 rounded-lg"
style="background: #52c41a; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
@click="handleStartCard(asset, $event)"
>
<text class="text-12px font-500">复机</text>
复机
</view>
</template>
</template>
@@ -767,18 +767,16 @@ onMounted(async () => {
<!-- 设备操作按钮 -->
<template v-else>
<view
class="px-3 py-1 rounded-lg"
style="background: #ff4d4f; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
@click="handleStopDevice(asset, $event)"
>
<text class="text-12px font-500">停机</text>
停机
</view>
<view
class="px-3 py-1 rounded-lg"
style="background: #52c41a; color: white;"
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
@click="handleStartDevice(asset, $event)"
>
<text class="text-12px font-500">复机</text>
复机
</view>
</template>
@@ -802,7 +800,7 @@ onMounted(async () => {
<view class="bg-white rounded-2xl py-12 flex flex-col items-center">
<i class="i-mdi-package-variant-closed text-6xl text-[#cbd5e1] mb-3" />
<text class="text-base text-[#64748b] mb-1">暂无数据</text>
<text class="text-sm text-[#94a3b8]">{{ iccidKeyword || virtualNoKeyword ? '未找到相关结果' : '暂无资产数据' }}</text>
<text class="text-sm text-[#94a3b8]">{{ searchKeyword ? '未找到相关结果' : '暂无资产数据' }}</text>
</view>
</view>
</view>
@@ -822,5 +820,13 @@ onMounted(async () => {
title="选择状态"
@confirm="onStatusConfirm"
/>
<!-- 搜索类型选择器 -->
<SimplePicker
v-model:show="showSearchTypePicker"
:options="searchTypeOptions"
title="选择搜索类型"
@confirm="onSearchTypeConfirm"
/>
</view>
</template>

View File

@@ -1,300 +0,0 @@
# 代理/企业管理系统 - 移动端 H5
> **小米米家APP风格** - 亮色、简约、大气的移动端单页应用
---
## 🚀 快速启动
```bash
# 启动开发服务器
pnpm dev:h5
# 访问地址
http://localhost:5173/
```
**默认入口**: `pages/agent-system/mobile.vue`
---
## ✨ 功能清单
### 代理端
| 模块 | 功能 | 状态 |
|------|------|------|
| 📊 数据概览 | 今日收益、累计佣金、资产统计 | ✅ |
| 💰 佣金钱包 | 佣金余额、收益明细 | ✅ |
| 💳 余额钱包 | 余额查询、充值记录 | ⏳ |
| 🏦 提现管理 | 提现申请、提现记录 | ⏳ |
| 📦 资产管理 | 资产列表 | ✅ |
| 🎁 套餐管理 | 套餐列表 | ⏳ |
### 企业端
| 模块 | 功能 | 状态 |
|------|------|------|
| 📊 数据概览 | 资产统计、套餐统计 | ✅ |
| 📦 资产管理 | 资产列表 | ✅ |
| 🎁 套餐管理 | 套餐列表 | ⏳ |
| 🏷️ 标签管理 | 标签列表、分配标签 | ✅ |
**注意**: 企业端没有佣金和钱包功能
---
## 🎨 设计风格
### 配色 (小米米家)
```scss
#ff6700 // 主色 - 小米橙
#ff8533 // Hover状态
#fff7f0 // 橙色背景
#fafafa // 页面背景
#ffffff // 卡片背景
#f5f5f5 // 输入框背景
#eeeeee // 分割线
```
### 布局特点
- **顶部固定导航** - Logo + 角色切换 + 搜索
- **卡片化布局** - 2列网格统计卡片
- **圆角** - 12px卡片, 8px按钮
- **柔和阴影** - `shadow-[0_2px_8px_rgba(0,0,0,0.04)]`
---
## 📱 页面结构
```
┌─────────────────────────────────┐
│ Logo [代理端][企业端] │ ← 固定顶部
│ [搜索框] │
├─────────────────────────────────┤
│ │
│ 数据概览 (默认页) │
│ ┌────┬────┐ │
│ │统计│统计│ │
│ ├────┼────┤ │
│ │统计│统计│ │
│ └────┴────┘ │
│ │
│ 快捷操作 (4格) │
│ 收益趋势图 │
│ 最近动态列表 │
│ │
└─────────────────────────────────┘
```
---
## 🧭 导航逻辑
### 页面切换
点击快捷操作按钮切换页面:
- **数据概览** (`dashboard`) - 默认首页
- **佣金钱包** (`commission`) - 佣金余额 + 明细
- **资产管理** (`assets`) - 资产列表
- **标签管理** (`tags`) - 企业端独有
### 返回逻辑
所有子页面都有**返回按钮**,点击返回数据概览
```vue
<button @click="navigateTo('dashboard')">
<i class="i-mdi-chevron-left" />
返回
</button>
```
---
## 💡 使用说明
### 1. 切换角色
顶部右侧角色切换:
```
[代理端] [企业端]
```
### 2. 快捷操作
数据概览页的 4 格快捷操作:
**代理端**:
- 申请提现
- 佣金钱包
- 余额钱包
- 我的资产
**企业端**:
- 标签管理
- 添加资产
- 套餐管理
- 数据统计
### 3. 查看详情
- 点击资产卡片 → 资产详情 (待实现)
- 点击标签卡片 → 标签分配 (待实现)
- 点击佣金记录 → 记录详情 (待实现)
---
## 🎯 下一步开发
### 优先级 P0
- [ ] **余额钱包页** (复制佣金钱包结构)
- [ ] **提现申请表单**
- 输入金额
- 选择提现方式
- 提交按钮
- [ ] **提现记录列表**
### 优先级 P1
- [ ] **资产详情页**
- [ ] **套餐列表页**
- [ ] **套餐详情页**
### 优先级 P2
- [ ] **图表集成** (ECharts for H5)
- [ ] **下拉刷新** (z-paging)
- [ ] **上拉加载更多**
- [ ] **标签分配拖拽**
---
## 🔧 代码示例
### 添加新页面
```vue
<script setup>
const currentView = ref('dashboard')
function navigateTo(view: string) {
currentView.value = view
}
</script>
<template>
<!-- 新页面 -->
<view v-if="currentView === 'new-page'" class="px-4">
<button @click="navigateTo('dashboard')">
<i class="i-mdi-chevron-left" />
返回
</button>
<text class="text-20px font-700">新页面</text>
<!-- 页面内容 -->
</view>
</template>
```
### 小米风格卡片
```vue
<view class="
bg-white rounded-12px p-4
shadow-[0_2px_8px_rgba(0,0,0,0.04)]
">
<text class="text-16px font-600 text-[#212121]">
卡片标题
</text>
</view>
```
### 橙色渐变卡片
```vue
<view class="
bg-gradient-to-br from-[#ff6700] to-[#ff8533]
rounded-16px p-6
shadow-[0_8px_24px_rgba(255,103,0,0.2)]
">
<text class="text-white text-40px font-700">
¥ 12,580.00
</text>
</view>
```
---
## 📐 尺寸规范
### 字号
```
40px - 主要金额
24px - 统计数值
20px - Logo
16px - 卡片标题
15px - 列表标题
14px - 正文
13px - 小标签/按钮
12px - 次要信息
11px - 徽章/状态
```
### 圆角
```
16px - 大卡片 (余额卡)
12px - 标准卡片
8px - 按钮
full - 圆形/药丸形
```
### 间距
```
gap-2 - 8px (小间距)
gap-3 - 12px (标准间距)
gap-4 - 16px (大间距)
gap-6 - 24px (章节间距)
p-4 - 16px (卡片内边距)
px-4 - 16px (页面左右边距)
```
---
## ✅ 检查清单
提交代码前确认:
- [ ] 使用十六进制颜色 (不用CSS变量)
- [ ] 卡片圆角 12px
- [ ] 按钮圆角 8px
- [ ] 阴影透明度 0.04
- [ ] 图标使用 MDI
- [ ] 橙色仅用于强调
- [ ] 测试代理/企业切换
- [ ] 测试返回按钮
- [ ] 页面可滚动
---
## 📂 文件位置
```
src/pages/agent-system/
├── mobile.vue # 移动端主页面 (当前)
└── index.vue # PC端页面 (已废弃)
```
---
**设计理念**: 参考小米米家APP - 简约、卡片化、橙色点缀 🎨

View File

@@ -1,372 +0,0 @@
# 套餐列表页面 - 使用说明
> **小米米家风格** - 简约、纯粹的套餐管理页面
---
## 🚀 快速启动
```bash
pnpm dev:h5
# 访问 http://localhost:5173/
```
**页面入口**: `src/pages/agent-system/index.vue`
---
## ✨ 页面结构
### 简化设计
-**无顶部导航** - 去除Logo、搜索框、角色切换
-**直接显示列表** - 首页即套餐列表
-**两层结构** - 列表页 + 详情页
```
首页 (套餐列表)
├─ 统计卡片 (4个)
├─ 筛选标签 (全部、生效中、待生效、已过期)
└─ 套餐卡片列表
└─ 点击卡片 → 套餐详情页
└─ 点击返回 → 回到首页
```
---
## 📱 页面展示
### 首页 - 套餐列表
```
┌─────────────────────────────────┐
│ │
│ ┌──────┬──────┐ │ ← 统计卡片
│ │套餐 │生效中│ │
│ │总数 │ │ │
│ │ 4 │ 2 │ │
│ └──────┴──────┘ │
│ ┌──────┬──────┐ │
│ │待生效│已过期│ │
│ │ 1 │ 1 │ │
│ └──────┴──────┘ │
│ │
│ [全部] 生效中 待生效 已过期 → │ ← 筛选标签
│ │
│ ┌─────────────────────┐ │ ← 套餐卡片
│ │ 月付套餐 50GB 生效中│ │
│ │ 正式套餐 优先级:1 │ │
│ │ │ │
│ │ 虚流量使用 │ │
│ │ [████░░░] 25/100GB │ │
│ │ 真流量: 12.5/50GB │ │
│ │ │ │
│ │ 激活:03-01 到期:04-01 │ │
│ │ (剩5天) │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ 加油包 20GB 生效中│ │
│ │ 加油包 优先级:2 │ │
│ │ [████░░░] 10/40GB │ │
│ │ 激活:03-15 到期:04-15│ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────┘
```
### 详情页 - 套餐详情
```
┌─────────────────────────────────┐
│ ← 返回 │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │ ← 基本信息
│ │ 月付套餐 50GB 生效中│ │
│ │ │ │
│ │ 套餐类型: 正式套餐 │ │
│ │ 优先级: 1 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │ ← 流量详情
│ │ 流量使用 │ │
│ │ │ │
│ │ 虚流量使用 25% │ │
│ │ [████████░░] │ │
│ │ │ │
│ │ 已用 剩余 总量│ │
│ │ 25GB 75GB 100GB│ │
│ │ │ │
│ │ 真流量: 12.5/50GB │ │
│ │ [████░░░░░] │ │
│ │ │ │
│ │ 虚流量比例: 1:2 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │ ← 时间信息
│ │ 时间信息 │ │
│ │ 创建:2024-02-25 │ │
│ │ 激活:2024-03-01 │ │
│ │ 到期:2024-04-01 │ │
│ │ 剩余5天 │ │
│ └─────────────────────┘ │
│ │
│ [续费套餐] [购买加油包] │
│ │
└─────────────────────────────────┘
```
---
## 🎨 核心功能
### 1. 统计卡片 (4个)
```vue
套餐总数 - 蓝色图标
生效中 - 绿色图标
待生效 - 橙色图标
已过期 - 红色图标
```
### 2. 状态筛选
点击筛选标签过滤套餐:
| 标签 | 筛选条件 | 颜色 |
|------|---------|------|
| 全部 | 显示所有 | 橙色背景 (激活态) |
| 生效中 | status = 1 | 绿色背景 (激活态) |
| 待生效 | status = 0 | 橙色背景 (激活态) |
| 已过期 | status = 3 | 红色背景 (激活态) |
### 3. 套餐卡片
每个卡片显示:
- **标题行** - 套餐名称 + 状态标签
- **类型行** - 套餐类型 + 优先级
- **虚流量进度条** - 8px高度,显示使用比例
- **真流量文字** - 已用/总量
- **时间行** - 激活时间 + 到期时间 + 剩余天数
### 4. 套餐详情
点击卡片进入详情页,显示:
- **基本信息** - 名称、类型、优先级、关联主套餐
- **流量使用** - 虚流量大进度条 + 三格统计 + 真流量小进度条 + 比例
- **时间信息** - 创建、激活、到期、剩余天数
- **操作按钮** - 续费套餐 / 购买加油包
---
## 🎯 状态颜色
### 套餐状态标签
| 状态 | 文字颜色 | 背景颜色 |
|------|---------|---------|
| 待生效 | `#ff9800` | `#fff3e0` |
| 生效中 | `#4caf50` | `#e8f5e9` |
| 已用完 | `#9e9e9e` | `#f5f5f5` |
| 已过期 | `#f44336` | `#ffebee` |
| 已失效 | `#757575` | `#eeeeee` |
### 进度条颜色
```scss
// 虚流量进度条
status === 1 (生效中) #4caf50 绿色
status === 2 (已用完) #f44336 红色
其他状态 #9e9e9e 灰色
// 真流量进度条
固定使用 #2196f3 蓝色
```
### 剩余天数
```scss
7天 #f44336 红色 (紧急)
8-30天 #ff9800 橙色 (提醒)
> 30天 不显示颜色 (正常)
```
---
## 💡 交互说明
### 列表页交互
1. **点击统计卡片** - 无交互 (纯展示)
2. **点击筛选标签** - 过滤套餐列表
3. **点击套餐卡片** - 进入套餐详情页
4. **上下滑动** - 浏览套餐列表
### 详情页交互
1. **点击返回按钮** - 返回套餐列表
2. **点击续费套餐** - (待实现) 打开续费表单
3. **点击购买加油包** - (待实现) 跳转加油包选择页
---
## 📊 数据示例
### 套餐列表数据
```typescript
interface PackageUsage {
package_usage_id: number // 使用记录ID
package_id: number // 套餐模板ID
package_name: string // 套餐名称
package_type: 'formal' | 'addon' // 套餐类型
status: 0 | 1 | 2 | 3 | 4 // 状态
status_name: string // 状态名称
data_limit_mb: number // 真流量总量(MB)
data_usage_mb: number // 已用真流量(MB)
virtual_limit_mb: number // 虚流量总量(MB)
virtual_used_mb: number // 已用虚流量(MB)
virtual_remain_mb: number // 剩余虚流量(MB)
virtual_ratio: number // 虚流量比例
priority: number // 优先级
usage_type: string // 使用类型
master_usage_id: number | null // 主套餐ID
activated_at: string | null // 激活时间
expires_at: string | null // 到期时间
created_at: string // 创建时间
}
```
---
## 🔧 核心函数
### 1. 流量格式化
```typescript
function formatDataSize(mb: number): string {
if (mb < 1024) return `${mb}MB`
return `${(mb / 1024).toFixed(1)}GB`
}
// 示例:
formatDataSize(512) // → "512MB"
formatDataSize(51200) // → "50.0GB"
```
### 2. 使用百分比
```typescript
function calcUsagePercent(used: number, total: number): number {
if (total === 0) return 0
return Math.min(Math.round((used / total) * 100), 100)
}
// 示例:
calcUsagePercent(25600, 102400) // → 25
```
### 3. 剩余天数
```typescript
function calcRemainingDays(expiresAt: string | null): number | null {
if (!expiresAt) return null
const now = new Date()
const expireDate = new Date(expiresAt)
const diff = expireDate.getTime() - now.getTime()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
// 示例:
calcRemainingDays('2024-04-01T10:00:00Z') // → 5 (假设今天是3月27日)
```
### 4. 时间格式化
```typescript
function formatDate(dateStr: string | null): string {
if (!dateStr) return '--'
const date = new Date(dateStr)
return `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`
}
// 示例:
formatDate('2024-03-01T10:00:00Z') // → "3-01"
formatDate(null) // → "--"
```
---
## ✅ 设计检查清单
- [x] 移除顶部导航栏
- [x] 移除Logo和搜索框
- [x] 移除角色切换
- [x] 首页直接显示套餐列表
- [x] 统计卡片正常显示
- [x] 筛选标签功能正常
- [x] 套餐卡片样式正确
- [x] 详情页返回功能正常
- [x] 状态颜色准确
- [x] 进度条显示正确
---
## 🎯 下一步开发
### 优先级 P0 (核心功能)
- [ ] **API接口对接** - 获取真实套餐数据
- [ ] **下拉刷新** - 刷新套餐列表
- [ ] **上拉加载** - 分页加载更多
### 优先级 P1 (重要功能)
- [ ] **续费功能** - 续费套餐表单和支付
- [ ] **购买加油包** - 加油包选择和购买流程
- [ ] **套餐详情操作** - 更多操作选项
### 优先级 P2 (增强体验)
- [ ] **图表统计** - 流量使用趋势图
- [ ] **到期提醒** - 即将到期推送通知
- [ ] **搜索功能** - 按套餐名称搜索
---
## 📐 布局尺寸
### 间距
```scss
页面边距: px-4 (16px)
卡片间距: gap-3 (12px)
卡片内边距: p-4 (16px)
小元素间距: gap-2 (8px)
```
### 字号
```scss
24px - 统计数值
16px - 套餐名称
14px - 按钮文字
13px - 流量数值
12px - 状态标签
11px - 时间信息
```
### 圆角
```scss
12px - 卡片圆角
8px - 进度条圆角
full - 状态标签圆角
```
---
**小米米家风格 - 简约纯粹** 🎨

View File

@@ -1,413 +0,0 @@
# 套餐管理系统 - 移动端H5
> **小米米家风格** - 套餐使用记录管理系统
---
## ✨ 功能说明
这是一个**套餐使用记录管理系统**,展示和管理用户的套餐使用情况。
### 核心功能
-**套餐统计** - 总数、生效中、待生效、已过期
-**套餐列表** - 筛选、搜索、分类展示
-**套餐详情** - 流量使用、时间信息、操作按钮
-**状态管理** - 5种状态标识和颜色区分
-**流量可视化** - 进度条显示虚流量/真流量
---
## 🚀 快速启动
```bash
# 启动开发服务器
pnpm dev:h5
# 访问地址
http://localhost:5173/
```
---
## 📋 数据字段说明
### 套餐状态 (status)
| 值 | 状态名称 | 颜色 | 说明 |
|----|---------|------|------|
| 0 | 待生效 | 橙色 | 已购买但未激活 |
| 1 | 生效中 | 绿色 | 正常使用中 |
| 2 | 已用完 | 灰色 | 流量已耗尽 |
| 3 | 已过期 | 红色 | 超过有效期 |
| 4 | 已失效 | 灰色 | 主动失效 |
### 套餐类型 (package_type)
- **formal** - 正式套餐 (主套餐)
- **addon** - 加油包 (附加套餐,有master_usage_id)
### 使用类型 (usage_type)
- **single_card** - 单卡使用
- **device** - 设备使用
### 流量相关
- **data_limit_mb** - 真流量总量(MB)
- **data_usage_mb** - 已用真流量(MB)
- **virtual_limit_mb** - 虚流量总量(MB)
- **virtual_used_mb** - 已用虚流量(MB)
- **virtual_remain_mb** - 剩余虚流量(MB)
- **virtual_ratio** - 虚流量比例 (真实/虚拟)
**示例**: 如果 virtual_ratio = 0.5
- 真流量 1GB = 虚流量 2GB
- 显示比例: 1:2
---
## 🎨 页面结构
### 1. 数据概览页 (Dashboard)
```
┌─────────────────────────────────┐
│ GT Agent [代理端][企业端] │
│ 🔍 [搜索套餐名称...] │
├─────────────────────────────────┤
│ │
│ ┌──────┬──────┐ │ ← 统计卡片
│ │套餐 │生效中│ │
│ │总数 │ │ │
│ │ 4 │ 2 │ │
│ └──────┴──────┘ │
│ ┌──────┬──────┐ │
│ │待生效│已过期│ │
│ │ 1 │ 1 │ │
│ └──────┴──────┘ │
│ │
│ ┌─────────────────────┐ │ ← 快捷操作
│ │ 套餐 添加 统计 记录│ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │ ← 最近套餐
│ │ 最近套餐 查看全部→│ │
│ │ │ │
│ │ 月付套餐 50GB │ │
│ │ 生效中 │ │
│ │ [进度条] 25/100GB │ │
│ │ 到期: 2024-04-01 │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────┘
```
### 2. 套餐列表页
```
┌─────────────────────────────────┐
│ ← 返回 套餐列表 [筛选] │
├─────────────────────────────────┤
│ │
│ [全部] 生效中 待生效 已过期 → │ ← 筛选标签
│ │
│ ┌─────────────────────┐ │ ← 套餐卡片
│ │ 月付套餐 50GB 生效中│ │
│ │ 正式套餐 优先级:1 │ │
│ │ │ │
│ │ 虚流量使用 │ │
│ │ [████░░░] 25/100GB │ │
│ │ 真流量: 12.5/50GB │ │
│ │ │ │
│ │ 激活:2024-03-01 │ │
│ │ 到期:2024-04-01(剩5天) │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ 加油包 20GB 生效中│ │
│ │ 加油包 优先级:2 │ │
│ │ [████░░░] 10/40GB │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────┘
```
### 3. 套餐详情页
```
┌─────────────────────────────────┐
│ ← 返回列表 │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │ ← 基本信息
│ │ 月付套餐 50GB 生效中│ │
│ │ │ │
│ │ 套餐类型: 正式套餐 │ │
│ │ 优先级: 1 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │ ← 流量使用
│ │ 流量使用 │ │
│ │ │ │
│ │ 虚流量使用 25% │ │
│ │ [████████░░] │ │
│ │ │ │
│ │ 已用 剩余 总量│ │
│ │ 25GB 75GB 100GB│ │
│ │ │ │
│ │ 真流量: 12.5/50GB │ │
│ │ [████░░░░░] │ │
│ │ │ │
│ │ 虚流量比例: 1:2 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │ ← 时间信息
│ │ 时间信息 │ │
│ │ 创建:2024-02-25 │ │
│ │ 激活:2024-03-01 │ │
│ │ 到期:2024-04-01 │ │
│ │ 剩余5天 │ │
│ └─────────────────────┘ │
│ │
│ [续费套餐] [购买加油包] │
│ │
└─────────────────────────────────┘
```
---
## 🎨 状态颜色方案
### 套餐状态标签
```scss
// 待生效 (status: 0)
color: #ff9800
background: #fff3e0
// 生效中 (status: 1)
color: #4caf50
background: #e8f5e9
// 已用完 (status: 2)
color: #9e9e9e
background: #f5f5f5
// 已过期 (status: 3)
color: #f44336
background: #ffebee
// 已失效 (status: 4)
color: #757575
background: #eeeeee
```
### 进度条颜色
```scss
// 正常使用 (status: 1)
进度条: #4caf50 (绿色)
// 已用完 (status: 2)
进度条: #f44336 (红色)
// 真流量进度条
进度条: #2196f3 (蓝色)
```
### 剩余天数颜色
```scss
// ≤ 7天 (即将过期)
text-color: #f44336 (红色)
// 7-30天 (提醒)
text-color: #ff9800 (橙色)
// > 30天 (正常)
text-color: #4caf50 (绿色)
```
---
## 💡 交互说明
### 1. 套餐筛选
点击顶部筛选标签:
- **全部** - 显示所有套餐
- **生效中** - 仅显示 status = 1
- **待生效** - 仅显示 status = 0
- **已过期** - 仅显示 status = 3
### 2. 查看详情
点击套餐卡片 → 跳转详情页
### 3. 返回导航
- 列表页 → 点击"← 返回" → 数据概览
- 详情页 → 点击"← 返回列表" → 列表页
---
## 📊 流量计算逻辑
### 虚流量比例计算
```typescript
// 示例: virtual_ratio = 0.5
// 真流量 1GB = 虚流量 2GB
= 1 : (1 / virtual_ratio)
= 1 : (1 / 0.5)
= 1 : 2
// 虚流量换算
virtual_limit_mb = data_limit_mb / virtual_ratio
= 50GB / 0.5
= 100GB
```
### 使用百分比
```typescript
usagePercent = (used / total) * 100
= (25600 / 102400) * 100
= 25%
```
### 剩余天数
```typescript
remainingDays = (expires_at - now) / (24)
=
// 颜色判断
if (remainingDays <= 7)
else if (remainingDays <= 30)
else 绿
```
---
## 🔧 数据示例
### 示例 1: 正式套餐
```json
{
"package_usage_id": 1,
"package_id": 101,
"package_name": "月付套餐 50GB",
"package_type": "formal",
"status": 1,
"status_name": "生效中",
"data_limit_mb": 51200, // 50GB
"data_usage_mb": 12800, // 12.5GB
"virtual_limit_mb": 102400, // 100GB (50GB / 0.5)
"virtual_used_mb": 25600, // 25GB
"virtual_remain_mb": 76800, // 75GB
"virtual_ratio": 0.5, // 1:2比例
"priority": 1,
"usage_type": "single_card",
"master_usage_id": null,
"activated_at": "2024-03-01T10:00:00Z",
"expires_at": "2024-04-01T10:00:00Z",
"created_at": "2024-02-25T15:30:00Z"
}
```
### 示例 2: 加油包
```json
{
"package_usage_id": 2,
"package_id": 102,
"package_name": "加油包 20GB",
"package_type": "addon",
"status": 1,
"status_name": "生效中",
"data_limit_mb": 20480, // 20GB
"data_usage_mb": 5120, // 5GB
"virtual_limit_mb": 40960, // 40GB
"virtual_used_mb": 10240, // 10GB
"virtual_remain_mb": 30720, // 30GB
"virtual_ratio": 0.5,
"priority": 2,
"usage_type": "single_card",
"master_usage_id": 1, // 关联主套餐ID
"activated_at": "2024-03-15T14:00:00Z",
"expires_at": "2024-04-15T14:00:00Z",
"created_at": "2024-03-15T13:00:00Z"
}
```
---
## ✅ 设计检查清单
- [x] 套餐状态使用对应颜色标签
- [x] 进度条颜色区分状态
- [x] 虚流量/真流量分别展示
- [x] 虚流量比例清晰显示
- [x] 剩余天数有颜色提醒
- [x] 加油包显示主套餐关联
- [x] 待生效套餐不显示激活/到期时间
- [x] 流量单位自动换算 (MB/GB)
- [x] 所有时间格式统一
- [x] 空状态有友好提示
---
## 🎯 下一步开发
### 优先级 P0
- [ ] **API接口对接** - 获取真实套餐数据
- [ ] **搜索功能** - 按套餐名称搜索
- [ ] **下拉刷新** - 刷新套餐列表
- [ ] **分页加载** - 长列表分页
### 优先级 P1
- [ ] **续费功能** - 续费套餐表单
- [ ] **购买加油包** - 加油包选择和购买
- [ ] **套餐详情操作** - 更多操作选项
- [ ] **使用记录** - 流量使用历史
### 优先级 P2
- [ ] **图表统计** - 流量使用趋势图
- [ ] **到期提醒** - 即将到期推送
- [ ] **自动续费** - 自动续费开关
---
## 📐 字段映射表
| API字段 | 显示名称 | 类型 | 说明 |
|---------|---------|------|------|
| package_usage_id | 套餐使用ID | number | 主键 |
| package_id | 套餐ID | number | 套餐模板ID |
| package_name | 套餐名称 | string | 显示标题 |
| package_type | 套餐类型 | string | formal/addon |
| status | 状态码 | number | 0-4 |
| status_name | 状态名称 | string | 中文状态 |
| data_limit_mb | 真流量总量 | number | MB单位 |
| data_usage_mb | 真流量已用 | number | MB单位 |
| virtual_limit_mb | 虚流量总量 | number | MB单位 |
| virtual_used_mb | 虚流量已用 | number | MB单位 |
| virtual_remain_mb | 虚流量剩余 | number | MB单位 |
| virtual_ratio | 虚流量比例 | number | 小数 |
| priority | 优先级 | number | 越小越优先 |
| usage_type | 使用类型 | string | single_card/device |
| master_usage_id | 主套餐ID | number/null | 加油包关联 |
| activated_at | 激活时间 | datetime/null | ISO格式 |
| expires_at | 到期时间 | datetime/null | ISO格式 |
| created_at | 创建时间 | datetime | ISO格式 |
---
**小米米家风格 - 简约、清晰、易用** 🎨