fix: 优化界面以及更新接口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 1m27s

This commit is contained in:
sexygoat
2026-04-15 11:54:43 +08:00
parent 0d0b5ec14a
commit 91d7d4c21f
30 changed files with 1863 additions and 1742 deletions

View File

@@ -39,54 +39,57 @@ Based on analysis: **SaaS (General) + B2B Service**
## 🌈 Color Palette
### Brand Colors (Xiaomi-Inspired)
### Brand Colors (Blue Theme)
```scss
// Primary Brand Colors
--brand-orange: #ff6700; // Primary CTA, hover states
--brand-orange-light: #ff8533; // Lighter variant
--brand-orange-dark: #e65a00; // Pressed states
--brand-gray: #333333; // Headings, important text
--brand-gray-light: #757575; // Secondary text
--brand-primary: #1677ff; // Primary CTA, hover states
--brand-primary-light: #4096ff; // Lighter variant
--brand-primary-dark: #0958d9; // Pressed states
--brand-secondary: #1e293b; // Dark Gray (headings)
--brand-accent: #ffffff; // White (emphasis)
```
### Semantic Colors (Light Mode)
```scss
// Primary Theme Colors
--theme-primary: #ff6700; // Orange (Trust + Energy)
--theme-secondary: #ff8533; // Lighter Orange
--theme-accent: #333333; // Dark Gray
--theme-primary: #1677ff; // Blue (Trust + Energy)
--theme-secondary: #1e293b; // Dark Gray
--theme-accent: #ffffff; // White
// Status Colors
--theme-success: #3ed268; // Green (Success states)
--theme-warning: #e68815; // Amber (Warnings)
--theme-error: #d31638; // Red (Errors)
--theme-success: #52c41a; // Green (Success states)
--theme-warning: #faad14; // Amber (Warnings)
--theme-error: #ff4d4f; // Red (Errors)
--theme-info: #1677ff; // Info (same as primary)
// Text Hierarchy
--theme-main-color: #4c484f; // Primary text (slate-900 equivalent)
--theme-content-color: #6b6569; // Body text (slate-700)
--theme-tips-color: #8a8486; // Secondary text (slate-500)
--theme-light-color: #bd9e84; // Muted text (beige)
--theme-disabled-color: #dce0eb; // Disabled states
--theme-main-color: #212121; // Primary text
--theme-content-color: #666666; // Body text
--theme-tips-color: #999999; // Secondary text
--theme-light-color: #cccccc; // Muted text
--theme-disabled-color: #f5f5f5; // Disabled states
// Backgrounds
--theme-bg-color: #ffffff; // Main background
--theme-bg-color-secondary: #faf8f7; // Card backgrounds
--theme-bg-gradient-start: #c5dfdf;
--theme-bg-gradient-end: #bd9e84;
--theme-bg-color: #fafafa; // Main background
--theme-bg-color-secondary: #ffffff; // Card backgrounds
--theme-bg-gradient-start: #1677ff;
--theme-bg-gradient-end: #4096ff;
// Borders
--theme-border-color: #c5dfdf; // Borders, dividers
--theme-border-color: #e8e8e8; // Borders, dividers
```
### Dark Mode Colors
```scss
.theme-dark {
--theme-primary: #3c9cff; // Blue (better for dark mode)
--theme-success: #5ac725;
--theme-warning: #f9ae3d;
--brand-primary: #4b9cff; // Blue (lighter for dark mode)
--brand-primary-light: #69b1ff;
--brand-primary-dark: #1668dc;
--theme-success: #73d13d;
--theme-warning: #ffc53d;
--theme-error: #f56c6c;
// Text (High Contrast Required)

View File

@@ -0,0 +1,265 @@
# 接口变化说明:资产标识符规范化 + 历史订单 + 导入必填
> **涉及提案**
> - `asset-identifier-standardization`BREAKING
> - `asset-historical-orders`(新增接口)
> - `import-mandatory-virtual-no`(行为变更)
>
> **影响范围**B 端资产操作、设备管理、订单创建、IoT 卡 / 设备导入
---
## 1. 资产操作接口路由变更
**变更前**:路径参数为数据库主键 ID需同时传 `asset_type`
**变更后**路径参数统一为资产标识符ICCID 或 VirtualNo系统内部自动识别资产类型
| 旧路由 | 新路由 |
|---|---|
| `GET /api/admin/assets/:asset_type/:id/realtime-status` | `GET /api/admin/assets/:identifier/realtime-status` |
| `POST /api/admin/assets/:asset_type/:id/refresh` | `POST /api/admin/assets/:identifier/refresh` |
| `GET /api/admin/assets/:asset_type/:id/packages` | `GET /api/admin/assets/:identifier/packages` |
| `GET /api/admin/assets/:asset_type/:id/current-package` | `GET /api/admin/assets/:identifier/current-package` |
| `GET /api/admin/assets/:asset_type/:id/wallet` | `GET /api/admin/assets/:identifier/wallet` |
| `GET /api/admin/assets/:asset_type/:id/wallet/transactions` | `GET /api/admin/assets/:identifier/wallet/transactions` |
| `PATCH /api/admin/assets/:asset_type/:id/polling-status` | `PATCH /api/admin/assets/:identifier/polling-status` |
| `POST /api/admin/assets/device/:device_id/stop` | `POST /api/admin/assets/:identifier/stop` |
| `POST /api/admin/assets/card/:iccid/stop` | `POST /api/admin/assets/:identifier/stop`(已合并) |
| `POST /api/admin/assets/device/:device_id/start` | `POST /api/admin/assets/:identifier/start` |
| `POST /api/admin/assets/card/:iccid/start` | `POST /api/admin/assets/:identifier/start`(已合并) |
| `PATCH /api/admin/devices/:id/deactivate` | `PATCH /api/admin/assets/:identifier/deactivate` |
| `PATCH /api/admin/iot-cards/:id/deactivate` | `PATCH /api/admin/assets/:identifier/deactivate`(已合并) |
**注意事项:**
- `:identifier` 只接受 ICCID 或 VirtualNo全局唯一标识符
- `stop` / `start` / `deactivate` 已合并为统一接口,系统内部按资产类型自动分支处理,前端无需区分卡和设备
- IMEI / SN / MSISDN 仍可用于 `GET /api/admin/assets/resolve/:identifier` 搜索,但不适用于操作接口
---
## 2. 设备管理接口路由变更
**变更前**:路径参数为数据库主键 ID
**变更后**:路径参数改为 VirtualNo虚拟号
| 旧路由 | 新路由 |
|---|---|
| `DELETE /api/admin/devices/:id` | `DELETE /api/admin/devices/:virtual_no` |
| `GET /api/admin/devices/:id/cards` | `GET /api/admin/devices/:virtual_no/cards` |
| `POST /api/admin/devices/:id/cards` | `POST /api/admin/devices/:virtual_no/cards` |
| `DELETE /api/admin/devices/:id/cards/:cardId` | `DELETE /api/admin/devices/:virtual_no/cards/:iccid` |
**注意事项:**
- 解绑卡接口第二段路径参数从 `cardId`(数据库 ID改为 `iccid`ICCID 字符串)
---
## 3. 创建订单接口请求体变更
### B 端:`POST /api/admin/orders`
**请求体字段变化:**
| 字段 | 旧 | 新 | 说明 |
|---|---|---|---|
| `order_type` | 必填 | **废弃** | 系统根据 `identifier` 解析结果自动填入 |
| `iot_card_id` | 单卡购买必填 | **废弃** | 改用 `identifier` |
| `device_id` | 设备购买必填 | **废弃** | 改用 `identifier` |
| `identifier` | — | **必填** | ICCID 或 VirtualNo最长 100 字符 |
| `package_ids` | 必填 | 不变 | — |
| `payment_method` | 必填 | 不变 | — |
**变更后请求体示例:**
```json
{
"identifier": "8986XXXXXXXXXXXXXXXX",[.md](%E5%89%8D%E7%AB%AF%E6%8E%A5%E5%8F%A3%E5%8F%98%E6%9B%B4%E9%80%9A%E7%9F%A5.md)
"package_ids": [5],
"payment_method": "wallet"
}
```
### C 端:`POST /api/c/v1/orders/create`
同 B 端,废弃 `order_type` / `iot_card_id` / `device_id`,统一使用 `identifier`。C 端原本已有此字段,行为不变。
---
## 4. 订单列表接口新增过滤参数
### `GET /api/admin/orders`
新增可选查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `identifier` | string | 按资产标识符精确过滤,匹配订单的 `asset_identifier` 快照字段 |
**示例:**
```
GET /api/admin/orders?identifier=8986XXXXXXXXXXXXXXXX
GET /api/admin/orders?identifier=DEV-001
```
---
## 5. 响应结构新增字段
### `OrderResponse`(所有订单相关接口响应)
| 新增字段 | 类型 | 说明 |
|---|---|---|
| `asset_identifier` | string | 下单时资产的标识符快照,资产删除后仍可读 |
| `asset_type` | string | 资产类型:`single_card``device` |
### `AssetResolveResponse``GET /api/admin/assets/resolve/:identifier`
| 新增字段 | 类型 | 说明 |
|---|---|---|
| `identifier` | string | 原样回传本次查询所用的标识符 |
---
## 6. 新增业务规则
### 已绑设备的卡不可单独购买套餐
- **触发条件**IoT 卡的 `is_standalone = false`(已绑定设备)
- **影响接口**所有套餐购买入口B 端后台、C 端 H5
- **返回错误**
```json
{
"code": 40001,
"msg": "该卡已绑定设备,请前往设备页面购买套餐"
}
```
---
---
## 7. 新增接口:资产历史订单
> **来源提案**`asset-historical-orders`
### `GET /api/admin/assets/:identifier/orders`
查询某资产本代历史订单可选通过换货链追溯前代最多10代
**路径参数:**
| 参数 | 说明 |
|---|---|
| `identifier` | 资产标识符ICCID 或 VirtualNo |
**查询参数:**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int | 否 | 页码,默认 1 |
| `page_size` | int | 否 | 每页条数,默认 20最大 100 |
| `include_previous` | bool | 否 | 是否追溯换货前代,默认 false |
**响应示例:**
```json
{
"code": 0,
"data": {
"current_generation": {
"generation": 2,
"identifier": "VNO-001",
"asset_type": "card",
"total": 3,
"page": 1,
"page_size": 20,
"items": [
{
"order_no": "ORD-20250101-001",
"order_type": "single_card",
"payment_status": 2,
"payment_status_text": "已支付",
"total_amount": 9900,
"payment_method": "wallet",
"paid_at": "2025-01-01T10:00:00Z",
"generation": 2,
"items": [
{
"package_name": "30天1GB套餐",
"quantity": 1,
"unit_price": 9900,
"amount": 9900
}
],
"created_at": "2025-01-01T09:58:00Z"
}
]
},
"previous_generations": [
{
"generation": 1,
"identifier": "VNO-000",
"asset_type": "card",
"exchange_no": "EX-20241201-001",
"exchanged_at": "2024-12-01T10:00:00Z",
"total": 1,
"items": [...]
}
],
"truncated": false
}
}
```
**注意事项:**
- `previous_generations` 仅在 `include_previous=true` 时返回,否则字段不存在
- `truncated: true` 表示换货链超过 10 代,已截断,前代数据不完整
- 前代订单每代最多返回 20 条(不分页)
- 依赖提案一(`asset-identifier-standardization`)中的 `Order.asset_identifier` 快照字段
---
## 8. 行为变更:导入接口 virtual_no 必填
> **来源提案**`import-mandatory-virtual-no`(无 Schema 结构变化,仅行为变更)
### `POST /api/admin/iot-cards/import` — BREAKING
| 场景 | 旧行为 | 新行为 |
|---|---|---|
| Excel 缺少 `virtual_no` 列 | 正常导入,卡的 `virtual_no` 存空字符串 | **整批失败**,任务状态=失败,错误:"Excel 文件缺少 virtual_no 列,请使用最新模板" |
| 某行 `virtual_no` 为空 | 该行导入成功 | 该行计入 `fail_count``failed_items` 含原因:"虚拟号(virtual_no)不能为空" |
| `virtual_no` 重复 | 无变化 | 无变化 |
> IoT 卡导入 Excel 模板**必须新增 `virtual_no` 列**,否则全批失败。
### `POST /api/admin/devices/import` — 统计语义变更
| 场景 | 旧行为 | 新行为 |
|---|---|---|
| 某行 `virtual_no` 为空 | 计入 `skip_count`,无失败详情 | 计入 `fail_count``failed_items` 含原因:"设备虚拟号(virtual_no)不能为空" |
> 前端若有"跳过了 N 行设备"的提示文案,需同步调整为失败处理逻辑。
---
## 前端迁移清单
### asset-identifier-standardization必须改
- [ ]`/assets/:asset_type/:id/` 系列 URL 改为 `/assets/:identifier/`,传 ICCID 或 VirtualNo
- [ ] 停复机不再区分卡/设备,统一调 `/assets/:identifier/stop``/assets/:identifier/start`
- [ ] 停用不再区分 `/iot-cards/:id/deactivate``/devices/:id/deactivate`,统一调 `/assets/:identifier/deactivate`
- [ ] 设备管理接口将 `/:id` 改为 `/:virtual_no`,解绑卡将 `/:cardId` 改为 `/:iccid`
- [ ] 创建订单请求体移除 `order_type``iot_card_id``device_id`,改传 `identifier`
- [ ] 订单列表可通过 `?identifier=xxx` 过滤指定资产的历史订单
- [ ] 展示订单详情时可直接读取 `asset_identifier` 字段,无需额外查询资产
### asset-historical-orders新功能
- [ ] 资产详情页接入 `GET /api/admin/assets/:identifier/orders` 展示历史订单
- [ ] 换货资产可通过 `?include_previous=true` 追溯前代购买记录
### import-mandatory-virtual-no需同步
- [ ] 更新 IoT 卡导入 Excel 模板,必须包含 `virtual_no`
- [ ] 导入结果展示逻辑:设备导入"跳过行"统计移至"失败行",并展示原因

View File

@@ -0,0 +1,129 @@
前端接口变更通知
背景 & 原因
平台需要展示代理的\*\*预充值钱包(主钱包)\*\*余额与流水,原来这部分数据完全不可见。同时,代理的佣金相关接口散落在 /my/ 路径下,与平台统一的 /shops/:id/ 资源路径存在职责重叠,本次统一收拢到 /shops/ 下。
---
一、BREAKING CHANGE — 必须迁移
1. 概况列表接口改造
┌──────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────┐ │ │ 旧 │ 新 │ ├──────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤ │ 路径 │ GET /api/admin/shops/commission-summary │ GET /api/admin/shops/fund-summary │ ├──────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤ │ 入参 │ 完全相同page, page_size, shop_name, username │ 无变化 │ ├──────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤ │ 响应新增字段 │ — │ main_balance预充值余额main_frozen_balance预充值冻结余额 │ ├──────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤ │ 响应删除字段 │ — │ 无 │ └──────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────┘
旧响应 items 字段(已废弃 DTO ShopCommissionSummaryItem→ 新 DTO ShopFundSummaryItem
{ "shop_id": 1, "shop_name": "代理A", "shop_code": "A001", "username": "agent_a", "phone": "138xxxx",
* "main_balance": 200000, // 新增:预充值钱包余额(分)
* "main_frozen_balance": 0, // 新增预充值冻结余额当前均为0 "total_commission": 50000, "withdrawn_commission": 10000, "unwithdraw_commission": 30000, "frozen_commission": 5000, "withdrawing_commission": 5000, "available_commission": 30000, "created_at": "2025-01-01T00:00:00Z" }
▎ 操作:把所有调用 /commission-summary 的地方改为 /fund-summary同时在 UI 上新增"预充值余额"列。
---
2. 废弃的 /my/ 接口 — 全部删除,必须切换
┌────────────────────────────────┬─────────────────────────────────────────────┬─────────────────────────────────────────────┐ │ 废弃路由 │ 替代路由 │ 变化 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ GET /my/commission-summary │ GET /shops/fund-summary │ 见上方,返回结构有变化 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ GET /my/withdrawal-requests │ GET /shops/{shop_id}/withdrawal-requests │ 入参、响应完全一致,只需把 shop_id 放进路径 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ GET /my/commission-records │ GET /shops/{shop_id}/commission-records │ 入参、响应完全一致,只需把 shop_id 放进路径 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ GET /my/commission-stats │ GET /shops/{shop_id}/commission-stats │ 见下方详情 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ GET /my/commission-daily-stats │ GET /shops/{shop_id}/commission-daily-stats │ 见下方详情 │ ├────────────────────────────────┼─────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ POST /my/withdrawal-requests │ POST /shops/{shop_id}/withdrawal-requests │ 见下方详情 │ └────────────────────────────────┴─────────────────────────────────────────────┴─────────────────────────────────────────────┘
▎ 旧 /my/ 接口已从服务端完全删除,调用会 404。
---
二、新增接口详情
GET /api/admin/shops/{shop_id}/commission-stats
替代 GET /my/commission-stats
入参Query
┌────────────┬────────┬──────────────────┐ │ 参数 │ 类型 │ 说明 │ ├────────────┼────────┼──────────────────┤ │ start_time │ string │ 开始时间(可选) │ ├────────────┼────────┼──────────────────┤ │ end_time │ string │ 结束时间(可选) │ └────────────┴────────┴──────────────────┘
▎ 旧接口的 shop_id query 参数已去掉,改为路径参数 {shop_id}
响应(无变化): { "total_amount": 50000, "cost_diff_amount": 30000, "one_time_amount": 20000, "cost_diff_percent": 600, "one_time_percent": 400, "total_count": 15, "cost_diff_count": 10, "one_time_count": 5 }
---
GET /api/admin/shops/{shop_id}/commission-daily-stats
替代 GET /my/commission-daily-stats
入参Query
┌────────────┬────────┬───────────────────────────────────┐ │ 参数 │ 类型 │ 说明 │ ├────────────┼────────┼───────────────────────────────────┤ │ start_date │ string │ 开始日期 YYYY-MM-DD可选 │ ├────────────┼────────┼───────────────────────────────────┤ │ end_date │ string │ 结束日期 YYYY-MM-DD可选 │ ├────────────┼────────┼───────────────────────────────────┤ │ days │ int │ 查询天数默认30最大365可选 │ └────────────┴────────┴───────────────────────────────────┘
响应(无变化): \[ { "date": "2025-04-08", "total_amount": 10000, "total_count": 3 }, { "date": "2025-04-07", "total_amount": 5000, "total_count": 2 } \]
---
POST /api/admin/shops/{shop_id}/withdrawal-requests
替代 POST /my/withdrawal-requests
Body无变化 { "amount": 10000, "withdrawal_method": "alipay", "account_name": "张三", "account_number": "138xxxx" }
权限变化(重要):
* 仅代理账号可调用,平台账号调用返回 403 仅代理商用户可发起提现
* 只能为自己的 shop_id 提现topLevel 代理不能替下级提现,否则返回 403 仅可为本人店铺发起提现
---
GET /api/admin/shops/{shop_id}/main-wallet/transactions ✨ 全新
预充值钱包流水列表(此前无此功能)
入参Query
┌──────────────────┬────────┬────────────────────────────────────────────────────────────────────────┐ │ 参数 │ 类型 │ 说明 │ ├──────────────────┼────────┼────────────────────────────────────────────────────────────────────────┤ │ page │ int │ 页码默认1 │ ├──────────────────┼────────┼────────────────────────────────────────────────────────────────────────┤ │ page_size │ int │ 每页数量默认20最大100 │ ├──────────────────┼────────┼────────────────────────────────────────────────────────────────────────┤ │ transaction_type │ string │ 交易类型过滤recharge充值入账/ deduct套餐扣款/ refund退款 │ ├──────────────────┼────────┼────────────────────────────────────────────────────────────────────────┤ │ start_date │ string │ 开始日期 YYYY-MM-DD │ ├──────────────────┼────────┼────────────────────────────────────────────────────────────────────────┤ │ end_date │ string │ 结束日期 YYYY-MM-DD │ └──────────────────┴────────┴────────────────────────────────────────────────────────────────────────┘
响应: { "items": \[ { "id": 1, "transaction_type": "recharge", "transaction_subtype": "", "amount": 50000, "balance_before": 150000, "balance_after": 200000, "remark": "线下充值", "created_at": "2025-04-08T10:00:00Z" } \], "total": 3, "page": 1, "size": 20 }
▎ amount 正数为入账,负数为扣款,单位:分
---
三、数据权限说明(前端需了解)
两种角色访问同一套接口,后端自动过滤数据:
┌──────────┬─────────────────────────┬──────────────────────────────────────────┐ │ 角色 │ /shops/fund-summary │ /shops/{shop_id}/... │ ├──────────┼─────────────────────────┼──────────────────────────────────────────┤ │ 平台账号 │ 返回所有代理店铺 │ 可查任意 shop_id │ ├──────────┼─────────────────────────┼──────────────────────────────────────────┤ │ 代理账号 │ 返回自己 + 所有下级店铺 │ 只能查自己和下级的 shop_id其他返回 403 │ └──────────┴─────────────────────────┴──────────────────────────────────────────┘
提现操作额外限制:代理账号只能为自己的 shop_id 发起提现,即使是上级代理也不能替下级操作。
---
四、变更全景图
变更前 变更后 ────────────────────────────────────────────────────────────────────── GET /shops/commission-summary → GET /shops/fund-summary ★ 改路径 + 新增字段 GET /shops/:id/withdrawal-requests GET /shops/:id/withdrawal-requests (不变) GET /shops/:id/commission-records GET /shops/:id/commission-records (不变) → GET /shops/:id/commission-stats ★ 新增 → GET /shops/:id/commission-daily-stats ★ 新增 → POST /shops/:id/withdrawal-requests ★ 新增 → GET /shops/:id/main-wallet/transactions ★ 新增(全新功能)
GET /my/commission-summary × 已删除 GET /my/withdrawal-requests × 已删除 GET /my/commission-records × 已删除 GET /my/commission-stats × 已删除 GET /my/commission-daily-stats × 已删除 POST /my/withdrawal-requests × 已删除
---
五、前端改造核实清单
* 将 GET /shops/commission-summary → 改为 GET /shops/fund-summaryUI 新增"预充值余额"列
* 代理侧"我的余额概览"调用改为 GET /shops/fund-summary不再有独立的 /my/commission-summary
* 代理侧"提现记录"改为 GET /shops/{自己shopID}/withdrawal-requests
* 代理侧"佣金明细"改为 GET /shops/{自己shopID}/commission-records
* 代理侧"佣金统计"改为 GET /shops/{自己shopID}/commission-stats去掉 query 里的 shop_id
* 代理侧"每日统计"改为 GET /shops/{自己shopID}/commission-daily-stats去掉 query 里的 shop_id
* 代理侧"发起提现"改为 POST /shops/{自己shopID}/withdrawal-requestsshop_id 进路径body 不变)
* 新建"预充值钱包流水"页面,调用 GET /shops/{shopID}/main-wallet/transactions
\

View File

@@ -1,7 +1,7 @@
/**
* 资产模块 API
*/
import { get, post } from '@/utils/request'
import { get, post, patch } from '@/utils/request'
/**
* 资产类型
@@ -139,13 +139,22 @@ export interface HistoryRecord {
}
/**
* 操作参数
* 操作参数 (旧版,使用 ID)
* @deprecated 使用 AssetOperationParams 代替
*/
export interface OperationParams {
id: string
reason?: string
}
/**
* 操作参数 (新版,使用 identifier)
*/
export interface AssetOperationParams {
identifier: string
reason?: string
}
/**
* 资产解析 (智能识别)
* GET /api/admin/assets/resolve/:identifier
@@ -156,7 +165,8 @@ export function resolveAsset(identifier: string) {
/**
* 获取资产详情
* GET /api/admin/assets/:type/:id
* GET /api/admin/assets/:identifier (已废弃 type/id 格式)
* @deprecated 使用 identifier 代替,路由: GET /api/admin/assets/:identifier
*/
export function getAssetDetail(type: AssetType, id: string) {
return get<AssetDetailResponse>(`/api/admin/assets/${type}/${id}`)
@@ -164,7 +174,7 @@ export function getAssetDetail(type: AssetType, id: string) {
/**
* 获取卡片详情
* GET /api/admin/cards/:id
* @deprecated 请使用 resolveAsset 通过 ICCID 查询
*/
export function getCardDetail(id: string) {
return get<CardInfo>(`/api/admin/cards/${id}`)
@@ -172,10 +182,11 @@ export function getCardDetail(id: string) {
/**
* 获取设备详情
* GET /api/admin/devices/:id
* GET /api/admin/devices/:virtual_no
* @param virtualNo 设备虚拟号
*/
export function getDeviceDetail(id: string) {
return get<DeviceInfo>(`/api/admin/devices/${id}`)
export function getDeviceDetail(virtualNo: string) {
return get<DeviceInfo>(`/api/admin/devices/${virtualNo}`)
}
/**
@@ -243,10 +254,11 @@ export interface AssetRealtimeStatus {
/**
* 获取资产实时状态
* GET /api/admin/assets/:asset_type/:id/realtime-status
* GET /api/admin/assets/:identifier/realtime-status
* @param identifier ICCID 或 VirtualNo
*/
export function getAssetRealtimeStatus(assetType: AssetType, id: number) {
return get<AssetRealtimeStatus>(`/api/admin/assets/${assetType}/${id}/realtime-status`)
export function getAssetRealtimeStatus(identifier: string) {
return get<AssetRealtimeStatus>(`/api/admin/assets/${identifier}/realtime-status`)
}
/**
@@ -275,41 +287,44 @@ export interface AssetPackage {
/**
* 获取资产套餐列表
* GET /api/admin/assets/:asset_type/:id/packages
* GET /api/admin/assets/:identifier/packages
* @param identifier ICCID 或 VirtualNo
*/
export function getAssetPackages(assetType: AssetType, id: number, params?: { page?: number, page_size?: number }) {
export function getAssetPackages(identifier: string, params?: { page?: number, page_size?: number }) {
return get<{
items: AssetPackage[]
page: number
page_size: number
total: number
}>(`/api/admin/assets/${assetType}/${id}/packages`, { params })
}>(`/api/admin/assets/${identifier}/packages`, { params })
}
/**
* 获取当前生效套餐
* GET /api/admin/assets/:asset_type/:id/current-package
* GET /api/admin/assets/:identifier/current-package
* @param identifier ICCID 或 VirtualNo
*/
export function getCurrentPackage(assetType: AssetType, id: number) {
return get<AssetPackage>(`/api/admin/assets/${assetType}/${id}/current-package`)
export function getCurrentPackage(identifier: string) {
return get<AssetPackage>(`/api/admin/assets/${identifier}/current-package`)
}
/**
* 获取资产历史记录
* GET /api/admin/assets/:type/:id/history
* GET /api/admin/assets/:identifier/history
* @param identifier ICCID 或 VirtualNo
*/
export function getAssetHistory(type: AssetType, id: string, params?: { page?: number, pageSize?: number }) {
export function getAssetHistory(identifier: string, params?: { page?: number, pageSize?: number }) {
return get<{
list: HistoryRecord[]
total: number
page: number
pageSize: number
}>(`/api/admin/assets/${type}/${id}/history`, { params })
}>(`/api/admin/assets/${identifier}/history`, { params })
}
/**
* 停用卡片
* POST /api/admin/cards/:id/stop
* @deprecated 使用 stopAsset 代替,路由: POST /api/admin/assets/:identifier/stop
*/
export function stopCard(data: OperationParams) {
return post<void>(`/api/admin/cards/${data.id}/stop`, {
@@ -319,7 +334,7 @@ export function stopCard(data: OperationParams) {
/**
* 启用卡片
* POST /api/admin/cards/:id/start
* @deprecated 使用 startAsset 代替,路由: POST /api/admin/assets/:identifier/start
*/
export function startCard(data: OperationParams) {
return post<void>(`/api/admin/cards/${data.id}/start`, {
@@ -329,21 +344,23 @@ export function startCard(data: OperationParams) {
/**
* 停用设备
* POST /api/admin/devices/:id/stop
* POST /api/admin/devices/:virtual_no/stop
* @param virtualNo 设备虚拟号
*/
export function stopDevice(data: OperationParams) {
return post<void>(`/api/admin/devices/${data.id}/stop`, {
data: { reason: data.reason },
export function stopDevice(virtualNo: string, reason?: string) {
return post<void>(`/api/admin/devices/${virtualNo}/stop`, {
data: { reason },
})
}
/**
* 启用设备
* POST /api/admin/devices/:id/start
* POST /api/admin/devices/:virtual_no/start
* @param virtualNo 设备虚拟号
*/
export function startDevice(data: OperationParams) {
return post<void>(`/api/admin/devices/${data.id}/start`, {
data: { reason: data.reason },
export function startDevice(virtualNo: string, reason?: string) {
return post<void>(`/api/admin/devices/${virtualNo}/start`, {
data: { reason },
})
}
@@ -367,7 +384,7 @@ export interface DeviceStopResponse {
/**
* 设备停机 (批量停机设备下所有已实名卡)
* POST /api/admin/assets/device/:device_id/stop
* @deprecated 使用 stopAsset 代替,路由: POST /api/admin/assets/:identifier/stop
*/
export function stopDeviceAsset(deviceId: number) {
return post<DeviceStopResponse>(`/api/admin/assets/device/${deviceId}/stop`)
@@ -375,7 +392,7 @@ export function stopDeviceAsset(deviceId: number) {
/**
* 设备复机 (批量复机设备下所有已实名卡)
* POST /api/admin/assets/device/:device_id/start
* @deprecated 使用 startAsset 代替,路由: POST /api/admin/assets/:identifier/start
*/
export function startDeviceAsset(deviceId: number) {
return post<void>(`/api/admin/assets/device/${deviceId}/start`)
@@ -383,7 +400,7 @@ export function startDeviceAsset(deviceId: number) {
/**
* 单卡停机 (通过ICCID)
* POST /api/admin/assets/card/:iccid/stop
* @deprecated 使用 stopAsset 代替,路由: POST /api/admin/assets/:identifier/stop
*/
export function stopCardByIccid(iccid: string) {
return post<void>(`/api/admin/assets/card/${iccid}/stop`)
@@ -391,8 +408,135 @@ export function stopCardByIccid(iccid: string) {
/**
* 单卡复机 (通过ICCID)
* POST /api/admin/assets/card/:iccid/start
* @deprecated 使用 startAsset 代替,路由: POST /api/admin/assets/:identifier/start
*/
export function startCardByIccid(iccid: string) {
return post<void>(`/api/admin/assets/card/${iccid}/start`)
}
/**
* 统一停机接口 (卡/设备)
* POST /api/admin/assets/:identifier/stop
* @param identifier ICCID 或 VirtualNo
* @param reason 停机原因
*/
export function stopAsset(identifier: string, reason?: string) {
return post<void>(`/api/admin/assets/${identifier}/stop`, {
data: { reason },
})
}
/**
* 统一复机接口 (卡/设备)
* POST /api/admin/assets/:identifier/start
* @param identifier ICCID 或 VirtualNo
* @param reason 复机原因
*/
export function startAsset(identifier: string, reason?: string) {
return post<void>(`/api/admin/assets/${identifier}/start`, {
data: { reason },
})
}
/**
* 统一停用接口 (卡/设备)
* POST /api/admin/assets/:identifier/deactivate
* @param identifier ICCID 或 VirtualNo
*/
export function deactivateAsset(identifier: string) {
return post<void>(`/api/admin/assets/${identifier}/deactivate`)
}
/**
* 资产钱包信息
* GET /api/admin/assets/:identifier/wallet
* @param identifier ICCID 或 VirtualNo
*/
export function getAssetWallet(identifier: string) {
return get<{
available_balance: number
frozen_balance: number
total_balance: number
}>(`/api/admin/assets/${identifier}/wallet`)
}
/**
* 资产钱包交易记录
* GET /api/admin/assets/:identifier/wallet/transactions
* @param identifier ICCID 或 VirtualNo
*/
export function getAssetWalletTransactions(identifier: string, params?: { page?: number, page_size?: number }) {
return get<{
items: Array<{
id: number
type: string
amount: number
balance_after: number
remark?: string
created_at: string
}>
page: number
page_size: number
total: number
}>(`/api/admin/assets/${identifier}/wallet/transactions`, { params })
}
/**
* 更新资产轮询状态
* PATCH /api/admin/assets/:identifier/polling-status
* @param identifier ICCID 或 VirtualNo
*/
export function updatePollingStatus(identifier: string, enable: boolean) {
return patch<void>(`/api/admin/assets/${identifier}/polling-status`, {
data: { enable_polling: enable },
})
}
/**
* 资产历史订单
* GET /api/admin/assets/:identifier/orders
* @param identifier ICCID 或 VirtualNo
* @param params 查询参数
*/
export function getAssetOrders(identifier: string, params?: {
page?: number
page_size?: number
include_previous?: boolean
}) {
return get<{
current_generation: {
generation: number
identifier: string
asset_type: string
total: number
page: number
page_size: number
items: Array<{
order_no: string
order_type: string
payment_status: number
payment_status_text: string
total_amount: number
payment_method: string
paid_at?: string
generation: number
items: Array<{
package_name: string
quantity: number
unit_price: number
amount: number
}>
created_at: string
}>
}
previous_generations?: Array<{
generation: number
identifier: string
asset_type: string
exchange_no: string
exchanged_at: string
total: number
}>
truncated?: boolean
}>(`/api/admin/assets/${identifier}/orders`, { params })
}

View File

@@ -4,7 +4,29 @@
import { get, post } from '@/utils/request'
/**
* 金概览
* 金概览 (代理端主页用)
* GET /api/admin/shops/fund-summary
*/
export interface FundSummary {
shop_id: number
shop_name: string
shop_code: string
username: string
phone: string
main_balance: number // 预充值钱包余额(分)
main_frozen_balance: number // 预充值冻结余额(分)
total_commission: number // 累计佣金(分)
available_commission: number // 可提现佣金(分)
frozen_commission: number // 冻结佣金(分)
unwithdraw_commission: number // 未提现佣金(分)
withdrawing_commission: number // 提现中金额(分)
withdrawn_commission: number // 已提现佣金(分)
created_at: string
}
/**
* 佣金概览 (旧接口,仅保留类型定义)
* @deprecated 使用 FundSummary 代替
*/
export interface CommissionSummary {
shop_id: number
@@ -67,24 +89,47 @@ export interface CommissionRecordsParams {
}
/**
* 佣金统计查询参数
* 佣金统计查询参数 (新接口shop_id 在路径中)
*/
export interface CommissionStatsParams {
shop_id?: number
start_time?: string
end_time?: string
}
/**
* 每日佣金统计查询参数
* 每日佣金统计查询参数 (新接口shop_id 在路径中)
*/
export interface DailyCommissionStatsParams {
shop_id?: number
start_date?: string // YYYY-MM-DD
end_date?: string // YYYY-MM-DD
days?: number // 查询天数默认30最大365
}
/**
* 预充值钱包交易记录
*/
export interface WalletTransaction {
id: number
transaction_type: string // 交易类型: recharge | deduct | refund
transaction_subtype: string
amount: number // 金额(分),正数为入账,负数为扣款
balance_before: number // 余额变动前(分)
balance_after: number // 余额变动后(分)
remark?: string // 备注
created_at: string // 创建时间
}
/**
* 预充值钱包交易列表参数
*/
export interface WalletTransactionsParams {
page?: number
page_size?: number
transaction_type?: string // 交易类型: recharge | deduct | refund
start_date?: string // YYYY-MM-DD
end_date?: string // YYYY-MM-DD
}
/**
* 列表响应
*/
@@ -95,34 +140,63 @@ export interface ListResponse<T> {
total: number
}
/**
* 获取资金概览 (代理端主页用)
* GET /api/admin/shops/fund-summary
*/
export function getFundSummary() {
return get<{ items: FundSummary[] }>('/api/admin/shops/fund-summary').then(res => res.items?.[0])
}
/**
* 获取我的佣金概览
* GET /api/admin/my/commission-summary
* @deprecated 使用 getFundSummary 代替
* GET /api/admin/shops/{shop_id}/commission-summary
*/
export function getCommissionSummary() {
return get<CommissionSummary>('/api/admin/my/commission-summary')
return get<CommissionSummary>('/api/admin/shops/1/commission-summary')
}
/**
* 获取我的佣金明细
* GET /api/admin/my/commission-records
* GET /api/admin/shops/{shop_id}/commission-records
* @param shopId 店铺ID
*/
export function getCommissionRecords(params?: CommissionRecordsParams) {
return get<ListResponse<CommissionRecord>>('/api/admin/my/commission-records', { params })
export function getCommissionRecords(shopId: number, params?: CommissionRecordsParams) {
return get<ListResponse<CommissionRecord>>(`/api/admin/shops/${shopId}/commission-records`, { params })
}
/**
* 获取我的佣金统计
* GET /api/admin/my/commission-stats
* GET /api/admin/shops/{shop_id}/commission-stats
* @param shopId 店铺ID
* @param params 查询参数
*/
export function getCommissionStats(params?: CommissionStatsParams) {
return get<CommissionStats>('/api/admin/my/commission-stats', { params })
export function getCommissionStats(shopId: number, params?: CommissionStatsParams) {
return get<CommissionStats>(`/api/admin/shops/${shopId}/commission-stats`, { params })
}
/**
* 获取我的每日佣金统计
* GET /api/admin/my/commission-daily-stats
* GET /api/admin/shops/{shop_id}/commission-daily-stats
* @param shopId 店铺ID
* @param params 查询参数
*/
export function getCommissionDailyStats(params?: DailyCommissionStatsParams) {
return get<DailyCommissionStats[]>('/api/admin/my/commission-daily-stats', { params })
export function getCommissionDailyStats(shopId: number, params?: DailyCommissionStatsParams) {
return get<DailyCommissionStats[]>(`/api/admin/shops/${shopId}/commission-daily-stats`, { params })
}
/**
* 获取预充值钱包流水
* GET /api/admin/shops/{shop_id}/main-wallet/transactions
* @param shopId 店铺ID
* @param params 查询参数
*/
export function getMainWalletTransactions(shopId: number, params?: WalletTransactionsParams) {
return get<{
items: WalletTransaction[]
total: number
page: number
size: number
}>(`/api/admin/shops/${shopId}/main-wallet/transactions`, { params })
}

View File

@@ -74,16 +74,20 @@ export interface ListResponse<T> {
/**
* 发起提现申请
* POST /api/admin/my/withdrawal-requests
* POST /api/admin/shops/{shop_id}/withdrawal-requests
* @param shopId 店铺ID
* @param data 提现申请参数
*/
export function createWithdrawal(data: WithdrawalCreateParams) {
return post<WithdrawalCreateResponse>('/api/admin/my/withdrawal-requests', { data })
export function createWithdrawal(shopId: number, data: WithdrawalCreateParams) {
return post<WithdrawalCreateResponse>(`/api/admin/shops/${shopId}/withdrawal-requests`, { data })
}
/**
* 获取我的提现记录
* GET /api/admin/my/withdrawal-requests
* 获取提现记录
* GET /api/admin/shops/{shop_id}/withdrawal-requests
* @param shopId 店铺ID
* @param params 查询参数
*/
export function getWithdrawalRecords(params?: WithdrawalListParams) {
return get<ListResponse<WithdrawalRecord>>('/api/admin/my/withdrawal-requests', { params })
export function getWithdrawalRecords(shopId: number, params?: WithdrawalListParams) {
return get<ListResponse<WithdrawalRecord>>(`/api/admin/shops/${shopId}/withdrawal-requests`, { params })
}

View File

@@ -176,7 +176,7 @@ function selectOption(index: number) {
}
.simple-picker-item-active .simple-picker-item-text {
color: var(--brand-color, #ff6700);
color: var(--brand-primary, #1677ff);
font-weight: 600;
}
</style>

View File

@@ -2,27 +2,35 @@
<view class="skeleton">
<view v-if="type === 'home'" class="skeleton-home">
<!-- 顶部用户信息骨架 -->
<view class="safe-area-top bg-white px-4 pt-4 pb-4">
<view class="flex items-center justify-between">
<view>
<view class="skeleton-block h-6 w-32 mb-2" />
<view class="skeleton-block h-4 w-24" />
<view class="px-4 pt-4">
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] px-4 py-4">
<view class="flex items-center">
<view class="skeleton-circle w-56px h-56px mr-4" />
<view class="flex-1">
<view class="skeleton-block h-5 w-32 mb-2" />
<view class="skeleton-block h-3 w-16 mb-2" />
<view class="skeleton-block h-3 w-24" />
</view>
</view>
<view class="skeleton-circle w-12 h-12" />
</view>
</view>
<!-- 数据卡片骨架 -->
<view class="px-4 pt-4">
<view class="bg-white rounded-2xl p-5">
<view class="grid grid-cols-2 gap-6">
<view class="skeleton-block h-5 w-24 mb-3" />
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
<view class="flex justify-between">
<view>
<view class="skeleton-block h-3 w-16 mb-2" />
<view class="skeleton-block h-8 w-24" />
<view class="skeleton-block h-3 w-20 mb-1" />
<view class="skeleton-block h-5 w-24" />
</view>
<view>
<view class="skeleton-block h-3 w-16 mb-2" />
<view class="skeleton-block h-8 w-24" />
<view class="skeleton-block h-3 w-16 mb-1" />
<view class="skeleton-block h-5 w-20" />
</view>
<view class="text-right">
<view class="skeleton-block h-3 w-20 mb-1" />
<view class="skeleton-block h-5 w-24" />
</view>
</view>
</view>
@@ -30,8 +38,8 @@
<!-- 快捷功能骨架 -->
<view class="px-4 pt-6 pb-4">
<view class="skeleton-block h-4 w-16 mb-3" />
<view class="bg-white rounded-2xl overflow-hidden">
<view class="skeleton-block h-5 w-24 mb-3" />
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<view v-for="i in 5" :key="i" class="flex items-center px-4 py-4" :class="{ 'border-t border-[#f1f5f9]': i > 1 }">
<view class="skeleton-circle w-10 h-10 mr-3" />
<view class="flex-1">

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getAssetDetail, getAssetPackages, getCurrentPackage, getAssetHistory, stopCard, startCard, stopDevice, startDevice } from '@/api/assets'
import { getAssetDetail, getAssetPackages, getCurrentPackage, getAssetHistory, stopAsset, startAsset } from '@/api/assets'
import type { AssetType, CardInfo, DeviceInfo, AssetPackage, HistoryRecord } from '@/api/assets'
// 资产类型
const assetType = ref<AssetType>('card')
// 资产ID
const assetId = ref('')
// 资产标识符 (ICCID 或 VirtualNo)
const assetIdentifier = ref('')
// 资产详情数据
const assetDetail = ref<CardInfo | DeviceInfo | null>(null)
@@ -57,11 +57,11 @@ const statusColor = computed(() => {
// 页面加载
onMounted(() => {
// 从路由参数获取 assetType 和 assetId
// 从路由参数获取 assetType 和 assetIdentifier
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
assetType.value = currentPage.options.type as AssetType
assetId.value = currentPage.options.id
assetIdentifier.value = currentPage.options.id
loadAssetDetail()
})
@@ -120,7 +120,7 @@ async function loadPackages() {
try {
// 1. 调用当前生效套餐接口
try {
const current = await getCurrentPackage(assetType.value, Number(assetId.value))
const current = await getCurrentPackage(assetIdentifier.value)
currentPackage.value = current
} catch (error) {
console.warn('获取当前套餐失败:', error)
@@ -128,7 +128,7 @@ async function loadPackages() {
}
// 2. 调用套餐列表接口
const packagesData = await getAssetPackages(assetType.value, Number(assetId.value), {
const packagesData = await getAssetPackages(assetIdentifier.value, {
page: 1,
page_size: 50
})
@@ -147,7 +147,7 @@ async function loadPackages() {
// 加载历史记录
async function loadHistory() {
try {
const historyData = await getAssetHistory(assetType.value, assetId.value, {
const historyData = await getAssetHistory(assetIdentifier.value, {
page: 1,
pageSize: 20,
})
@@ -193,7 +193,7 @@ async function handleTabChange(tab: 'info' | 'package' | 'history') {
}
// 停机
async function stopAsset() {
async function handleStopAsset() {
uni.showModal({
title: '确认停机',
content: '停机后将无法使用网络服务,确定要停机吗?',
@@ -201,13 +201,8 @@ async function stopAsset() {
if (res.confirm) {
operating.value = true
try {
// 调用停机接口
if (assetType.value === 'card') {
await stopCard({ id: assetId.value })
}
else {
await stopDevice({ id: assetId.value })
}
// 调用统一停机接口
await stopAsset(assetIdentifier.value)
uni.showToast({
title: '停机成功',
@@ -232,7 +227,7 @@ async function stopAsset() {
}
// 复机
async function startAsset() {
async function handleStartAsset() {
uni.showModal({
title: '确认复机',
content: '复机后将恢复网络服务,确定要复机吗?',
@@ -240,13 +235,8 @@ async function startAsset() {
if (res.confirm) {
operating.value = true
try {
// 调用复机接口
if (assetType.value === 'card') {
await startCard({ id: assetId.value })
}
else {
await startDevice({ id: assetId.value })
}
// 调用统一复机接口
await startAsset(assetIdentifier.value)
uni.showToast({
title: '复机成功',
@@ -274,7 +264,7 @@ async function startAsset() {
function viewWallet() {
// 跳转到钱包页面
uni.navigateTo({
url: `/pages/agent-system/asset-wallet/index?type=${assetType.value}&id=${assetId.value}`,
url: `/pages/agent-system/asset-wallet/index?type=${assetType.value}&id=${assetIdentifier.value}`,
})
}
</script>
@@ -459,7 +449,7 @@ function viewWallet() {
<text class="text-16px font-700 text-[#212121] block mb-2">
{{ currentPackage.package_name || '-' }}
</text>
<text class="text-14px text-[#ff6700] font-600 block mb-3">
<text class="text-14px text-[var(--brand-primary)] font-600 block mb-3">
已用: {{ currentPackage.virtual_used_mb || 0 }}MB / 总量: {{ currentPackage.virtual_limit_mb || 0 }}MB
</text>
@@ -546,7 +536,7 @@ function viewWallet() {
transition-all duration-200
active:opacity-80
"
@click="stopAsset"
@click="handleStopAsset"
>
<i class="i-mdi-pause-circle text-18px text-[#c62828] mr-1" />
<text class="text-15px font-600 text-[#c62828]">
@@ -562,7 +552,7 @@ function viewWallet() {
transition-all duration-200
active:opacity-80
"
@click="startAsset"
@click="handleStartAsset"
>
<i class="i-mdi-play-circle text-18px text-[#2e7d32] mr-1" />
<text class="text-15px font-600 text-[#2e7d32]">

View File

@@ -17,7 +17,7 @@
// 套餐列表
const packageList = ref<AssetPackage[]>([])
// 是否展开套餐列表
const showPackageList = ref(false)
const showPackageList = ref(true)
// 是否为卡片
const isCard = computed(() => assetType.value === 'card')
@@ -39,10 +39,11 @@
assetType.value = result.asset_type
assetInfo.value = result
// 加载实时状态和当前套餐
if (result.asset_type && result.asset_id) {
loadRealtimeStatus(result.asset_type, result.asset_id)
loadCurrentPackage(result.asset_type, result.asset_id)
// 加载实时状态和当前套餐 (使用 identifier)
if (searchKeyword.value.trim()) {
loadRealtimeStatus(searchKeyword.value.trim())
loadCurrentPackage(searchKeyword.value.trim())
loadPackageList(searchKeyword.value.trim())
}
} catch (error : any) {
console.error('查询失败:', error)
@@ -53,9 +54,9 @@
}
// 加载实时状态
async function loadRealtimeStatus(type : 'card' | 'device', id : number) {
async function loadRealtimeStatus(identifier : string) {
try {
const status = await getAssetRealtimeStatus(type, id)
const status = await getAssetRealtimeStatus(identifier)
realtimeStatus.value = status
} catch (error : any) {
console.error('加载实时状态失败:', error)
@@ -63,10 +64,10 @@
}
// 加载当前套餐
async function loadCurrentPackage(type : 'card' | 'device', id : number) {
async function loadCurrentPackage(identifier : string) {
try {
console.log('开始加载当前套餐:', type, id)
const pkg = await getCurrentPackage(type, id)
console.log('开始加载当前套餐:', identifier)
const pkg = await getCurrentPackage(identifier)
console.log('当前套餐数据:', pkg)
currentPackage.value = pkg
} catch (error : any) {
@@ -77,10 +78,10 @@
}
// 加载套餐列表
async function loadPackageList(type : 'card' | 'device', id : number) {
async function loadPackageList(identifier : string) {
try {
console.log('开始加载套餐列表:', type, id)
const response = await getAssetPackages(type, id, { page: 1, page_size: 50 })
console.log('开始加载套餐列表:', identifier)
const response = await getAssetPackages(identifier, { page: 1, page_size: 50 })
console.log('套餐列表数据:', response)
packageList.value = response?.items || []
} catch (error : any) {
@@ -93,8 +94,8 @@
async function togglePackageList() {
showPackageList.value = !showPackageList.value
// 第一次展开时加载套餐列表
if (showPackageList.value && packageList.value.length === 0 && assetType.value && assetInfo.value?.asset_id) {
await loadPackageList(assetType.value, assetInfo.value.asset_id)
if (showPackageList.value && packageList.value.length === 0 && searchKeyword.value.trim()) {
await loadPackageList(searchKeyword.value.trim())
}
}
@@ -325,9 +326,9 @@
</text>
</view>
<!-- 电话号码 -->
<!-- msisdn -->
<view v-if="cardInfo.msisdn" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">电话号码</text>
<text class="text-12px text-[#999] w-70px">msisdn</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.msisdn }}
</text>
@@ -376,7 +377,7 @@
<!-- 累计充值 -->
<view v-if="cardInfo.accumulated_recharge !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">累计充值</text>
<text class="flex-1 text-13px font-600 text-[#ff6700] text-right">
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
¥{{ cardInfo.accumulated_recharge }}
</text>
</view>
@@ -424,7 +425,7 @@
<!-- 本月用量 -->
<view class="flex items-center justify-between">
<text class="text-12px text-[#666]">本月用量</text>
<text class="text-13px font-600 text-[#ff6700]">
<text class="text-13px font-600 text-[var(--brand-primary)]">
{{
realtimeStatus.current_month_usage_mb !== null && realtimeStatus.current_month_usage_mb !== undefined
? formatDataSize(realtimeStatus.current_month_usage_mb)
@@ -495,7 +496,7 @@
<view class="flex items-center gap-2">
<i :class="[
'text-16px',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[#ff6700]' : 'i-mdi-gas-station text-[#2e7d32]',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-14px font-600 text-[#212121]">
{{ currentPackage.package_name }}
@@ -564,7 +565,7 @@
<view class="flex items-center gap-1.5">
<i :class="[
'text-14px',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[#ff6700]' : 'i-mdi-gas-station text-[#2e7d32]',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-13px font-600 text-[#212121]">{{ pkg.package_name }}</text>
<text v-if="pkg.master_usage_id" class="text-10px text-[#999]">(加油包)</text>
@@ -697,7 +698,7 @@
<!-- 绑定卡数 -->
<view v-if="deviceInfo.bound_card_count !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">绑定卡数</text>
<text class="flex-1 text-13px font-600 text-[#ff6700] text-right">
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
{{ deviceInfo.bound_card_count }}
</text>
</view>
@@ -753,7 +754,7 @@
<!-- 累计充值 -->
<view v-if="deviceInfo.accumulated_recharge !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">累计充值</text>
<text class="flex-1 text-13px font-600 text-[#ff6700] text-right">
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
¥{{ deviceInfo.accumulated_recharge }}
</text>
</view>
@@ -876,7 +877,7 @@
<!-- 今日用量 -->
<view v-if="realtimeStatus.device_realtime.daily_usage" class="flex items-center justify-between text-11px">
<text class="text-[#666]">今日用量</text>
<text class="text-[#ff6700] font-600">{{ formatBytes(realtimeStatus.device_realtime.daily_usage) }}</text>
<text class="text-[var(--brand-primary)] font-600">{{ formatBytes(realtimeStatus.device_realtime.daily_usage) }}</text>
</view>
<!-- 下载流量 -->
@@ -1017,7 +1018,7 @@
<view class="flex items-center gap-2">
<i :class="[
'text-16px',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[#ff6700]' : 'i-mdi-gas-station text-[#2e7d32]',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-14px font-600 text-[#212121]">
{{ currentPackage.package_name }}
@@ -1086,7 +1087,7 @@
<view class="flex items-center gap-1.5">
<i :class="[
'text-14px',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[#ff6700]' : 'i-mdi-gas-station text-[#2e7d32]',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-13px font-600 text-[#212121]">{{ pkg.package_name }}</text>
<text v-if="pkg.master_usage_id" class="text-10px text-[#999]">(加油包)</text>

View File

@@ -1,99 +1,99 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getAssetWallet } from '@/api/wallet'
import type { AssetWalletInfo, AssetType } from '@/api/wallet'
import { onLoad } from '@dcloudio/uni-app'
import { ref, onMounted } from 'vue'
import { getAssetWallet } from '@/api/wallet'
import type { AssetWalletInfo, AssetType } from '@/api/wallet'
import { onLoad } from '@dcloudio/uni-app'
// 资产类型和ID (从URL参数获取)
const assetType = ref<AssetType>('card')
const assetId = ref<number>(0)
// 资产类型和ID (从URL参数获取)
const assetType = ref<AssetType>('card')
const assetId = ref<number>(0)
// 钱包信息
const walletInfo = ref<AssetWalletInfo | null>(null)
// 钱包信息
const walletInfo = ref<AssetWalletInfo | null>(null)
// 加载状态
const loading = ref(false)
// 加载状态
const loading = ref(false)
// 格式化金额(分转元)
function formatAmount(amount: number) {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取钱包状态文本
function getStatusText(status: number, statusText?: string) {
if (statusText) return statusText
const map: Record<number, string> = {
1: '正常',
2: '冻结',
3: '已停用',
}
return map[status] || '未知'
}
// 获取钱包状态颜色
function getStatusColor(status: number) {
const map: Record<number, { bg: string, text: string }> = {
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 正常
2: { bg: '#fff3e0', text: '#e65100' }, // 冻结
3: { bg: '#f5f5f5', text: '#999' }, // 已停用
}
return map[status] || { bg: '#f5f5f5', text: '#999' }
}
// 格式化时间
function formatTime(time: string) {
if (!time) return '-'
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 页面加载时获取参数
onLoad((options) => {
if (options.asset_type) {
assetType.value = options.asset_type as AssetType
}
if (options.asset_id) {
assetId.value = Number(options.asset_id)
}
})
// 页面挂载
onMounted(() => {
if (assetId.value) {
loadWalletInfo()
}
})
// 加载钱包信息
async function loadWalletInfo() {
if (!assetId.value) {
uni.$u.toast('缺少资产ID')
return
// 格式化金额(分转元)
function formatAmount(amount : number) {
return `¥${(amount / 100).toFixed(2)}`
}
loading.value = true
// 获取钱包状态文本
function getStatusText(status : number, statusText ?: string) {
if (statusText) return statusText
const map : Record<number, string> = {
1: '正常',
2: '冻结',
3: '已停用',
}
return map[status] || '未知'
}
try {
const wallet = await getAssetWallet(assetType.value, assetId.value)
walletInfo.value = wallet
// 获取钱包状态颜色
function getStatusColor(status : number) {
const map : Record<number, { bg : string, text : string }> = {
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 正常
2: { bg: '#fff3e0', text: '#e65100' }, // 冻结
3: { bg: '#f5f5f5', text: '#999' }, // 已停用
}
return map[status] || { bg: '#f5f5f5', text: '#999' }
}
catch (error) {
console.error('加载资产钱包信息失败:', error)
}
finally {
loading.value = false
}
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
// 格式化时间
function formatTime(time : string) {
if (!time) return '-'
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 页面加载时获取参数
onLoad((options) => {
if (options.asset_type) {
assetType.value = options.asset_type as AssetType
}
if (options.asset_id) {
assetId.value = Number(options.asset_id)
}
})
// 页面挂载
onMounted(() => {
if (assetId.value) {
loadWalletInfo()
}
})
// 加载钱包信息
async function loadWalletInfo() {
if (!assetId.value) {
uni.$u.toast('缺少资产ID')
return
}
loading.value = true
try {
const wallet = await getAssetWallet(assetType.value, assetId.value)
walletInfo.value = wallet
}
catch (error) {
console.error('加载资产钱包信息失败:', error)
}
finally {
loading.value = false
}
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
</script>
<template>
@@ -106,7 +106,8 @@ function goBack() {
<!-- 钱包信息 -->
<view v-else-if="walletInfo" class="px-4 pt-4">
<!-- 余额卡片 -->
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4 mb-3">
<view
class="bg-gradient-to-r from-[var(--brand-primary)] to-[var(--brand-primary-light)] rounded-16px shadow-brand-colored p-4 mb-3">
<view class="flex items-center justify-between mb-3">
<view>
<text class="text-13px text-white/70 block mb-1">
@@ -116,13 +117,10 @@ function goBack() {
{{ formatAmount(walletInfo.available_balance) }}
</text>
</view>
<view
:style="{
<view :style="{
backgroundColor: getStatusColor(walletInfo.status).bg,
color: getStatusColor(walletInfo.status).text,
}"
class="px-3 py-1 rounded-8px text-12px font-600"
>
}" class="px-3 py-1 rounded-8px text-12px font-600">
{{ getStatusText(walletInfo.status, walletInfo.status_text) }}
</view>
</view>
@@ -146,7 +144,7 @@ function goBack() {
<!-- 钱包详情 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="flex items-center gap-2 mb-3">
<i class="i-mdi-information-outline text-20px text-[#ff6700]" />
<i class="i-mdi-information-outline text-20px text-[var(--brand-primary)]" />
<text class="text-15px font-600 text-[#212121]">
钱包详情
</text>
@@ -227,26 +225,27 @@ function goBack() {
</template>
<style scoped>
.center {
display: flex;
align-items: center;
justify-content: center;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
.center {
display: flex;
align-items: center;
justify-content: center;
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.space-y-2>view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>

View File

@@ -5,8 +5,7 @@ import type { UserInfo } from '@/api/auth'
import { getEnterpriseCards, getEnterpriseDevices } from '@/api/enterprise'
import { getCarriers } from '@/api/carrier'
import type { CarrierInfo } from '@/api/carrier'
import { stopDeviceAsset, startDeviceAsset, stopCardByIccid, startCardByIccid } from '@/api/assets'
import type { DeviceStopResponse } from '@/api/assets'
import { stopAsset, startAsset } from '@/api/assets'
import Skeleton from '@/components/Skeleton.vue'
import SimplePicker from '@/components/SimplePicker.vue'
import { formatTime, formatDataSize } from '@/utils/format'
@@ -404,10 +403,9 @@ async function handleStopDevice(asset: AssetItem, event: Event) {
event.stopPropagation()
const device = asset.raw as DeviceInfo
const deviceId = device.id || device.device_id
if (!deviceId) {
uni.$u.toast('设备ID不存在')
if (!device.virtual_no) {
uni.$u.toast('设备虚拟号不存在')
return
}
@@ -418,14 +416,13 @@ async function handleStopDevice(asset: AssetItem, event: Event) {
if (res.confirm) {
uni.showLoading({ title: '停机中...' })
try {
const result = await stopDeviceAsset(Number(deviceId))
const result = await stopAsset(device.virtual_no)
uni.hideLoading()
// 显示停机结果
const msg = `停机完成\n成功: ${result.success_count}\n失败: ${result.fail_count}\n跳过: ${result.skip_count}`
// 显示停机结果 (统一接口不返回详情,仅提示成功)
uni.showModal({
title: '停机结果',
content: msg,
title: '停机成功',
content: '设备已停机',
showCancel: false,
success: () => {
// 刷新列表
@@ -447,10 +444,9 @@ async function handleStartDevice(asset: AssetItem, event: Event) {
event.stopPropagation()
const device = asset.raw as DeviceInfo
const deviceId = device.id || device.device_id
if (!deviceId) {
uni.$u.toast('设备ID不存在')
if (!device.virtual_no) {
uni.$u.toast('设备虚拟号不存在')
return
}
@@ -461,7 +457,7 @@ async function handleStartDevice(asset: AssetItem, event: Event) {
if (res.confirm) {
uni.showLoading({ title: '复机中...' })
try {
await startDeviceAsset(Number(deviceId))
await startAsset(device.virtual_no)
uni.hideLoading()
uni.$u.toast('复机成功')
// 刷新列表
@@ -494,7 +490,7 @@ async function handleStopCard(asset: AssetItem, event: Event) {
if (res.confirm) {
uni.showLoading({ title: '停机中...' })
try {
await stopCardByIccid(card.iccid)
await stopAsset(card.iccid)
uni.hideLoading()
uni.$u.toast('停机成功')
// 刷新列表
@@ -527,7 +523,7 @@ async function handleStartCard(asset: AssetItem, event: Event) {
if (res.confirm) {
uni.showLoading({ title: '复机中...' })
try {
await startCardByIccid(card.iccid)
await startAsset(card.iccid)
uni.hideLoading()
uni.$u.toast('复机成功')
// 刷新列表
@@ -682,135 +678,124 @@ onMounted(async () => {
@scroll="onScroll"
>
<view v-if="assetList.length > 0" class="px-4 pb-4">
<view
v-for="asset in assetList"
:key="`${asset.type}-${asset.id}`"
class="card shadow-sm mb-3 cursor-pointer transition-fast hover:shadow-md active:scale-98"
@click="viewAssetDetail(asset)"
>
<!-- 部区域 -->
<view class="flex items-start justify-between mb-3">
<!-- 左侧信息 -->
<view class="flex items-start gap-2 flex-1 min-w-0">
<!-- 状态指示点 -->
<view
class="w-2 h-2 rounded-full mt-1 flex-shrink-0"
:style="{ backgroundColor: asset.statusColor }"
/>
<!-- 资产名称 -->
<view class="flex-1 min-w-0">
<text class="text-base font-600 text-primary block truncate">
<view
v-for="asset in assetList"
:key="`${asset.type}-${asset.id}`"
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3 cursor-pointer"
@click="viewAssetDetail(asset)"
>
<!-- 部区域左侧状态+名称右侧类型标签 -->
<view class="flex items-start justify-between mb-3">
<view class="flex items-center gap-2 flex-1 min-w-0">
<view
class="w-2 h-2 rounded-full mt-1 flex-shrink-0"
:style="{ backgroundColor: asset.statusColor }"
/>
<text class="text-15px font-600 text-[#212121] truncate">
{{ asset.name }}
</text>
<text class="text-xs text-tertiary mt-1 block">
{{ asset.detail }}
</text>
</view>
<view
class="px-2 py-1 rounded-6px text-11px font-500 flex-shrink-0 ml-2"
:style="{
backgroundColor: asset.type === 'card' ? '#e6f4ff' : '#f5f5f5',
color: asset.type === 'card' ? 'var(--brand-primary)' : '#666',
}"
>
{{ asset.type === 'card' ? 'IoT卡' : '设备' }}
</view>
</view>
<!-- 类型徽章 -->
<view
class="px-2 py-1 radius-sm text-xs font-500 ml-2 flex-shrink-0"
:style="{
backgroundColor: asset.type === 'card' ? 'rgba(255, 103, 0, 0.1)' : 'rgba(51, 51, 51, 0.1)',
color: asset.type === 'card' ? 'var(--brand-primary)' : 'var(--brand-secondary)',
}"
>
{{ asset.type === 'card' ? 'IoT卡' : '设备' }}
</view>
</view>
<!-- 底部状态栏 -->
<view class="flex items-center justify-between pt-3 border-t border-border-secondary">
<!-- 状态标签 -->
<view class="flex items-center gap-1">
<view
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: asset.statusColor }"
/>
<text class="text-sm font-500" :style="{ color: asset.statusColor }">
{{ asset.status }}
<!-- 详情信息 -->
<view class="mb-3">
<text class="text-12px text-[#999]">
{{ asset.detail }}
</text>
</view>
<!-- 操作按钮 -->
<view class="flex items-center gap-2">
<!-- IoT卡操作按钮 -->
<template v-if="asset.type === 'card'">
<!-- 根据网络状态显示对应按钮 -->
<template v-if="(asset.raw as CardInfo).network_status === 0">
<!-- 已停机,只显示复机按钮 -->
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
style="background: #52c41a; color: white;"
@click="handleStartCard(asset, $event)"
>
<text class="text-xs font-500">复机</text>
</view>
</template>
<template v-else-if="(asset.raw as CardInfo).network_status === 1">
<!-- 正常运行,只显示停机按钮 -->
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
style="background: #ff4d4f; color: white;"
@click="handleStopCard(asset, $event)"
>
<text class="text-xs font-500">停机</text>
</view>
<!-- 底部区域状态标签 + 操作按钮 -->
<view class="flex items-center justify-between pt-3 border-t border-[#f5f5f5]">
<view class="flex items-center gap-1">
<view
class="w-1.5 h-1.5 rounded-full"
:style="{ backgroundColor: asset.statusColor }"
/>
<text class="text-13px font-500" :style="{ color: asset.statusColor }">
{{ asset.status }}
</text>
</view>
<view class="flex items-center gap-2">
<!-- IoT卡操作按钮 -->
<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;"
@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;"
@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;"
@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;"
@click="handleStartCard(asset, $event)"
>
<text class="text-12px font-500">复机</text>
</view>
</template>
</template>
<!-- 设备操作按钮 -->
<template v-else>
<!-- 其他状态,显示两个按钮 -->
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
class="px-3 py-1 rounded-lg"
style="background: #ff4d4f; color: white;"
@click="handleStopCard(asset, $event)"
@click="handleStopDevice(asset, $event)"
>
<text class="text-xs font-500">停机</text>
<text class="text-12px font-500">停机</text>
</view>
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
class="px-3 py-1 rounded-lg"
style="background: #52c41a; color: white;"
@click="handleStartCard(asset, $event)"
@click="handleStartDevice(asset, $event)"
>
<text class="text-xs font-500">复机</text>
<text class="text-12px font-500">复机</text>
</view>
</template>
</template>
<!-- 设备操作按钮 - 始终显示两个按钮 -->
<template v-else>
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
style="background: #ff4d4f; color: white;"
@click="handleStopDevice(asset, $event)"
>
<text class="text-xs font-500">停机</text>
</view>
<view
class="px-3 py-1 rounded-lg flex items-center justify-center"
style="background: #52c41a; color: white;"
@click="handleStartDevice(asset, $event)"
>
<text class="text-xs font-500">复机</text>
</view>
</template>
<!-- 箭头 -->
<i class="i-mdi-chevron-right text-xl text-quaternary" />
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="py-4 text-center">
<i class="i-mdi-loading animate-spin text-xl text-brand mr-2" />
<text class="text-sm text-[#64748b]">加载中...</text>
<!-- 加载更多提示 -->
<view v-if="loading" class="py-4 text-center">
<i class="i-mdi-loading animate-spin text-xl text-brand mr-2" />
<text class="text-sm text-[#64748b]">加载中...</text>
</view>
<view v-else-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))" class="py-4 text-center">
<text class="text-sm text-[#94a3b8]">没有更多了</text>
</view>
</view>
<view v-else-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))" class="py-4 text-center">
<text class="text-sm text-[#94a3b8]">没有更多了</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="px-4">

View File

@@ -53,7 +53,7 @@ const isFormValid = computed(() => {
const { old_password, new_password, confirm_password } = formData.value
return (
old_password.length > 0
&& new_password.length >= 6
&& new_password.length >= 8
&& new_password === confirm_password
)
})
@@ -74,9 +74,9 @@ async function submitChange() {
return
}
if (formData.value.new_password.length < 6) {
if (formData.value.new_password.length < 8) {
uni.showToast({
title: '新密码至少6位',
title: '新密码至少8位',
icon: 'none',
})
return
@@ -149,256 +149,126 @@ function clearForm() {
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 内容区域 -->
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view class="px-4 pt-5">
<!-- 提示信息 -->
<view class="bg-[#e3f2fd] rounded-12px p-4 mb-4 flex items-start gap-3">
<i class="i-mdi-information text-20px text-[#1976d2] mt-0.5" />
<view class="flex-1">
<text class="text-13px text-[#1976d2] block mb-1 font-600">
密码安全提示
</text>
<text class="text-12px text-[#1976d2] leading-relaxed">
为了您的账号安全,建议密码长度至少8位,包含大小写字母数字和特殊字符
</text>
</view>
</view>
<!-- 表单卡片 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
<!-- 当前密码 -->
<view class="mb-4">
<text class="text-13px text-[#999] block mb-2">
当前密码 <text class="text-[#ff6b6b]">*</text>
</text>
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
<i class="i-mdi-lock text-20px text-[#999]" />
<input
v-model="formData.old_password"
:password="!passwordVisible.old"
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
placeholder="请输入当前密码"
>
<i
:class="[
'text-20px text-[#999]',
passwordVisible.old ? 'i-mdi-eye-off' : 'i-mdi-eye',
]"
@click="togglePasswordVisible('old')"
/>
</view>
</view>
<!-- 新密码 -->
<view class="mb-4">
<view class="flex items-center justify-between mb-2">
<text class="text-13px text-[#999]">
新密码 <text class="text-[#ff6b6b]">*</text>
</text>
<view
v-if="formData.new_password"
class="flex items-center gap-1"
>
<text class="text-11px text-[#999]">
强度:
</text>
<text
:style="{ color: passwordStrength.color }"
class="text-11px font-600"
>
{{ passwordStrength.text }}
</text>
</view>
</view>
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
<i class="i-mdi-lock-outline text-20px text-[#999]" />
<input
v-model="formData.new_password"
:password="!passwordVisible.new"
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
placeholder="请输入新密码(至少6位)"
>
<i
:class="[
'text-20px text-[#999]',
passwordVisible.new ? 'i-mdi-eye-off' : 'i-mdi-eye',
]"
@click="togglePasswordVisible('new')"
/>
</view>
<!-- 密码强度指示器 -->
<view
v-if="formData.new_password"
class="flex items-center gap-1 mt-2"
<view class="bg-white rounded-12px p-4 mb-3">
<text class="text-12px text-[#999] block mb-2">当前密码</text>
<view class="flex items-center">
<input
v-model="formData.old_password"
:password="!passwordVisible.old"
class="flex-1 text-16px text-[#212121]"
placeholder="请输入当前密码"
placeholder-class="text-[#ccc]"
>
<view
v-for="i in 3"
:key="i"
:class="[
'flex-1 h-2px rounded-full',
i <= passwordStrength.level ? 'opacity-100' : 'opacity-20',
]"
:style="{ backgroundColor: passwordStrength.color }"
/>
</view>
</view>
<!-- 确认密码 -->
<view>
<text class="text-13px text-[#999] block mb-2">
确认密码 <text class="text-[#ff6b6b]">*</text>
</text>
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
<i class="i-mdi-lock-check text-20px text-[#999]" />
<input
v-model="formData.confirm_password"
:password="!passwordVisible.confirm"
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
placeholder="请再次输入新密码"
>
<i
:class="[
'text-20px text-[#999]',
passwordVisible.confirm ? 'i-mdi-eye-off' : 'i-mdi-eye',
]"
@click="togglePasswordVisible('confirm')"
/>
</view>
<!-- 密码匹配提示 -->
<view
v-if="formData.confirm_password"
class="flex items-center gap-1 mt-2"
>
<i
:class="[
'text-14px',
formData.new_password === formData.confirm_password
? 'i-mdi-check-circle text-[#3ed268]'
: 'i-mdi-close-circle text-[#ff6b6b]',
]"
/>
<text
:class="[
'text-12px',
formData.new_password === formData.confirm_password
? 'text-[#3ed268]'
: 'text-[#ff6b6b]',
]"
>
{{ formData.new_password === formData.confirm_password ? '密码一致' : '密码不一致' }}
</text>
</view>
</view>
</view>
<!-- 密码要求说明 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
<text class="text-14px font-600 text-[#212121] block mb-3">
密码要求
</text>
<view class="space-y-2">
<view class="flex items-start gap-2">
<i
:class="[
'text-14px mt-0.5',
formData.new_password.length >= 6
? 'i-mdi-check-circle text-[#3ed268]'
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
]"
/>
<text class="flex-1 text-13px text-[#666]">
至少6个字符(建议8个以上)
</text>
</view>
<view class="flex items-start gap-2">
<i
:class="[
'text-14px mt-0.5',
/[a-zA-Z]/.test(formData.new_password)
? 'i-mdi-check-circle text-[#3ed268]'
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
]"
/>
<text class="flex-1 text-13px text-[#666]">
包含字母
</text>
</view>
<view class="flex items-start gap-2">
<i
:class="[
'text-14px mt-0.5',
/\d/.test(formData.new_password)
? 'i-mdi-check-circle text-[#3ed268]'
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
]"
/>
<text class="flex-1 text-13px text-[#666]">
包含数字
</text>
</view>
<view class="flex items-start gap-2">
<i
:class="[
'text-14px mt-0.5',
/[^a-zA-Z\d]/.test(formData.new_password)
? 'i-mdi-check-circle text-[#3ed268]'
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
]"
/>
<text class="flex-1 text-13px text-[#666]">
包含特殊字符(推荐)
</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="space-y-3">
<!-- 确认修改 -->
<view
:class="[
'py-3 rounded-12px center',
'transition-all duration-200',
isFormValid && !submitting
? 'bg-[#212121] active:bg-[#000]'
: 'bg-[#ccc]',
]"
@click="submitChange"
>
<i
v-if="submitting"
class="i-mdi-loading animate-spin text-18px text-white mr-2"
<image
:src="passwordVisible.old ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
class="w-20px h-20px"
mode="aspectFit"
@click="togglePasswordVisible('old')"
/>
<text class="text-15px font-600 text-white">
{{ submitting ? '修改中...' : '确认修改' }}
</view>
</view>
<view class="bg-white rounded-12px p-4 mb-3">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#999]">新密码</text>
<text
v-if="formData.new_password"
:style="{ color: passwordStrength.color }"
class="text-12px font-600"
>
{{ passwordStrength.text }}
</text>
</view>
<view class="flex items-center">
<input
v-model="formData.new_password"
:password="!passwordVisible.new"
class="flex-1 text-16px text-[#212121]"
placeholder="请输入新密码"
placeholder-class="text-[#ccc]"
>
<image
:src="passwordVisible.new ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
class="w-20px h-20px"
mode="aspectFit"
@click="togglePasswordVisible('new')"
/>
</view>
<view v-if="formData.new_password" class="flex items-center gap-1 mt-2">
<view
v-for="i in 3"
:key="i"
class="flex-1 h-2px rounded-full"
:style="{ backgroundColor: i <= passwordStrength.level ? passwordStrength.color : '#e0e0e0' }"
/>
</view>
</view>
<!-- 清空表单 -->
<view
class="
py-3 rounded-12px center
bg-white shadow-[0_2px_12px_rgba(0,0,0,0.06)]
transition-all duration-200
active:bg-[#f5f5f5]
"
@click="clearForm"
>
<i class="i-mdi-refresh text-18px text-[#666] mr-2" />
<text class="text-15px font-600 text-[#666]">
清空
<view class="bg-white rounded-12px p-4 mb-3">
<text class="text-12px text-[#999] block mb-2">确认密码</text>
<view class="flex items-center">
<input
v-model="formData.confirm_password"
:password="!passwordVisible.confirm"
class="flex-1 text-16px text-[#212121]"
placeholder="请再次输入新密码"
placeholder-class="text-[#ccc]"
>
<image
:src="passwordVisible.confirm ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
class="w-20px h-20px"
mode="aspectFit"
@click="togglePasswordVisible('confirm')"
/>
</view>
<view v-if="formData.confirm_password" class="mt-2">
<text
:class="[
'text-12px',
formData.new_password === formData.confirm_password ? 'text-[#52c41a]' : 'text-[#ff4d4f]',
]"
>
{{ formData.new_password === formData.confirm_password ? '密码一致' : '密码不一致' }}
</text>
</view>
</view>
<!-- 底部留白 -->
<view
:class="[
'py-4 rounded-12px text-center text-16px font-600',
isFormValid && !submitting
? 'bg-brand text-white'
: 'bg-[#f5f5f5] text-[#ccc]',
]"
@click="submitChange"
>
{{ submitting ? '修改中...' : '确认修改' }}
</view>
<view class="bg-white rounded-12px p-4 mt-4">
<text class="text-13px font-600 text-[#212121] block mb-3">密码要求</text>
<view class="space-y-2">
<view class="flex items-center gap-2">
<text :class="formData.new_password.length >= 8 ? 'text-[#52c41a]' : 'text-[#ccc]'"></text>
<text class="text-13px text-[#666]">至少8个字符</text>
</view>
<view class="flex items-center gap-2">
<text :class="/[a-zA-Z]/.test(formData.new_password) ? 'text-[#52c41a]' : 'text-[#ccc]'"></text>
<text class="text-13px text-[#666]">包含字母</text>
</view>
<view class="flex items-center gap-2">
<text :class="/\d/.test(formData.new_password) ? 'text-[#52c41a]' : 'text-[#ccc]'"></text>
<text class="text-13px text-[#666]">包含数字</text>
</view>
<view class="flex items-center gap-2">
<text :class="/[^a-zA-Z\d]/.test(formData.new_password) ? 'text-[#52c41a]' : 'text-[#ccc]'"></text>
<text class="text-13px text-[#666]">包含特殊字符</text>
</view>
</view>
</view>
<view class="h-8" />
</view>
</view>
@@ -423,6 +293,10 @@ function clearForm() {
line-height: 1.6;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);

View File

@@ -1,11 +1,20 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getCommissionSummary, getCommissionRecords, getCommissionStats } from '@/api/commission'
import type { CommissionSummary, CommissionRecord, CommissionStats } from '@/api/commission'
import { getFundSummary, getCommissionRecords, getCommissionStats } from '@/api/commission'
import type { FundSummary, CommissionRecord, CommissionStats } from '@/api/commission'
import SimplePicker from '@/components/SimplePicker.vue'
// 佣金概览
const commissionSummary = ref<CommissionSummary | null>(null)
const commissionSummary = ref<FundSummary | null>(null)
// 店铺ID (从登录信息获取)
const shopId = ref<number>(0)
// 从本地存储获取用户信息
function getUserInfo() {
const userInfo = uni.getStorageSync('user_info')
return userInfo || null
}
// 佣金明细列表
const commissionRecords = ref<CommissionRecord[]>([])
@@ -92,6 +101,12 @@ function formatTime(time: string) {
// 页面加载
onMounted(() => {
// 从登录信息获取 shop_id
const userInfo = getUserInfo()
if (userInfo?.shop_id) {
shopId.value = userInfo.shop_id
}
loadCommissionSummary()
loadCommissionStats()
loadCommissionRecords()
@@ -100,7 +115,7 @@ onMounted(() => {
// 加载佣金概览
async function loadCommissionSummary() {
try {
const summary = await getCommissionSummary()
const summary = await getFundSummary()
commissionSummary.value = summary
}
catch (error) {
@@ -110,8 +125,10 @@ async function loadCommissionSummary() {
// 加载佣金统计
async function loadCommissionStats() {
if (!shopId.value) return
try {
const stats = await getCommissionStats()
const stats = await getCommissionStats(shopId.value)
commissionStats.value = stats
}
catch (error) {
@@ -121,6 +138,8 @@ async function loadCommissionStats() {
// 加载佣金明细
async function loadCommissionRecords() {
if (!shopId.value) return
loadingRecords.value = true
try {
@@ -152,7 +171,7 @@ async function loadCommissionRecords() {
}
}
const response = await getCommissionRecords(params)
const response = await getCommissionRecords(shopId.value, params)
if (page.value === 1) {
commissionRecords.value = response.items || []
@@ -214,217 +233,155 @@ function refreshList() {
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 加载中 -->
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view v-if="loading" class="pt-20 center">
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<!-- 内容区域 -->
<view v-else class="px-4 pt-4">
<!-- 佣金概览卡片 -->
<view v-if="commissionSummary" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="flex items-center justify-between mb-3">
<view class="flex items-center gap-2">
<i class="i-mdi-currency-cny text-24px text-[#ff6700]" />
<text class="text-15px font-600 text-[#212121]">
佣金概览
</text>
</view>
</view>
<view v-else>
<view class="bg-brand px-4 py-3 mx-4 mt-4 rounded-12px">
<text class="text-11px text-white/60">可提现佣金</text>
<text class="text-24px font-700 text-white block mt-1">
{{ commissionSummary ? formatAmount(commissionSummary.available_commission) : '--' }}
</text>
</view>
<view class="grid grid-cols-2 gap-3">
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
<text class="text-11px text-[#999] block mb-1">可提现</text>
<text class="text-18px font-700 text-[#ff6700]">
{{ formatAmount(commissionSummary.available_commission) }}
<view class="bg-[var(--bg-container)] rounded-12px p-4 mx-4 mt-3">
<view class="grid grid-cols-3 gap-4">
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">累计佣金</text>
<text class="text-16px font-700 text-[var(--text-primary)] mt-1">
{{ commissionSummary ? formatAmount(commissionSummary.total_commission) : '--' }}
</text>
</view>
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
<text class="text-11px text-[#999] block mb-1">累计佣金</text>
<text class="text-18px font-700 text-[#212121]">
{{ formatAmount(commissionSummary.total_commission) }}
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">冻结</text>
<text class="text-16px font-700 text-[var(--text-tertiary)] mt-1">
{{ commissionSummary ? formatAmount(commissionSummary.frozen_commission) : '--' }}
</text>
</view>
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
<text class="text-11px text-[#999] block mb-1">冻结</text>
<text class="text-18px font-700 text-[#999]">
{{ formatAmount(commissionSummary.frozen_commission) }}
</text>
</view>
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
<text class="text-11px text-[#999] block mb-1">已提现</text>
<text class="text-18px font-700 text-[#52c41a]">
{{ formatAmount(commissionSummary.withdrawn_commission) }}
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">已提现</text>
<text class="text-16px font-700 text-[#52c41a] mt-1">
{{ commissionSummary ? formatAmount(commissionSummary.withdrawn_commission) : '--' }}
</text>
</view>
</view>
</view>
<!-- 佣金统计卡片 -->
<view v-if="commissionStats" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="px-4 mt-4">
<text class="text-14px font-600 text-[var(--text-primary)] block mb-2">佣金统计</text>
<view class="bg-[var(--bg-container)] rounded-12px p-4">
<view class="flex items-center justify-between">
<text class="text-13px text-[var(--text-secondary)]">成本价差</text>
<view class="flex items-center gap-2">
<text class="text-14px font-600 text-[var(--text-primary)]">
{{ commissionStats ? formatAmount(commissionStats.cost_diff_amount) : '--' }}
</text>
<text class="text-11px text-[var(--text-tertiary)]">
{{ commissionStats ? (commissionStats.cost_diff_percent / 10).toFixed(1) + '%' : '--' }}
</text>
</view>
</view>
<view class="flex items-center justify-between mt-3">
<text class="text-13px text-[var(--text-secondary)]">一次性佣金</text>
<view class="flex items-center gap-2">
<text class="text-14px font-600 text-[var(--text-primary)]">
{{ commissionStats ? formatAmount(commissionStats.one_time_amount) : '--' }}
</text>
<text class="text-11px text-[var(--text-tertiary)]">
{{ commissionStats ? (commissionStats.one_time_percent / 10).toFixed(1) + '%' : '--' }}
</text>
</view>
</view>
<view class="flex items-center justify-between mt-3 pt-3 border-t border-[var(--border-secondary)]">
<text class="text-13px font-600 text-[var(--text-primary)]">总计</text>
<text class="text-16px font-700 text-brand">
{{ commissionStats ? formatAmount(commissionStats.total_amount) : '--' }}
</text>
</view>
</view>
</view>
<view class="px-4 mt-4">
<text class="text-14px font-600 text-[var(--text-primary)] block mb-2">佣金明细</text>
<view class="flex items-center gap-2 mb-3">
<i class="i-mdi-chart-pie text-20px text-[#ff6700]" />
<text class="text-15px font-600 text-[#212121]">
佣金统计
</text>
</view>
<view class="space-y-2">
<view class="flex items-center justify-between">
<text class="text-13px text-[#666]">成本价差</text>
<view class="flex items-center gap-2">
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionStats.cost_diff_amount) }}
</text>
<text class="text-11px text-[#999]">
{{ (commissionStats.cost_diff_percent / 10).toFixed(1) }}%
</text>
</view>
</view>
<view class="flex items-center justify-between">
<text class="text-13px text-[#666]">一次性佣金</text>
<view class="flex items-center gap-2">
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionStats.one_time_amount) }}
</text>
<text class="text-11px text-[#999]">
{{ (commissionStats.one_time_percent / 10).toFixed(1) }}%
</text>
</view>
</view>
<view class="pt-2 border-t-1px border-[#f5f5f5]">
<view class="flex items-center justify-between">
<text class="text-13px font-600 text-[#212121]">总计</text>
<text class="text-16px font-700 text-[#ff6700]">
{{ formatAmount(commissionStats.total_amount) }}
</text>
</view>
<text class="text-11px text-[#999] mt-1 block text-right">
{{ commissionStats.total_count }}
</text>
<input
v-model="searchKeyword"
class="flex-1 h-40px bg-[var(--bg-container)] rounded-8px px-3 text-14px text-[var(--text-primary)]"
placeholder="搜索 ICCID / 虚拟号 / 订单号"
placeholder-class="text-[var(--text-quaternary)]"
@confirm="handleSearch"
>
<view
class="px-4 h-40px bg-brand text-white text-14px rounded-8px flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
</view>
</view>
<!-- 搜索框 -->
<view class="mb-3">
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<view class="flex items-center px-4 py-3 gap-2">
<i class="i-mdi-magnify text-20px text-[#999]" />
<input
v-model="searchKeyword"
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
placeholder="搜索 ICCID / 虚拟号 / 订单号"
@confirm="handleSearch"
>
<view
v-if="searchKeyword"
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
@click="clearSearch"
>
<i class="i-mdi-close-circle text-18px text-[#999]" />
</view>
<view
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
<view class="flex items-center gap-2 mb-3">
<view class="flex-1 bg-[var(--bg-container)] rounded-8px px-3 h-40px flex items-center" @click="showSourcePicker = true">
<text class="flex-1 text-14px text-[var(--text-primary)]">{{ selectedSourceName }}</text>
<text class="text-12px text-[var(--text-tertiary)]"></text>
</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="flex items-center gap-2 mb-3">
<!-- 佣金来源筛选 -->
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showSourcePicker = true">
<text class="flex-1 text-14px text-[#212121]">{{ selectedSourceName }}</text>
<i class="i-mdi-chevron-down text-18px text-[#999]" />
</view>
</view>
<!-- 佣金明细列表 -->
<view class="mb-3">
<view class="flex items-center justify-between mb-2 px-2">
<text class="text-14px font-600 text-[#212121]">
佣金明细
</text>
<text class="text-12px text-[#999]">
{{ total }} 条记录
</text>
<view class="flex items-center justify-between mb-3">
<text class="text-12px text-[var(--text-secondary)]"> {{ total }} 条记录</text>
</view>
<view v-if="loadingRecords && page === 1" class="pt-10 center">
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<view v-else-if="commissionRecords.length > 0" class="space-y-2">
<view
v-for="record in commissionRecords"
:key="record.id"
class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4"
class="bg-[var(--bg-container)] rounded-12px p-4"
>
<view class="flex items-center justify-between mb-2">
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<i class="i-mdi-cash-plus text-20px text-[#ff6700]" />
<text class="text-14px font-600 text-[#212121]">
<text class="text-14px font-600 text-[var(--text-primary)]">
{{ getSourceText(record.commission_source) }}
</text>
<view
:style="{
backgroundColor: getStatusColor(record.status).bg,
color: getStatusColor(record.status).text,
}"
class="px-2 py-0.5 rounded text-11px font-600"
>
{{ getStatusText(record.status) }}
</view>
</view>
<view
:style="{
backgroundColor: getStatusColor(record.status).bg,
color: getStatusColor(record.status).text,
}"
class="px-2 py-1 rounded-6px text-11px font-600"
>
{{ getStatusText(record.status) }}
</view>
</view>
<view class="flex items-center justify-between">
<text class="text-12px text-[#999]">
{{ formatTime(record.created_at) }}
</text>
<text class="text-18px font-700 text-[#ff6700]">
<text class="text-18px font-700 text-brand">
+{{ formatAmount(record.amount) }}
</text>
</view>
<view v-if="record.order_id" class="mt-2 pt-2 border-t-1px border-[#f5f5f5]">
<text class="text-11px text-[#999]">
订单ID: {{ record.order_id }}
</text>
<view class="flex items-center justify-between mt-2">
<text class="text-12px text-[var(--text-tertiary)]">{{ formatTime(record.created_at) }}</text>
<text v-if="record.order_id" class="text-11px text-[var(--text-tertiary)]">订单ID: {{ record.order_id }}</text>
</view>
</view>
<!-- 加载更多 -->
<view v-if="commissionRecords.length < total" class="py-3 center">
<text v-if="loadingRecords" class="text-12px text-[#999]">
加载中...
</text>
<text v-else class="text-12px text-[#ff6700]" @click="loadMore">
加载更多
</text>
<text v-if="loadingRecords" class="text-12px text-[var(--text-secondary)]">加载中...</text>
<text v-else class="text-12px text-brand" @click="loadMore">加载更多</text>
</view>
</view>
<view v-else class="pt-10 center flex-col">
<i class="i-mdi-cash-remove text-60px text-[#ddd] mb-3" />
<text class="text-14px text-[#999]">
暂无佣金记录
</text>
<view v-else class="pt-10 center">
<text class="text-14px text-[var(--text-secondary)]">暂无佣金记录</text>
</view>
</view>
<!-- 底部留白 -->
<view class="h-8" />
<view class="h-16" />
</view>
<!-- 佣金来源选择器 -->
<SimplePicker
v-model:show="showSourcePicker"
:options="sourceOptions"

View File

@@ -202,16 +202,6 @@ function getCardStatusText(status: number, statusName?: string) {
return map[status] ?? '未知'
}
// 获取运营商图标
function getCarrierIcon(carrierName: string) {
const map: Record<string, string> = {
'中国移动': 'i-mdi-cellphone',
'中国联通': 'i-mdi-signal-variant',
'中国电信': 'i-mdi-wifi',
}
return map[carrierName] || 'i-mdi-sim'
}
// 清空搜索
function clearSearch() {
searchKeyword.value = ''
@@ -257,204 +247,120 @@ function onCarrierConfirm(option: { label: string, value: any }) {
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 加载中 -->
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view v-if="loading" class="pt-20 center">
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<!-- 内容区域 -->
<view v-else class="px-4 pt-4">
<!-- 统计卡片 -->
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<i class="i-mdi-sim text-24px text-[#ff6700]" />
<text class="text-15px font-600 text-[#212121]">
企业授权IoT卡
</text>
</view>
<view class="text-right">
<text class="text-12px text-[#999] block">
总计
</text>
<text class="text-24px font-700 text-[#ff6700]">
{{ stats.total }}
</text>
</view>
</view>
<view v-else>
<view class="bg-brand px-4 py-3 mx-4 mt-4 rounded-12px">
<text class="text-11px text-white/60">企业授权IoT卡</text>
<text class="text-24px font-700 text-white block mt-1">{{ stats.total }}</text>
</view>
<!-- 搜索框 -->
<view class="mb-3">
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<view class="flex items-center px-4 py-3 gap-2">
<i class="i-mdi-magnify text-20px text-[#999]" />
<input
v-model="searchKeyword"
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
placeholder="搜索 ICCID / 虚拟号 / 手机号"
@confirm="handleSearch"
>
<view
v-if="searchKeyword"
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
@click="clearSearch"
>
<i class="i-mdi-close-circle text-18px text-[#999]" />
</view>
<view
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
</view>
</view>
</view>
<!-- 筛选条件 -->
<view class="flex items-center gap-2 mb-3">
<!-- 运营商筛选 -->
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showCarrierPicker = true">
<text class="flex-1 text-14px text-[#212121]">{{ selectedCarrierName }}</text>
<i class="i-mdi-chevron-down text-18px text-[#999]" />
</view>
</view>
<!-- 状态统计和筛选 -->
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="grid grid-cols-3 gap-3 mb-3">
<view class="text-center">
<text class="text-11px text-[#999] block mb-1">
正常
</text>
<text class="text-20px font-700 text-[#3ed268]">
{{ stats.active }}
</text>
</view>
<view class="text-center">
<text class="text-11px text-[#999] block mb-1">
停机
</text>
<text class="text-20px font-700 text-[#ff6b6b]">
{{ stats.inactive }}
</text>
</view>
<view class="text-center">
<text class="text-11px text-[#999] block mb-1">
已分销
</text>
<text class="text-20px font-700 text-[#ff6700]">
{{ stats.distributed }}
</text>
</view>
</view>
<!-- 状态筛选 -->
<view class="flex items-center gap-2">
<view
v-for="option in statusOptions"
:key="option.value"
:class="[
'flex-1 text-center py-2 rounded-8px text-13px transition-all duration-200',
statusFilter === option.value
? 'bg-[#ff6700] text-white font-600'
: 'bg-[#f5f5f5] text-[#666]',
]"
@click="handleStatusChange(option.value)"
<view class="px-4 mt-4">
<view class="flex items-center gap-2 mb-3">
<input
v-model="searchKeyword"
class="flex-1 h-40px bg-[var(--bg-container)] rounded-full px-4 text-14px text-[var(--text-primary)]"
placeholder="搜索 ICCID / 虚拟号 / 手机号"
placeholder-class="text-[var(--text-quaternary)]"
@confirm="handleSearch"
>
{{ option.label }}
<view
class="px-4 h-40px bg-brand text-white text-14px rounded-full flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
</view>
<view class="flex items-center gap-2 mb-3">
<view class="flex-1 bg-[var(--bg-container)] rounded-8px px-3 h-40px flex items-center" @click="showCarrierPicker = true">
<text class="flex-1 text-14px text-[var(--text-primary)]">{{ selectedCarrierName }}</text>
<text class="text-12px text-[var(--text-tertiary)]"></text>
</view>
</view>
</view>
<!-- 卡片列表 -->
<view v-if="filteredCards.length > 0" class="space-y-3 pb-4">
<view
v-for="card in filteredCards"
:key="card.id"
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"
@click="viewCardDetail(card)"
>
<!-- 顶部状态栏 -->
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<i
:class="[
getCarrierIcon(card.carrier_name),
'text-24px text-[#212121]',
]"
/>
<view>
<text class="text-16px font-700 text-[#212121] block">
{{ card.carrier_name || '-' }}
</text>
<text class="text-11px text-[#999]">
{{ getCardStatusText(card.status, card.status_name) }}
</text>
</view>
<view class="px-4">
<view class="bg-[var(--bg-container)] rounded-12px p-4 mb-3">
<view class="grid grid-cols-3 gap-3">
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">正常</text>
<text class="text-18px font-700 text-[#52c41a] mt-1">{{ stats.active }}</text>
</view>
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">停机</text>
<text class="text-18px font-700 text-[#ff4d4f] mt-1">{{ stats.inactive }}</text>
</view>
<view class="text-center">
<text class="text-11px text-[var(--text-tertiary)] block">已分销</text>
<text class="text-18px font-700 text-brand mt-1">{{ stats.distributed }}</text>
</view>
</view>
<view class="flex items-center gap-2 mt-4">
<view
v-for="option in statusOptions"
:key="option.value"
:class="[
'flex-1 text-center py-2 rounded-full text-13px transition-all',
statusFilter === option.value
? 'bg-brand text-white font-600'
: 'bg-[var(--bg-muted)] text-[var(--text-secondary)]',
]"
@click="handleStatusChange(option.value)"
>
{{ option.label }}
</view>
</view>
</view>
<view v-if="filteredCards.length > 0" class="space-y-3 pb-4">
<view
v-for="card in filteredCards"
:key="card.id"
class="bg-[var(--bg-container)] rounded-12px p-4"
@click="viewCardDetail(card)"
>
<view class="flex items-center justify-between">
<view>
<text class="text-15px font-600 text-[var(--text-primary)]">{{ card.carrier_name || '-' }}</text>
<text class="text-12px text-[var(--text-tertiary)] ml-2">{{ getCardStatusText(card.status, card.status_name) }}</text>
</view>
<view
:style="{
backgroundColor: getNetworkStatusColor(card.network_status).bg,
color: getNetworkStatusColor(card.network_status).text,
}"
class="px-3 py-1 rounded-8px text-12px font-600"
class="px-3 py-1 rounded-full text-12px font-600"
>
{{ getNetworkStatusText(card.network_status, card.network_status_name) }}
</view>
</view>
<text class="text-12px text-[#999]">
<view class="mt-3 text-13px text-[var(--text-secondary)]">
{{ card.package_name || '暂无套餐' }}
</text>
</view>
<!-- 卡片信息 -->
<view class="px-4 py-3">
<view class="space-y-2">
<!-- ICCID -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">ICCID</text>
<text class="flex-1 text-13px text-[#212121] text-right break-all">
{{ card.iccid || '-' }}
</text>
</view>
<!-- 手机号 (MSISDN) -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">手机号</text>
<text class="flex-1 text-13px font-600 text-[#212121] text-right">
{{ card.msisdn || '-' }}
</text>
</view>
<!-- 虚拟号 -->
<view v-if="card.virtual_no" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">虚拟号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ card.virtual_no }}
</text>
</view>
</view>
</view>
<!-- 使用情况 -->
<view class="px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="mt-3 flex items-center justify-between text-12px text-[var(--text-tertiary)]">
<text>{{ card.iccid || '-' }}</text>
<text>{{ card.msisdn || '-' }}</text>
</view>
<view v-if="card.virtual_no" class="mt-1 text-12px text-[var(--text-tertiary)]">
虚拟号: {{ card.virtual_no }}
</view>
<view class="mt-3 bg-[var(--bg-muted)] rounded-8px p-3">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#999]">
流量使用
</text>
<text class="text-12px font-600 text-[#212121]">
<text class="text-12px text-[var(--text-secondary)]">流量</text>
<text class="text-12px font-600 text-[var(--text-primary)]">
{{ card.data_usage || '0' }} / {{ card.data_total || '0' }}
</text>
</view>
<!-- 进度条 -->
<view class="h-6px bg-[#e0e0e0] rounded-full overflow-hidden">
<view class="h-6px bg-[var(--border-primary)] rounded-full overflow-hidden">
<view
:style="{
width: `${(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100}%`,
@@ -462,37 +368,28 @@ function onCarrierConfirm(option: { label: string, value: any }) {
:class="[
'h-full rounded-full transition-all duration-300',
(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9
? 'bg-[#ff6b6b]'
: 'bg-[#3ed268]',
? 'bg-[#ff4d4f]'
: 'bg-[#52c41a]',
]"
/>
</view>
</view>
<view class="flex items-center justify-between mt-2 text-11px text-[#999]">
<view class="mt-2 flex items-center justify-between text-11px text-[var(--text-tertiary)]">
<text>激活: {{ card.activate_time || '-' }}</text>
<text>到期: {{ card.expire_date || '-' }}</text>
</view>
</view>
</view>
<view v-else class="pt-16 center">
<text class="text-14px text-[var(--text-secondary)]">{{ searchKeyword ? '未找到匹配的卡' : '暂无卡数据' }}</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="pt-20 center flex-col">
<i class="i-mdi-sim-off text-80px text-[#ddd] mb-4" />
<text class="text-15px text-[#999] mb-2">
{{ searchKeyword ? '未找到匹配的卡片' : '暂无卡片数据' }}
</text>
<text class="text-12px text-[#ccc]">
{{ searchKeyword ? '请尝试其他搜索关键词' : '请联系管理员添加卡片' }}
</text>
</view>
<!-- 底部留白 -->
<view class="h-8" />
<view class="h-16" />
</view>
<!-- 运营商选择器 -->
<SimplePicker
v-model:show="showCarrierPicker"
:options="carrierOptions"

View File

@@ -4,48 +4,33 @@ import { getUserInfo } from '@/api/auth'
import { getEnterpriseDevices } from '@/api/enterprise'
import type { DeviceInfo } from '@/api/enterprise'
// 企业ID (用于调用企业接口)
const enterpriseId = ref<number>(0)
// 设备列表
const deviceList = ref<DeviceInfo[]>([])
// 加载状态
const loading = ref(false)
// 搜索关键词
const searchKeyword = ref('')
// 不再需要状态筛选
// 直接显示设备列表 (搜索由后端处理)
const filteredDevices = computed(() => {
return deviceList.value
})
// 统计数据
const stats = computed(() => {
const total = deviceList.value.length
return { total }
})
// 页面加载
onMounted(() => {
loadEnterpriseDevices()
})
// 加载企业设备列表
async function loadEnterpriseDevices() {
loading.value = true
try {
// 1. 获取企业ID (优先从本地存储获取)
if (!enterpriseId.value) {
const userInfo = uni.getStorageSync('user_info')
if (userInfo?.enterprise_id) {
enterpriseId.value = userInfo.enterprise_id
} else {
// 如果本地存储没有,则调用接口获取
const apiUserInfo = await getUserInfo()
if (!apiUserInfo.enterprise_id) {
throw new Error('未找到企业ID')
@@ -54,12 +39,10 @@ async function loadEnterpriseDevices() {
}
}
// 2. 获取企业设备列表 (支持分页和虚拟号搜索)
const params: any = {
page: 1,
page_size: 100, // 暂时获取更多数据,因为没有分页加载
page_size: 100,
}
// 添加虚拟号搜索支持
if (searchKeyword.value) {
params.virtual_no = searchKeyword.value
}
@@ -69,35 +52,21 @@ async function loadEnterpriseDevices() {
}
catch (error: any) {
console.error('加载企业设备列表失败:', error)
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message)
}
// API 请求错误已由拦截器统一处理
}
finally {
loading.value = false
}
}
// 查看设备详情
function viewDeviceDetail(device: any) {
// 跳转到资产详情页
uni.navigateTo({
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
})
}
// 获取设备型号图标
function getDeviceModelIcon(model: string) {
// 根据设备型号返回不同图标
if (model?.includes('WM')) {
return 'i-mdi-router-wireless'
}
return 'i-mdi-devices'
}
// 格式化授权时间
function formatAuthorizedTime(time: string) {
if (!time) return '-'
try {
@@ -111,178 +80,85 @@ function formatAuthorizedTime(time: string) {
}
}
// 清空搜索
function clearSearch() {
searchKeyword.value = ''
deviceList.value = []
loadEnterpriseDevices()
}
// 执行搜索
function handleSearch() {
deviceList.value = []
loadEnterpriseDevices()
}
// 刷新列表
function refreshList() {
deviceList.value = []
loadEnterpriseDevices()
}
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 加载中 -->
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view v-if="loading" class="pt-20 center">
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<!-- 内容区域 -->
<view v-else class="px-4 pt-4">
<!-- 搜索框 -->
<view class="mb-3">
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<view class="flex items-center px-4 py-3 gap-2">
<i class="i-mdi-magnify text-20px text-[#999]" />
<input
v-model="searchKeyword"
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
placeholder="搜索设备名称 / 设备型号 / 虚拟号"
@confirm="handleSearch"
>
<view
v-if="searchKeyword"
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
@click="clearSearch"
>
<i class="i-mdi-close-circle text-18px text-[#999]" />
</view>
<view
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
<view v-else>
<view class="bg-brand px-4 py-3 mx-4 mt-4 rounded-12px">
<text class="text-11px text-white/60">企业授权设备</text>
<text class="text-24px font-700 text-white block mt-1">{{ stats.total }}</text>
</view>
<view class="px-4 mt-4">
<view class="flex items-center gap-2 mb-3">
<input
v-model="searchKeyword"
class="flex-1 h-40px bg-[var(--bg-container)] rounded-full px-4 text-14px text-[var(--text-primary)]"
placeholder="搜索设备名称 / 设备型号 / 虚拟号"
placeholder-class="text-[var(--text-quaternary)]"
@confirm="handleSearch"
>
<view
class="px-4 h-40px bg-[var(--brand-primary)] text-white text-14px rounded-full flex items-center justify-center"
@click="handleSearch"
>
搜索
</view>
</view>
</view>
<!-- 统计卡片 -->
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<i class="i-mdi-devices text-24px text-[#ff6700]" />
<text class="text-15px font-600 text-[#212121]">
企业授权设备
</text>
</view>
<view class="text-right">
<text class="text-12px text-[#999] block">
总计
</text>
<text class="text-24px font-700 text-[#ff6700]">
{{ stats.total }}
</text>
</view>
</view>
</view>
<!-- 设备列表 -->
<view v-if="filteredDevices.length > 0" class="space-y-3 pb-4">
<view
v-for="device in filteredDevices"
:key="device.virtual_no"
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"
@click="viewDeviceDetail(device)"
>
<!-- 顶部状态栏 -->
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<i
:class="[
getDeviceModelIcon(device.device_model),
'text-24px text-[#212121]',
]"
/>
<view>
<text class="text-16px font-700 text-[#212121] block">
{{ device.device_name || '-' }}
</text>
<text class="text-11px text-[#999]">
{{ device.device_model || '-' }}
</text>
</view>
<view class="px-4">
<view v-if="filteredDevices.length > 0" class="space-y-3 pb-4">
<view
v-for="device in filteredDevices"
:key="device.virtual_no"
class="bg-[var(--bg-container)] rounded-12px p-4"
@click="viewDeviceDetail(device)"
>
<view class="flex items-center justify-between">
<view>
<text class="text-15px font-600 text-[var(--text-primary)]">{{ device.device_name || '-' }}</text>
<text class="text-12px text-[var(--text-tertiary)] ml-2">{{ device.device_model || '-' }}</text>
</view>
<view class="px-3 py-1 rounded-8px text-12px font-600 bg-[#e8f5e9] text-[#2e7d32]">
<view class="px-3 py-1 rounded-full text-12px font-600 bg-[#e8f5e9] text-[#52c41a]">
已授权
</view>
</view>
<text class="text-12px text-[#999]">
<view class="mt-3 text-12px text-[var(--text-tertiary)]">
绑定卡数: {{ device.card_count || 0 }}
</text>
</view>
<!-- 设备信息 -->
<view class="px-4 py-3">
<view class="space-y-2">
<!-- 虚拟号 -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">虚拟号</text>
<text class="flex-1 text-13px font-600 text-[#212121] text-right break-all">
{{ device.virtual_no || '-' }}
</text>
</view>
<!-- 设备型号 -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">设备型号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ device.device_model || '-' }}
</text>
</view>
<!-- 绑定卡数 -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">绑定卡数</text>
<text class="flex-1 text-13px font-600 text-[#ff6700] text-right">
{{ device.card_count || 0 }}
</text>
</view>
</view>
</view>
<!-- 授权时间 -->
<view class="px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="mt-3 text-13px text-[var(--text-secondary)]">
<text class="mr-4">虚拟号: {{ device.virtual_no || '-' }}</text>
</view>
<view class="mt-3 bg-[var(--bg-muted)] rounded-8px p-3">
<view class="flex items-center justify-between">
<text class="text-11px text-[#999]">
授权时间
</text>
<text class="text-12px font-600 text-[#212121]">
<text class="text-12px text-[var(--text-secondary)]">授权时间</text>
<text class="text-12px font-600 text-[var(--text-primary)]">
{{ formatAuthorizedTime(device.authorized_at) }}
</text>
</view>
</view>
</view>
</view>
<view v-else class="pt-16 center">
<text class="text-14px text-[var(--text-secondary)]">{{ searchKeyword ? '未找到匹配的设备' : '暂无设备数据' }}</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="pt-20 center flex-col">
<i class="i-mdi-devices-off text-80px text-[#ddd] mb-4" />
<text class="text-15px text-[#999] mb-2">
{{ searchKeyword ? '未找到匹配的设备' : '暂无设备数据' }}
</text>
<text class="text-12px text-[#ccc]">
{{ searchKeyword ? '请尝试其他搜索关键词' : '请联系管理员添加设备' }}
</text>
</view>
<!-- 底部留白 -->
<view class="h-8" />
<view class="h-16" />
</view>
</view>
</template>
@@ -297,21 +173,4 @@ function refreshList() {
.space-y-3 > view:not(:last-child) {
margin-bottom: 12px;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>

View File

@@ -1,301 +1,283 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getUserInfo, logout as logoutApi } from '@/api/auth'
import { clearToken } from '@/utils/auth'
import type { UserInfo } from '@/api/auth'
import { getCommissionSummary, getCommissionDailyStats } from '@/api/commission'
import type { CommissionSummary, DailyCommissionStats } from '@/api/commission'
import Skeleton from '@/components/Skeleton.vue'
import { ref, computed, onMounted } from 'vue'
import { getUserInfo, logout as logoutApi } from '@/api/auth'
import { clearToken } from '@/utils/auth'
import type { UserInfo } from '@/api/auth'
import { getFundSummary, getCommissionDailyStats } from '@/api/commission'
import type { FundSummary, DailyCommissionStats } from '@/api/commission'
// 用户信息
const userInfo = ref<UserInfo | null>(null)
// 佣金概览
const commissionSummary = ref<CommissionSummary | null>(null)
// 每日佣金统计 (最近7天)
const dailyStats = ref<DailyCommissionStats[]>([])
// 加载状态
const loading = ref(true)
const loadingCommission = ref(false)
// 用户信息
const userInfo = ref<UserInfo | null>(null)
// 佣金概览
const commissionSummary = ref<FundSummary | null>(null)
// 每日佣金统计 (最近7天)
const dailyStats = ref<DailyCommissionStats[]>([])
// 加载状态
const loading = ref(true)
const loadingCommission = ref(false)
// 是否为代理账号
const isAgent = computed(() => userInfo.value?.user_type === 3)
// 店铺ID
const shopId = ref<number>(0)
// 格式化金额(分转元,带千分号)
function formatAmount(amount: number) {
const yuan = (amount / 100).toFixed(2)
const parts = yuan.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `¥${parts.join('.')}`
}
// 是否为代理账号
const isAgent = computed(() => userInfo.value?.user_type === 3)
// 格式化日期 (MM-DD)
function formatDate(dateStr: string) {
const date = new Date(dateStr)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
}
// 格式化金额(分转元,带千分号)
function formatAmount(amount : number) {
const yuan = (amount / 100).toFixed(2)
const parts = yuan.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `¥${parts.join('.')}`
}
// 根据用户类型动态生成菜单
const menuItems = computed(() => {
const menus = [
{
title: '资产列表',
desc: userInfo.value?.user_type === 3 ? '代理资产列表' : '企业资产列表',
icon: 'i-mdi-view-list',
path: '/pages/agent-system/assets/index',
},
]
// 格式化日期 (MM-DD)
function formatDate(dateStr : string) {
const date = new Date(dateStr)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
}
// 企业端显示授权卡和授权设备 (仅 user_type === 4)
if (userInfo.value?.user_type === 4) {
menus.push(
// 根据用户类型动态生成菜单
const menuItems = computed(() => {
const menus = [
{
title: '授权卡列表',
desc: '企业授权IoT卡',
icon: 'i-mdi-sim',
path: '/pages/agent-system/enterprise-cards/index',
title: '资产列表',
desc: userInfo.value?.user_type === 3 ? '代理资产列表' : '企业资产列表',
icon: '/static/icons/资产列表.png',
path: '/pages/agent-system/assets/index',
},
{
title: '授权设备列表',
desc: '企业授权设备',
icon: 'i-mdi-devices',
path: '/pages/agent-system/enterprise-devices/index',
}
)
}
]
// 仅代理端显示佣金和提现
if (userInfo.value?.user_type === 3) {
menus.push(
{
title: '佣金中心',
desc: '收益统计',
icon: 'i-mdi-wallet',
path: '/pages/agent-system/commission-center/index',
},
{
title: '提现管理',
desc: '申请提现',
icon: 'i-mdi-cash',
path: '/pages/agent-system/withdraw/index',
}
)
}
return menus
})
// 加载佣金数据
async function loadCommissionData() {
if (!isAgent.value) return
loadingCommission.value = true
try {
// 1. 加载佣金概览
const summary = await getCommissionSummary()
commissionSummary.value = summary
// 2. 加载最近7天佣金统计
const stats = await getCommissionDailyStats({ days: 7 })
dailyStats.value = stats || []
}
catch (error) {
console.error('加载佣金数据失败:', error)
}
finally {
loadingCommission.value = false
}
}
// 加载首页数据
async function loadHomeData() {
loading.value = true
try {
// 加载用户信息
const user = await getUserInfo()
userInfo.value = user
console.log('首页数据加载成功:', { user })
// 如果是代理账号,加载佣金数据
if (user.user_type === 3) {
await loadCommissionData()
// 企业端显示授权卡和授权设备 (仅 user_type === 4)
if (userInfo.value?.user_type === 4) {
menus.push(
{
title: '授权卡列表',
desc: '企业授权IoT卡',
icon: '/static/icons/授权卡列表.png',
path: '/pages/agent-system/enterprise-cards/index',
},
{
title: '授权设备列表',
desc: '企业授权设备',
icon: '/static/icons/授权设备列表.png',
path: '/pages/agent-system/enterprise-devices/index',
}
)
}
}
catch (error: any) {
console.error('加载首页数据失败:', error)
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message)
// 仅代理端显示佣金和提现
if (userInfo.value?.user_type === 3) {
menus.push(
{
title: '佣金中心',
desc: '收益统计',
icon: '/static/icons/佣金中心.png',
path: '/pages/agent-system/commission-center/index',
},
{
title: '提现管理',
desc: '申请提现',
icon: '/static/icons/提现管理.png',
path: '/pages/agent-system/withdraw/index',
}
)
}
// API 请求错误已由拦截器统一处理
}
finally {
loading.value = false
}
}
// 跳转页面
function navigateTo(path: string) {
uni.navigateTo({ url: path })
}
// 退出登录
function logout() {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用登出接口
await logoutApi()
}
catch (error) {
console.error('登出接口调用失败:', error)
}
finally {
// 清除本地登录状态
clearToken()
uni.removeStorageSync('refresh_token')
uni.removeStorageSync('user_info')
// 跳转到登录页
uni.reLaunch({
url: '/pages/common/login/index',
})
}
}
},
return menus
})
}
onMounted(() => {
loadHomeData()
})
// 加载佣金数据
async function loadCommissionData() {
if (!isAgent.value || !shopId.value) return
loadingCommission.value = true
try {
// 1. 加载佣金概览 (新接口 /shops/fund-summary)
const summary = await getFundSummary()
commissionSummary.value = summary
// 2. 加载最近7天佣金统计
const stats = await getCommissionDailyStats(shopId.value, { days: 30 })
dailyStats.value = stats || []
}
catch (error) {
console.error('加载佣金数据失败:', error)
}
finally {
loadingCommission.value = false
}
}
// 加载首页数据
async function loadHomeData() {
loading.value = true
try {
// 加载用户信息
const user = await getUserInfo()
userInfo.value = user
// 设置店铺ID
if (user.shop_id) {
shopId.value = user.shop_id
}
console.log('首页数据加载成功:', { user })
// 如果是代理账号,加载佣金数据
if (user.user_type === 3) {
await loadCommissionData()
}
}
catch (error : any) {
console.error('加载首页数据失败:', error)
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
if (error instanceof Error && error.message && !error.statusCode) {
uni.$u.toast(error.message)
}
// API 请求错误已由拦截器统一处理
}
finally {
loading.value = false
}
}
// 跳转页面
function navigateTo(path : string) {
uni.navigateTo({ url: path })
}
// 退出登录
function logout() {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用登出接口
await logoutApi()
}
catch (error) {
console.error('登出接口调用失败:', error)
}
finally {
// 清除本地登录状态
clearToken()
uni.removeStorageSync('refresh_token')
uni.removeStorageSync('user_info')
// 跳转到登录页
uni.reLaunch({
url: '/pages/common/login/index',
})
}
}
},
})
}
onMounted(() => {
loadHomeData()
})
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 加载骨架屏 -->
<Skeleton v-if="loading" type="home" />
<!-- 加载 -->
<view v-if="loading" class="pt-20 center">
<text class="text-14px text-[#999]">加载中...</text>
</view>
<!-- 实际内容 -->
<view v-else>
<!-- 顶部用户信息 -->
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] px-6 pt-6 pb-5">
<view class="flex items-center justify-between">
<!-- 左侧用户信息 -->
<view class="flex-1">
<view class="flex items-center gap-2 mb-1">
<text class="text-22px font-700 text-white">
<!-- 顶部用户信息卡片 -->
<view class="px-4 pt-4">
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] px-4 py-4">
<view class="flex items-center">
<!-- 左侧头像 -->
<view class="w-56px h-56px rounded-full bg-[#f5f5f5] center mr-4"
@click="navigateTo('/pages/agent-system/user-info/index')">
<text class="text-14px text-[#666] font-700">
{{ userInfo?.user_type === 3 ? '代理' : '企业' }}
</text>
</view>
<!-- 右侧用户信息 -->
<view class="flex-1">
<text class="text-18px font-700 text-[#212121] block mb-1">
{{ userInfo?.username || '欢迎回来' }}
</text>
<view
v-if="userInfo?.user_type === 3"
class="px-2 py-0.5 bg-white/20 rounded-6px"
>
<text class="text-11px text-white font-600">代理</text>
</view>
<view
v-else-if="userInfo?.user_type === 4"
class="px-2 py-0.5 bg-white/20 rounded-6px"
>
<text class="text-11px text-white font-600">企业</text>
</view>
</view>
<view class="flex items-center gap-2">
<i class="i-mdi-phone text-14px text-white/70" />
<text class="text-13px text-white/90">
<text class="text-13px text-[#999]">
{{ userInfo?.phone || '-' }}
</text>
</view>
</view>
<!-- 右侧头像 -->
<view
class="w-56px h-56px rounded-full bg-white/20 backdrop-blur center border-2px border-white/30"
@click="navigateTo('/pages/agent-system/user-info/index')"
>
<text class="text-24px text-white font-700">
{{ userInfo?.username?.substring(0, 1) || 'U' }}
</text>
</view>
</view>
</view>
<!-- 代理端: 佣金概览 -->
<view v-if="isAgent && commissionSummary" class="px-4 pt-4">
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4 mb-3">
<view class="flex items-center justify-between mb-3">
<view>
<text class="text-13px text-white/70 block mb-1">
可提现佣金
</text>
<text class="text-32px font-700 text-white block">
{{ formatAmount(commissionSummary.available_commission) }}
</text>
</view>
<i class="i-mdi-cash-multiple text-48px text-white/20" />
</view>
<view class="grid grid-cols-3 gap-3 pt-3 border-t-1px border-white/20">
<text class="text-16px font-700 text-[#212121] block mb-3">可提现佣金</text>
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view class="grid grid-cols-4 gap-3">
<view class="text-center">
<text class="text-11px text-white/70 block mb-1">累计</text>
<text class="text-14px font-600 text-white">
<text class="text-11px text-[#999] block mb-1">累计</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.total_commission) }}
</text>
</view>
<view class="text-center">
<text class="text-11px text-white/70 block mb-1">冻结</text>
<text class="text-14px font-600 text-white">
<text class="text-11px text-[#999] block mb-1">冻结</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.frozen_commission) }}
</text>
</view>
<view class="text-center">
<text class="text-11px text-white/70 block mb-1">已提现</text>
<text class="text-14px font-600 text-white">
<text class="text-11px text-[#999] block mb-1">已提现</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.withdrawn_commission) }}
</text>
</view>
<view class="text-center">
<text class="text-11px text-[#999] block mb-1">可提现</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.available_commission) }}
</text>
</view>
</view>
</view>
<!-- 日佣金趋势 -->
<view v-if="dailyStats.length > 0" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<!-- 日佣金 -->
<view v-if="dailyStats.length > 0">
<view class="flex items-center justify-between mb-3">
<view class="flex items-center gap-2">
<i class="i-mdi-chart-line text-20px text-[#ff6700]" />
<text class="text-15px font-600 text-[#212121]">
近7日佣金
</text>
</view>
<text class="text-16px font-700 text-[#212121]">今日佣金</text>
<text class="text-13px text-[#999]" @click="navigateTo('/pages/agent-system/commission-center/index')">更多</text>
</view>
<!-- 简单的趋势条形图 -->
<view class="space-y-2">
<view
v-for="stat in dailyStats"
:key="stat.date"
class="flex items-center gap-2"
>
<text class="text-11px text-[#999] w-40px">
{{ formatDate(stat.date) }}
</text>
<view class="flex-1 h-20px bg-[#f5f5f5] rounded-full overflow-hidden relative">
<view
:style="{
width: `${Math.min(100, (stat.total_amount / Math.max(...dailyStats.map(s => s.total_amount))) * 100)}%`,
}"
class="h-full bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-full"
/>
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
<view class="grid grid-cols-3 gap-3">
<view class="text-center">
<text class="text-12px text-[#999] block mb-1">日期</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatDate(dailyStats[0].date) }}
</text>
</view>
<view class="text-center">
<text class="text-12px text-[#999] block mb-1">佣金金额</text>
<text class="text-14px font-600 text-[#212121]">
{{ formatAmount(dailyStats[0].total_amount) }}
</text>
</view>
<view class="text-center">
<text class="text-12px text-[#999] block mb-1">订单笔数</text>
<text class="text-14px font-600 text-[#212121]">
{{ dailyStats[0].total_count }}
</text>
</view>
<text class="text-11px font-600 text-[#212121] w-60px text-right">
{{ formatAmount(stat.total_amount) }}
</text>
</view>
</view>
</view>
@@ -303,23 +285,55 @@ onMounted(() => {
<!-- 快捷功能区 -->
<view class="px-4 pt-4">
<text class="text-13px text-[#999] block mb-3">快捷功能</text>
<text class="text-16px font-700 text-[#212121] block mb-3">快捷功能</text>
<!-- 功能列表 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-3">
<view
v-for="(item, index) in menuItems"
:key="item.path"
class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200"
:class="{ 'border-t-1px border-[#f5f5f5]': index > 0 }"
@click="navigateTo(item.path)"
>
<view class="w-40px h-40px rounded-12px bg-[#fff3e0] center mr-3">
<i :class="item.icon" class="text-20px text-[#ff6700]" />
</view>
<!-- 资产列表 -->
<view class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/assets/index')">
<image src="@/static/icons/资产列表.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">{{ item.title }}</text>
<text class="text-12px text-[#999] block mt-0.5">{{ item.desc }}</text>
<text class="text-15px text-[#212121] font-600 block">资产列表</text>
<text class="text-12px text-[#999] block mt-0.5">{{ userInfo?.user_type === 3 ? '代理资产列表' : '企业资产列表' }}</text>
</view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 企业端: 授权卡列表 -->
<view v-if="userInfo?.user_type === 4" class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/enterprise-cards/index')">
<image src="@/static/icons/授权卡列表.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">授权卡列表</text>
<text class="text-12px text-[#999] block mt-0.5">企业授权IoT卡</text>
</view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 企业端: 授权设备列表 -->
<view v-if="userInfo?.user_type === 4" class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/enterprise-devices/index')">
<image src="@/static/icons/授权设备列表.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">授权设备列表</text>
<text class="text-12px text-[#999] block mt-0.5">企业授权设备</text>
</view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 代理端: 佣金中心 -->
<view v-if="userInfo?.user_type === 3" class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/commission-center/index')">
<image src="@/static/icons/佣金中心.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">佣金中心</text>
<text class="text-12px text-[#999] block mt-0.5">收益统计</text>
</view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 代理端: 提现管理 -->
<view v-if="userInfo?.user_type === 3" class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/withdraw/index')">
<image src="@/static/icons/提现管理.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">提现管理</text>
<text class="text-12px text-[#999] block mt-0.5">申请提现</text>
</view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
@@ -328,35 +342,19 @@ onMounted(() => {
<!-- 其他操作区 -->
<view class="px-4 pt-4 pb-6">
<text class="text-13px text-[#999] block mb-3">设置</text>
<text class="text-16px font-700 text-[#212121] block mb-3">设置</text>
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<!-- 个人信息 -->
<view
class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200"
@click="navigateTo('/pages/agent-system/user-info/index')"
>
<i class="i-mdi-account-circle text-20px text-[#ff6700] mr-3" />
<text class="flex-1 text-15px text-[#212121]">个人信息</text>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 修改密码 -->
<view
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200"
@click="navigateTo('/pages/agent-system/change-password/index')"
>
<i class="i-mdi-lock-reset text-20px text-[#ff6700] mr-3" />
<view class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/change-password/index')">
<image src="@/static/icons/修改密码.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<text class="flex-1 text-15px text-[#212121]">修改密码</text>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
<!-- 退出登录 -->
<view
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200"
@click="logout"
>
<i class="i-mdi-logout text-20px text-[#999] mr-3" />
<view class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="logout">
<image src="@/static/icons/退出登录.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
<text class="flex-1 text-15px text-[#212121]">退出登录</text>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view>
@@ -367,26 +365,27 @@ onMounted(() => {
</template>
<style scoped>
.center {
display: flex;
align-items: center;
justify-content: center;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
.center {
display: flex;
align-items: center;
justify-content: center;
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.space-y-2>view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>

View File

@@ -608,7 +608,7 @@ const filteredPackages = computed(() => {
<view class="bg-white rounded-12px p-4 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
<view class="flex items-center justify-between mb-3">
<text class="text-16px font-600 text-[#212121]">佣金明细</text>
<button class="text-13px text-[#ff6700]">查看全部 </button>
<button class="text-13px text-[var(--brand-primary)]">查看全部 </button>
</view>
<view class="space-y-3">
@@ -660,9 +660,9 @@ const filteredPackages = computed(() => {
<view v-if="currentTab === 'my-packages'">
<!-- 总计卡片 (橙色渐变) -->
<view class="
bg-gradient-to-br from-[#ff6700] to-[#ff8533]
bg-gradient-to-br from-[var(--brand-primary)] to-[var(--brand-primary-light)]
rounded-16px p-6 mb-4
shadow-[0_8px_24px_rgba(255,103,0,0.2)]
shadow-brand-colored
">
<text class="text-13px text-white/70 block mb-2">套餐总计</text>
@@ -699,7 +699,7 @@ const filteredPackages = computed(() => {
<view class="
bg-white rounded-12px p-4
shadow-[0_2px_8px_rgba(0,0,0,0.04)]
border-2 border-[#ff6700]/20
border-2 border-[var(--brand-primary)]/20
">
<view class="flex items-start justify-between mb-3">
<text class="text-16px font-600 text-[#212121]">
@@ -833,7 +833,7 @@ const filteredPackages = computed(() => {
<i class="i-mdi-gift-outline w-48px h-48px text-[#e0e0e0] mb-2" />
<text class="text-13px text-[#9e9e9e] block mb-3">暂无加油包</text>
<button class="
bg-[#ff6700] text-white
bg-[var(--brand-primary)] text-white
px-5 py-2 rounded-8px
text-13px font-500
">
@@ -872,7 +872,7 @@ const filteredPackages = computed(() => {
<view class="flex gap-2 mb-3 overflow-x-auto pb-2">
<button
:class="{
'bg-[#fff7f0] text-[#ff6700]': filterStatus === null,
'bg-[var(--brand-primary-light)] text-[var(--brand-primary)]': filterStatus === null,
'text-[#9e9e9e]': filterStatus !== null
}"
class="px-4 py-1.5 rounded-full text-13px font-500 whitespace-nowrap"
@@ -1076,7 +1076,7 @@ const filteredPackages = computed(() => {
<span
v-for="tag in asset.tags"
:key="tag"
class="px-2 py-0.5 bg-[#fff7f0] text-[#ff6700] text-11px rounded-full"
class="px-2 py-0.5 bg-[var(--brand-primary-light)] text-[var(--brand-primary)] text-11px rounded-full"
>
{{ tag }}
</span>
@@ -1131,7 +1131,7 @@ const filteredPackages = computed(() => {
<text class="text-11px text-[#9e9e9e]">标签总数</text>
</view>
<view class="flex-1 text-center">
<text class="text-24px font-700 text-[#ff6700] block">
<text class="text-24px font-700 text-[var(--brand-primary)] block">
{{ tagList.reduce((sum, t) => sum + t.asset_count, 0) }}
</text>
<text class="text-11px text-[#9e9e9e]">已分配资产</text>
@@ -1179,7 +1179,7 @@ const filteredPackages = computed(() => {
已分配 {{ tag.asset_count }} 个资产
</text>
<button
class="text-12px text-[#ff6700] font-500"
class="text-12px text-[var(--brand-primary)] font-500"
@click="enterAssignMode(tag)"
>
分配
@@ -1192,11 +1192,11 @@ const filteredPackages = computed(() => {
<button class="
w-full
mt-4
bg-[#ff6700] text-white
bg-[var(--brand-primary)] text-white
py-3 rounded-12px
text-14px font-500
flex items-center justify-center gap-2
shadow-[0_2px_8px_rgba(255,103,0,0.2)]
shadow-brand-xs
">
<i class="i-mdi-plus w-18px h-18px" />
创建新标签
@@ -1255,7 +1255,7 @@ const filteredPackages = computed(() => {
<!-- 勾选框 -->
<view
:class="{
'bg-[#ff6700] border-[#ff6700]': asset.tags.includes(selectedTag.name),
'bg-[var(--brand-primary)] border-[var(--brand-primary)]': asset.tags.includes(selectedTag.name),
'bg-white border-[#e0e0e0]': !asset.tags.includes(selectedTag.name)
}"
class="
@@ -1296,10 +1296,10 @@ const filteredPackages = computed(() => {
<view class="fixed bottom-0 left-0 right-0 bg-white p-4 pb-safe border-t border-[#eeeeee]">
<button class="
w-full
bg-[#ff6700] text-white
bg-[var(--brand-primary)] text-white
py-3 rounded-8px
text-15px font-600
shadow-[0_2px_8px_rgba(255,103,0,0.2)]
shadow-brand-xs
">
保存分配
</button>
@@ -1418,10 +1418,10 @@ const filteredPackages = computed(() => {
<!-- 虚流量比例 -->
<view class="flex items-center justify-between p-3 bg-[#fff7f0] rounded-8px">
<view class="flex items-center gap-2">
<i class="i-mdi-calculator w-18px h-18px text-[#ff6700]" />
<i class="i-mdi-calculator w-18px h-18px text-[var(--brand-primary)]" />
<text class="text-13px text-[#757575]">虚流量比例</text>
</view>
<text class="text-16px font-700 text-[#ff6700]">
<text class="text-16px font-700 text-[var(--brand-primary)]">
1 : {{ (1 / selectedPackage.virtual_ratio).toFixed(1) }}
</text>
</view>
@@ -1488,10 +1488,10 @@ const filteredPackages = computed(() => {
续费套餐
</button>
<button class="
bg-[#ff6700] text-white
bg-[var(--brand-primary)] text-white
py-3 rounded-8px
text-14px font-500
shadow-[0_2px_8px_rgba(255,103,0,0.2)]
shadow-brand-xs
">
购买加油包
</button>

View File

@@ -125,7 +125,7 @@ onMounted(() => {
<text class="text-11px text-[#999] mt-1">总IoT卡</text>
</view>
<view class="text-center">
<text class="text-24px font-700 text-[#ff6700] block">{{ packageStats.hasPackage }}</text>
<text class="text-24px font-700 text-[var(--brand-primary)] block">{{ packageStats.hasPackage }}</text>
<text class="text-11px text-[#999] mt-1">已订购</text>
</view>
<view class="text-center">
@@ -153,7 +153,7 @@ onMounted(() => {
<view class="px-4 py-3 bg-[#f8fafc] border-b-1px border-[#f1f5f9]">
<view class="flex items-center justify-between">
<view class="flex items-center gap-2">
<i class="i-mdi-package-variant text-20px text-[#ff6700]" />
<i class="i-mdi-package-variant text-20px text-[var(--brand-primary)]" />
<text class="text-15px font-600 text-[#212121]">{{ group.name }}</text>
</view>
<text class="text-12px text-[#999]">{{ group.count }} </text>

View File

@@ -1,13 +1,22 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getCommissionSummary } from '@/api/commission'
import type { CommissionSummary } from '@/api/commission'
import { getFundSummary } from '@/api/commission'
import type { FundSummary } from '@/api/commission'
import { createWithdrawal, getWithdrawalRecords } from '@/api/withdrawal'
import type { WithdrawalRecord } from '@/api/withdrawal'
import SimplePicker from '@/components/SimplePicker.vue'
// 佣金概览
const commissionSummary = ref<CommissionSummary | null>(null)
const commissionSummary = ref<FundSummary | null>(null)
// 店铺ID (从登录信息获取)
const shopId = ref<number>(0)
// 从本地存储获取用户信息
function getUserInfo() {
const userInfo = uni.getStorageSync('user_info')
return userInfo || null
}
// 提现金额(元)
const withdrawAmount = ref('')
@@ -47,9 +56,9 @@ const statusOptions = [
// 获取当前选中的状态名称
const selectedStatusName = computed(() => {
if (statusFilter.value === undefined) return '提现状态'
if (statusFilter.value === undefined) return '全部状态'
const option = statusOptions.find(o => o.value === statusFilter.value)
return option?.label || '提现状态'
return option?.label || '全部状态'
})
// Tab选项
@@ -136,6 +145,12 @@ function formatTime(time: string) {
// 页面加载
onMounted(() => {
// 从登录信息获取 shop_id
const userInfo = getUserInfo()
if (userInfo?.shop_id) {
shopId.value = userInfo.shop_id
}
loadCommissionSummary()
loadWithdrawRecords()
})
@@ -144,8 +159,9 @@ onMounted(() => {
async function loadCommissionSummary() {
loading.value = true
try {
const summary = await getCommissionSummary()
const summary = await getFundSummary()
commissionSummary.value = summary
withdrawAmount.value = availableAmount.value.toFixed(2)
}
catch (error) {
console.error('加载佣金概览失败:', error)
@@ -157,6 +173,8 @@ async function loadCommissionSummary() {
// 加载提现记录
async function loadWithdrawRecords() {
if (!shopId.value) return
loadingRecords.value = true
try {
@@ -170,7 +188,7 @@ async function loadWithdrawRecords() {
params.status = statusFilter.value
}
const response = await getWithdrawalRecords(params)
const response = await getWithdrawalRecords(shopId.value, params)
if (page.value === 1) {
withdrawRecords.value = response.items || []
@@ -213,39 +231,48 @@ async function handleSubmit() {
return
}
if (!accountName.value.trim()) {
const name = accountName.value.trim()
if (!name) {
uni.$u.toast('请输入收款人姓名')
return
}
if (!/^[\u4e00-\u9fa5]{2,20}$|^[a-zA-Z]{2,20}$/.test(name)) {
uni.$u.toast('姓名格式错误')
return
}
if (!accountNumber.value.trim()) {
const account = accountNumber.value.trim()
if (!account) {
uni.$u.toast('请输入支付宝账号')
return
}
const phoneRegex = /^1[3-9]\d{9}$/
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!phoneRegex.test(account) && !emailRegex.test(account)) {
uni.$u.toast('支付宝账号格式错误')
return
}
submitting.value = true
try {
const result = await createWithdrawal({
account_name: accountName.value.trim(),
account_number: accountNumber.value.trim(),
amount: Math.round(amount * 100), // 元转分
const result = await createWithdrawal(shopId.value, {
account_name: name,
account_number: account,
amount: Math.round(amount * 100),
withdrawal_method: 'alipay',
})
uni.$u.toast('提现申请已提交')
// 清空表单
withdrawAmount.value = ''
accountName.value = ''
accountNumber.value = ''
// 刷新数据
await loadCommissionSummary()
page.value = 1
await loadWithdrawRecords()
// 切换到记录Tab
activeTab.value = 'records'
}
catch (error) {
@@ -284,35 +311,22 @@ function loadMore() {
</script>
<template>
<view class="bg-[#fafafa] min-h-screen pb-safe">
<!-- 可提现金额卡片 -->
<view class="px-4 pt-4 pb-3">
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4">
<view class="flex items-center justify-between">
<view>
<text class="text-13px text-white/70 block mb-1">
可提现金额
</text>
<text class="text-32px font-700 text-white block">
¥{{ formattedAvailableAmount }}
</text>
</view>
<i class="i-mdi-cash-multiple text-48px text-white/20" />
</view>
</view>
<view class="bg-[var(--bg-page)] min-h-screen pb-safe">
<view class="bg-brand rounded-12px px-4 py-3 mx-4">
<text class="text-11px text-white/60">可提现金额</text>
<text class="text-24px font-700 text-white block mt-1">¥{{ formattedAvailableAmount }}</text>
</view>
<!-- Tab切换 -->
<view class="px-4 mb-3">
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-1 flex">
<view class="px-4 mt-4">
<view class="flex rounded-full bg-[var(--bg-container)] p-1">
<view
v-for="tab in tabs"
:key="tab.key"
:class="[
'flex-1 text-center py-2 rounded-8px text-14px transition-all duration-200',
'flex-1 text-center py-2 rounded-full text-14px transition-all',
activeTab === tab.key
? 'bg-[#ff6700] text-white font-600'
: 'text-[#666]',
? 'bg-brand text-white font-600'
: 'text-[var(--text-secondary)]',
]"
@click="handleTabChange(tab.key)"
>
@@ -321,237 +335,136 @@ function loadMore() {
</view>
</view>
<!-- 申请提现 -->
<view v-if="activeTab === 'apply'" class="px-4">
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<!-- 提现金额 -->
<view class="mb-4">
<text class="text-13px text-[#666] block mb-2">提现金额</text>
<view class="flex items-center border-1px border-[#e0e0e0] rounded-8px px-3 py-2">
<text class="text-20px text-[#666] mr-1">¥</text>
<input
v-model="withdrawAmount"
type="digit"
class="flex-1 text-20px text-[#212121]"
placeholder="0.00"
>
</view>
<!-- 快捷金额 -->
<view class="flex items-center gap-2 mt-2">
<view
v-for="amount in [100, 500, 1000]"
:key="amount"
class="px-3 py-1 bg-[#f5f5f5] text-[#666] text-12px rounded-6px"
@click="setQuickAmount(amount)"
>
{{ amount }}
</view>
<view
class="px-3 py-1 bg-[#fff3e0] text-[#ff6700] text-12px rounded-6px font-600"
@click="setAllAmount"
>
全部提现
</view>
</view>
</view>
<!-- 收款人姓名 -->
<view class="mb-4 border-2px border-[#ff6700] rounded-16px p-4 bg-white shadow-[0_2px_12px_rgba(255,103,0,0.15)]">
<view class="flex items-center mb-3">
<text class="text-14px font-600 text-[#212121] mr-1">收款人姓名</text>
<text class="text-12px text-[#ff6700]">*</text>
</view>
<view v-if="activeTab === 'apply'" class="px-4 mt-4">
<view class="bg-[var(--bg-container)] rounded-12px p-4">
<text class="text-12px text-[var(--text-tertiary)]">提现金额</text>
<view class="flex items-center mt-1">
<text class="text-24px text-[var(--text-secondary)] mr-1">¥</text>
<input
v-model="accountName"
class="w-full text-16px text-[#212121] font-500"
placeholder="请输入支付宝实名认证的真实姓名"
placeholder-style="color: #ccc"
v-model="withdrawAmount"
type="digit"
class="flex-1 text-24px text-[var(--text-primary)]"
placeholder="0.00"
placeholder-class="text-[var(--text-quaternary)]"
>
</view>
<!-- 支付宝账号 -->
<view class="mb-4 border-2px border-[#ff6700] rounded-16px p-4 bg-white shadow-[0_2px_12px_rgba(255,103,0,0.15)]">
<view class="flex items-center mb-3">
<text class="text-14px font-600 text-[#212121] mr-1">支付宝账号</text>
<text class="text-12px text-[#ff6700]">*</text>
</view>
<input
v-model="accountNumber"
class="w-full text-16px text-[#212121] font-500"
placeholder="请输入支付宝账号/手机号/邮箱"
placeholder-style="color: #ccc"
<view class="flex items-center gap-2 mt-3">
<view
v-for="amount in [100, 500, 1000]"
:key="amount"
:class="[
'px-4 py-1.5 text-13px rounded-full transition-all',
withdrawAmount == amount.toString()
? 'bg-brand text-white font-600'
: 'bg-[var(--bg-muted)] text-[var(--text-secondary)]',
]"
@click="setQuickAmount(amount)"
>
</view>
<!-- 费用说明 -->
<view class="bg-gradient-to-r from-[#f5f5f5] to-[#fafafa] rounded-12px p-4 mb-4 border-1px border-[#e0e0e0]">
<view class="flex items-center justify-between mb-3">
<text class="text-13px text-[#666]">提现金额</text>
<text class="text-16px font-600 text-[#212121]">¥{{ formattedWithdrawAmount }}</text>
{{ amount }}
</view>
<view class="pt-3 border-t-2px border-[#ff6700]">
<view class="flex items-center justify-between">
<view>
<text class="text-14px font-600 text-[#212121] block">实际到账</text>
<text class="text-11px text-[#52c41a] block mt-0.5">
无手续费,全额到账
</text>
</view>
<text class="text-24px font-700 text-[#52c41a]">¥{{ actualAmount }}</text>
</view>
<view
:class="[
'px-4 py-1.5 text-13px rounded-full transition-all font-500',
withdrawAmount == availableAmount.toFixed(2)
? 'bg-brand text-white'
: 'bg-[var(--bg-muted)] text-[var(--text-secondary)]',
]"
@click="setAllAmount"
>
全部
</view>
</view>
</view>
<!-- 提交按钮 -->
<view
:class="[
'w-full py-3 rounded-8px text-center text-15px font-600 transition-all duration-200',
submitting ? 'bg-[#ccc] text-white' : 'bg-[#ff6700] text-white',
]"
@click="handleSubmit"
<view class="bg-[var(--bg-container)] rounded-12px p-4 mt-3">
<text class="text-12px text-[var(--text-tertiary)]">收款人姓名</text>
<input
v-model="accountName"
class="w-full text-16px text-[var(--text-primary)] mt-1"
placeholder="请输入姓名"
placeholder-class="text-[var(--text-quaternary)]"
>
{{ submitting ? '提交中...' : '提交申请' }}
</view>
</view>
<!-- 温馨提示 -->
<view class="mt-3 p-4 bg-[#fff3e0] rounded-12px">
<view class="flex items-start gap-2">
<i class="i-mdi-information text-16px text-[#e65100] mt-0.5" />
<view class="flex-1">
<text class="text-12px font-600 text-[#e65100] block mb-2">温馨提示</text>
<text class="text-12px text-[#666] block leading-relaxed">
1. 目前仅支持支付宝提现<br>
2. 无手续费,全额到账<br>
3. 收款人姓名需与支付宝实名一致<br>
4. 提现申请提交后将在1-3个工作日内处理
</text>
</view>
</view>
<view class="bg-[var(--bg-container)] rounded-12px p-4 mt-3">
<text class="text-12px text-[var(--text-tertiary)]">支付宝账号</text>
<input
v-model="accountNumber"
class="w-full text-16px text-[var(--text-primary)] mt-1"
placeholder="请输入账号"
placeholder-class="text-[var(--text-quaternary)]"
>
</view>
<view class="bg-[var(--bg-container)] rounded-12px p-4 mt-3">
<view class="flex items-center justify-between">
<text class="text-14px text-[var(--text-secondary)]">提现金额</text>
<text class="text-20px font-600 text-brand">¥{{ formattedWithdrawAmount }}</text>
</view>
</view>
<view
:class="[
'w-full py-4 rounded-12px text-center text-16px font-600 mt-4',
submitting ? 'bg-[var(--bg-muted)] text-[var(--text-quaternary)]' : 'bg-brand text-white',
]"
@click="handleSubmit"
>
{{ submitting ? '提交中...' : '确认提现' }}
</view>
</view>
<!-- 提现记录 -->
<view v-else class="px-4">
<!-- 筛选条件 -->
<view v-else class="px-4 mt-4">
<view class="flex items-center gap-2 mb-3">
<!-- 状态筛选 -->
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showStatusPicker = true">
<text class="flex-1 text-14px text-[#212121]">{{ selectedStatusName }}</text>
<i class="i-mdi-chevron-down text-18px text-[#999]" />
<view class="flex-1 bg-[var(--bg-container)] rounded-12px px-4 py-3" @click="showStatusPicker = true">
<text class="text-14px text-[var(--text-primary)]">{{ selectedStatusName }}</text>
</view>
</view>
<!-- 记录列表 -->
<view v-if="loadingRecords && page === 1" class="pt-10 center">
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
<text class="text-14px text-[var(--text-secondary)]">加载中...</text>
</view>
<view v-else-if="withdrawRecords.length > 0" class="space-y-2 mb-3">
<view v-else-if="withdrawRecords.length > 0" class="space-y-3">
<view
v-for="record in withdrawRecords"
:key="record.id"
class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4"
class="bg-[var(--bg-container)] rounded-12px p-4"
>
<view class="flex items-center justify-between mb-3">
<view class="flex items-center gap-2">
<i class="i-mdi-bank-transfer text-20px text-[#ff6700]" />
<text class="text-14px font-600 text-[#212121]">
{{ record.withdrawal_no }}
</text>
</view>
<view class="flex items-center justify-between">
<text class="text-13px text-[var(--text-tertiary)]">{{ formatTime(record.created_at) }}</text>
<view
:style="{
backgroundColor: getStatusColor(record.status).bg,
color: getStatusColor(record.status).text,
}"
class="px-2 py-1 rounded-6px text-11px font-600"
class="px-3 py-1 rounded-full text-12px font-600"
>
{{ getStatusText(record.status, record.status_name) }}
</view>
</view>
<view class="space-y-2">
<view class="flex items-center justify-between mb-2">
<text class="text-13px text-[#666]">提现金额</text>
<text class="text-18px font-700 text-[#212121]">
{{ formatAmount(record.amount) }}
</text>
</view>
<view v-if="record.fee > 0" class="flex items-center justify-between">
<text class="text-12px text-[#999]">手续费</text>
<text class="text-13px text-[#ff6700]">
-{{ formatAmount(record.fee) }}
</text>
</view>
<view class="flex items-center justify-between pt-2 border-t-1px border-[#f5f5f5]">
<view>
<text class="text-12px text-[#999] block">实际到账</text>
<text v-if="record.fee === 0" class="text-10px text-[#52c41a] block mt-0.5">
无手续费
</text>
</view>
<text class="text-18px font-700 text-[#52c41a]">
{{ formatAmount(record.actual_amount) }}
</text>
</view>
<view class="pt-2 border-t-1px border-[#f5f5f5]">
<view class="flex items-center justify-between mb-1">
<text class="text-11px text-[#999]">收款人</text>
<text class="text-12px text-[#212121]">
{{ record.account_name }}
</text>
</view>
<view class="flex items-center justify-between mb-1">
<text class="text-11px text-[#999]">支付宝账号</text>
<text class="text-12px text-[#212121]">
{{ record.account_number }}
</text>
</view>
<view class="flex items-center justify-between">
<text class="text-11px text-[#999]">申请时间</text>
<text class="text-11px text-[#999]">
{{ formatTime(record.created_at) }}
</text>
</view>
</view>
<!-- 拒绝原因 -->
<view v-if="record.status === 3 && record.reject_reason" class="pt-2 border-t-1px border-[#f5f5f5]">
<text class="text-11px text-[#c62828] block">
拒绝原因: {{ record.reject_reason }}
</text>
</view>
<view class="flex items-center justify-between mt-3">
<text class="text-14px text-[var(--text-secondary)]">{{ record.account_name }}</text>
<text class="text-20px font-700 text-brand">-{{ formatAmount(record.amount) }}</text>
</view>
<view v-if="record.status === 3 && record.reject_reason" class="mt-2 pt-2 border-t border-[var(--border-secondary)]">
<text class="text-12px text-[var(--color-error)]">{{ record.reject_reason }}</text>
</view>
</view>
<!-- 加载更多 -->
<view v-if="withdrawRecords.length < total" class="py-3 center">
<text v-if="loadingRecords" class="text-12px text-[#999]">
加载中...
</text>
<text v-else class="text-12px text-[#ff6700]" @click="loadMore">
加载更多
</text>
<view v-if="withdrawRecords.length < total" class="py-4 center">
<text v-if="loadingRecords" class="text-14px text-[var(--text-secondary)]">加载中...</text>
<text v-else class="text-14px text-[var(--brand-primary)]" @click="loadMore">加载更多</text>
</view>
</view>
<view v-else class="pt-10 center flex-col">
<i class="i-mdi-file-document-outline text-60px text-[#ddd] mb-3" />
<text class="text-14px text-[#999]">
暂无提现记录
</text>
<view v-else class="pt-16 center">
<text class="text-14px text-[var(--text-secondary)]">暂无记录</text>
</view>
</view>
<!-- 底部留白 -->
<view class="h-8" />
<view class="h-16" />
<!-- 状态选择器 -->
<SimplePicker
v-model:show="showStatusPicker"
:options="statusOptions"

View File

@@ -202,7 +202,7 @@ onLoad((options: any) => {
width: 100%;
height: 100%;
background:
linear-gradient(90deg, transparent 0%, rgba(255, 103, 0, 0.02) 50%, transparent 100%);
linear-gradient(90deg, transparent 0%, rgba(22, 119, 255, 0.02) 50%, transparent 100%);
animation: scanline 8s linear infinite;
}
@@ -215,7 +215,7 @@ onLoad((options: any) => {
height: 800rpx;
top: -300rpx;
right: -200rpx;
background: radial-gradient(circle, rgba(255, 103, 0, 0.08), transparent 60%);
background: radial-gradient(circle, rgba(22, 119, 255, 0.08), transparent 60%);
border-radius: 50%;
animation: pulse1 10s infinite ease-in-out;
}
@@ -225,7 +225,7 @@ onLoad((options: any) => {
height: 600rpx;
bottom: -200rpx;
left: -150rpx;
background: radial-gradient(circle, rgba(255, 103, 0, 0.06), transparent 60%);
background: radial-gradient(circle, rgba(22, 119, 255, 0.06), transparent 60%);
border-radius: 50%;
animation: pulse2 12s infinite ease-in-out;
}
@@ -235,7 +235,7 @@ onLoad((options: any) => {
height: 400rpx;
top: 50%;
right: -100rpx;
background: radial-gradient(circle, rgba(255, 103, 0, 0.04), transparent 60%);
background: radial-gradient(circle, rgba(22, 119, 255, 0.04), transparent 60%);
border-radius: 50%;
animation: pulse3 14s infinite ease-in-out;
}
@@ -384,8 +384,8 @@ onLoad((options: any) => {
&.focused {
background: #ffffff;
border-color: #d4af37;
box-shadow: 0 0 0 4rpx rgba(212, 175, 55, 0.08);
border-color: var(--brand-primary);
box-shadow: 0 0 0 4rpx rgba(22, 119, 255, 0.08);
transform: translateY(-2rpx);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -5,8 +5,10 @@
:root,
page {
/* === 品牌色 Brand Colors - 只保留3个核心颜色 === */
--brand-primary: #d4af37; /* 主色 - 色 */
/* === 品牌色 Brand Colors === */
--brand-primary: #1677ff; /* 主色 - 色 */
--brand-primary-light: #4096ff; /* 主色浅 */
--brand-primary-dark: #0958d9; /* 主色深 */
--brand-secondary: #1e293b; /* 辅色 - 深灰/黑色 */
--brand-accent: #ffffff; /* 强调 - 白色 */
@@ -17,7 +19,7 @@ page {
--color-warning-bg: #fffbe6; /* 警告背景 */
--color-error: #ff4d4f; /* 错误 - Red */
--color-error-bg: #fff2f0; /* 错误背景 */
--color-info: #1890ff; /* 信息 - Blue */
--color-info: #1677ff; /* 信息 - Blue(同主色) */
--color-info-bg: #e6f7ff; /* 信息背景 */
/* === 文字色阶 Text Colors === */
@@ -44,7 +46,7 @@ page {
--shadow-md: 0 2px 8px 0 rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
--shadow-xl: 0 8px 24px 0 rgba(0, 0, 0, 0.15);
--shadow-brand: 0 4px 12px 0 rgba(255, 103, 0, 0.2);
--shadow-brand: 0 4px 12px 0 rgba(22, 119, 255, 0.2);
/* === 圆角 Radius === */
--radius-xs: 4px;
@@ -98,11 +100,13 @@ page {
/* === 暗色主题 Dark Theme === */
.theme-dark {
--brand-primary: #ff8533; /* 暗色下色更柔和 */
--brand-primary: #4b9cff; /* 暗色下色更柔和 */
--brand-primary-light: #69b1ff;
--brand-primary-dark: #1668dc;
--color-success: #73d13d;
--color-warning: #ffc53d;
--color-error: #ff7875;
--color-info: #40a9ff;
--color-info: #4b9cff;
--text-primary: #ffffff;
--text-secondary: #b0b8c1;
@@ -150,6 +154,14 @@ page {
.bg-muted { background-color: var(--bg-muted); }
.bg-brand { background-color: var(--brand-primary); }
.bg-brand-light { background-color: var(--brand-primary-light); }
.bg-brand-dark { background-color: var(--brand-primary-dark); }
.bg-gradient-brand { background: linear-gradient(135deg, var(--brand-primary), var(--brand-primary-light)); }
.shadow-brand-colored { box-shadow: 0 4px 16px rgba(22, 119, 255, 0.3); }
.shadow-brand-sm { box-shadow: 0 2px 12px rgba(22, 119, 255, 0.15); }
.shadow-brand-xs { box-shadow: 0 2px 8px rgba(22, 119, 255, 0.2); }
.border-brand { border-color: var(--brand-primary); }
.border-primary { border-color: var(--border-primary); }
.border-secondary { border-color: var(--border-secondary); }
@@ -229,7 +241,7 @@ page {
.input:focus {
border-color: var(--brand-primary);
outline: none;
box-shadow: 0 0 0 3px rgba(255, 103, 0, 0.1);
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.1);
}
.input::placeholder {

View File

@@ -47,6 +47,14 @@ export function post<T = any>(url: string, config?: HttpRequestConfig): Promise<
return request({ ...config, url, method: 'POST' });
}
export function put<T = any>(url: string, config?: HttpRequestConfig): Promise<T> {
return request({ ...config, url, method: 'PUT' });
}
export function patch<T = any>(url: string, config?: HttpRequestConfig): Promise<T> {
return request({ ...config, url, method: 'PATCH' });
}
export function upload<T = any>(url: string, config?: HttpRequestConfig): Promise<T> {
return request({ ...config, url, method: 'UPLOAD' });
}