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 ## 🌈 Color Palette
### Brand Colors (Xiaomi-Inspired) ### Brand Colors (Blue Theme)
```scss ```scss
// Primary Brand Colors // Primary Brand Colors
--brand-orange: #ff6700; // Primary CTA, hover states --brand-primary: #1677ff; // Primary CTA, hover states
--brand-orange-light: #ff8533; // Lighter variant --brand-primary-light: #4096ff; // Lighter variant
--brand-orange-dark: #e65a00; // Pressed states --brand-primary-dark: #0958d9; // Pressed states
--brand-gray: #333333; // Headings, important text --brand-secondary: #1e293b; // Dark Gray (headings)
--brand-gray-light: #757575; // Secondary text --brand-accent: #ffffff; // White (emphasis)
``` ```
### Semantic Colors (Light Mode) ### Semantic Colors (Light Mode)
```scss ```scss
// Primary Theme Colors // Primary Theme Colors
--theme-primary: #ff6700; // Orange (Trust + Energy) --theme-primary: #1677ff; // Blue (Trust + Energy)
--theme-secondary: #ff8533; // Lighter Orange --theme-secondary: #1e293b; // Dark Gray
--theme-accent: #333333; // Dark Gray --theme-accent: #ffffff; // White
// Status Colors // Status Colors
--theme-success: #3ed268; // Green (Success states) --theme-success: #52c41a; // Green (Success states)
--theme-warning: #e68815; // Amber (Warnings) --theme-warning: #faad14; // Amber (Warnings)
--theme-error: #d31638; // Red (Errors) --theme-error: #ff4d4f; // Red (Errors)
--theme-info: #1677ff; // Info (same as primary)
// Text Hierarchy // Text Hierarchy
--theme-main-color: #4c484f; // Primary text (slate-900 equivalent) --theme-main-color: #212121; // Primary text
--theme-content-color: #6b6569; // Body text (slate-700) --theme-content-color: #666666; // Body text
--theme-tips-color: #8a8486; // Secondary text (slate-500) --theme-tips-color: #999999; // Secondary text
--theme-light-color: #bd9e84; // Muted text (beige) --theme-light-color: #cccccc; // Muted text
--theme-disabled-color: #dce0eb; // Disabled states --theme-disabled-color: #f5f5f5; // Disabled states
// Backgrounds // Backgrounds
--theme-bg-color: #ffffff; // Main background --theme-bg-color: #fafafa; // Main background
--theme-bg-color-secondary: #faf8f7; // Card backgrounds --theme-bg-color-secondary: #ffffff; // Card backgrounds
--theme-bg-gradient-start: #c5dfdf; --theme-bg-gradient-start: #1677ff;
--theme-bg-gradient-end: #bd9e84; --theme-bg-gradient-end: #4096ff;
// Borders // Borders
--theme-border-color: #c5dfdf; // Borders, dividers --theme-border-color: #e8e8e8; // Borders, dividers
``` ```
### Dark Mode Colors ### Dark Mode Colors
```scss ```scss
.theme-dark { .theme-dark {
--theme-primary: #3c9cff; // Blue (better for dark mode) --brand-primary: #4b9cff; // Blue (lighter for dark mode)
--theme-success: #5ac725; --brand-primary-light: #69b1ff;
--theme-warning: #f9ae3d; --brand-primary-dark: #1668dc;
--theme-success: #73d13d;
--theme-warning: #ffc53d;
--theme-error: #f56c6c; --theme-error: #f56c6c;
// Text (High Contrast Required) // 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 * 资产模块 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 { export interface OperationParams {
id: string id: string
reason?: string reason?: string
} }
/**
* 操作参数 (新版,使用 identifier)
*/
export interface AssetOperationParams {
identifier: string
reason?: string
}
/** /**
* 资产解析 (智能识别) * 资产解析 (智能识别)
* GET /api/admin/assets/resolve/:identifier * 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) { export function getAssetDetail(type: AssetType, id: string) {
return get<AssetDetailResponse>(`/api/admin/assets/${type}/${id}`) 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) { export function getCardDetail(id: string) {
return get<CardInfo>(`/api/admin/cards/${id}`) 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) { export function getDeviceDetail(virtualNo: string) {
return get<DeviceInfo>(`/api/admin/devices/${id}`) 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) { export function getAssetRealtimeStatus(identifier: string) {
return get<AssetRealtimeStatus>(`/api/admin/assets/${assetType}/${id}/realtime-status`) 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<{ return get<{
items: AssetPackage[] items: AssetPackage[]
page: number page: number
page_size: number page_size: number
total: 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) { export function getCurrentPackage(identifier: string) {
return get<AssetPackage>(`/api/admin/assets/${assetType}/${id}/current-package`) 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<{ return get<{
list: HistoryRecord[] list: HistoryRecord[]
total: number total: number
page: number page: number
pageSize: 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) { export function stopCard(data: OperationParams) {
return post<void>(`/api/admin/cards/${data.id}/stop`, { 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) { export function startCard(data: OperationParams) {
return post<void>(`/api/admin/cards/${data.id}/start`, { 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) { export function stopDevice(virtualNo: string, reason?: string) {
return post<void>(`/api/admin/devices/${data.id}/stop`, { return post<void>(`/api/admin/devices/${virtualNo}/stop`, {
data: { reason: data.reason }, data: { reason },
}) })
} }
/** /**
* 启用设备 * 启用设备
* POST /api/admin/devices/:id/start * POST /api/admin/devices/:virtual_no/start
* @param virtualNo 设备虚拟号
*/ */
export function startDevice(data: OperationParams) { export function startDevice(virtualNo: string, reason?: string) {
return post<void>(`/api/admin/devices/${data.id}/start`, { return post<void>(`/api/admin/devices/${virtualNo}/start`, {
data: { reason: data.reason }, 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) { export function stopDeviceAsset(deviceId: number) {
return post<DeviceStopResponse>(`/api/admin/assets/device/${deviceId}/stop`) 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) { export function startDeviceAsset(deviceId: number) {
return post<void>(`/api/admin/assets/device/${deviceId}/start`) return post<void>(`/api/admin/assets/device/${deviceId}/start`)
@@ -383,7 +400,7 @@ export function startDeviceAsset(deviceId: number) {
/** /**
* 单卡停机 (通过ICCID) * 单卡停机 (通过ICCID)
* POST /api/admin/assets/card/:iccid/stop * @deprecated 使用 stopAsset 代替,路由: POST /api/admin/assets/:identifier/stop
*/ */
export function stopCardByIccid(iccid: string) { export function stopCardByIccid(iccid: string) {
return post<void>(`/api/admin/assets/card/${iccid}/stop`) return post<void>(`/api/admin/assets/card/${iccid}/stop`)
@@ -391,8 +408,135 @@ export function stopCardByIccid(iccid: string) {
/** /**
* 单卡复机 (通过ICCID) * 单卡复机 (通过ICCID)
* POST /api/admin/assets/card/:iccid/start * @deprecated 使用 startAsset 代替,路由: POST /api/admin/assets/:identifier/start
*/ */
export function startCardByIccid(iccid: string) { export function startCardByIccid(iccid: string) {
return post<void>(`/api/admin/assets/card/${iccid}/start`) 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' 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 { export interface CommissionSummary {
shop_id: number shop_id: number
@@ -67,24 +89,47 @@ export interface CommissionRecordsParams {
} }
/** /**
* 佣金统计查询参数 * 佣金统计查询参数 (新接口shop_id 在路径中)
*/ */
export interface CommissionStatsParams { export interface CommissionStatsParams {
shop_id?: number
start_time?: string start_time?: string
end_time?: string end_time?: string
} }
/** /**
* 每日佣金统计查询参数 * 每日佣金统计查询参数 (新接口shop_id 在路径中)
*/ */
export interface DailyCommissionStatsParams { export interface DailyCommissionStatsParams {
shop_id?: number
start_date?: string // YYYY-MM-DD start_date?: string // YYYY-MM-DD
end_date?: string // YYYY-MM-DD end_date?: string // YYYY-MM-DD
days?: number // 查询天数默认30最大365 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 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() { 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) { export function getCommissionRecords(shopId: number, params?: CommissionRecordsParams) {
return get<ListResponse<CommissionRecord>>('/api/admin/my/commission-records', { params }) 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) { export function getCommissionStats(shopId: number, params?: CommissionStatsParams) {
return get<CommissionStats>('/api/admin/my/commission-stats', { params }) 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) { export function getCommissionDailyStats(shopId: number, params?: DailyCommissionStatsParams) {
return get<DailyCommissionStats[]>('/api/admin/my/commission-daily-stats', { params }) 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) { export function createWithdrawal(shopId: number, data: WithdrawalCreateParams) {
return post<WithdrawalCreateResponse>('/api/admin/my/withdrawal-requests', { data }) 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) { export function getWithdrawalRecords(shopId: number, params?: WithdrawalListParams) {
return get<ListResponse<WithdrawalRecord>>('/api/admin/my/withdrawal-requests', { params }) 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 { .simple-picker-item-active .simple-picker-item-text {
color: var(--brand-color, #ff6700); color: var(--brand-primary, #1677ff);
font-weight: 600; font-weight: 600;
} }
</style> </style>

View File

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

View File

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

View File

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

View File

@@ -1,99 +1,99 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { getAssetWallet } from '@/api/wallet' import { getAssetWallet } from '@/api/wallet'
import type { AssetWalletInfo, AssetType } from '@/api/wallet' import type { AssetWalletInfo, AssetType } from '@/api/wallet'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
// 资产类型和ID (从URL参数获取) // 资产类型和ID (从URL参数获取)
const assetType = ref<AssetType>('card') const assetType = ref<AssetType>('card')
const assetId = ref<number>(0) 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) { function formatAmount(amount : number) {
return `¥${(amount / 100).toFixed(2)}` 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
} }
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) function getStatusColor(status : number) {
walletInfo.value = wallet 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() { function formatTime(time : string) {
uni.navigateBack() 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> </script>
<template> <template>
@@ -106,7 +106,8 @@ function goBack() {
<!-- 钱包信息 --> <!-- 钱包信息 -->
<view v-else-if="walletInfo" class="px-4 pt-4"> <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 class="flex items-center justify-between mb-3">
<view> <view>
<text class="text-13px text-white/70 block mb-1"> <text class="text-13px text-white/70 block mb-1">
@@ -116,13 +117,10 @@ function goBack() {
{{ formatAmount(walletInfo.available_balance) }} {{ formatAmount(walletInfo.available_balance) }}
</text> </text>
</view> </view>
<view <view :style="{
:style="{
backgroundColor: getStatusColor(walletInfo.status).bg, backgroundColor: getStatusColor(walletInfo.status).bg,
color: getStatusColor(walletInfo.status).text, 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) }} {{ getStatusText(walletInfo.status, walletInfo.status_text) }}
</view> </view>
</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="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"> <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 class="text-15px font-600 text-[#212121]">
钱包详情 钱包详情
</text> </text>
@@ -227,26 +225,27 @@ function goBack() {
</template> </template>
<style scoped> <style scoped>
.center { .center {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
} }
to {
transform: rotate(360deg);
}
}
.animate-spin { .space-y-2>view:not(:last-child) {
animation: spin 1s linear infinite; margin-bottom: 8px;
} }
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style> </style>

View File

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

View File

@@ -53,7 +53,7 @@ const isFormValid = computed(() => {
const { old_password, new_password, confirm_password } = formData.value const { old_password, new_password, confirm_password } = formData.value
return ( return (
old_password.length > 0 old_password.length > 0
&& new_password.length >= 6 && new_password.length >= 8
&& new_password === confirm_password && new_password === confirm_password
) )
}) })
@@ -74,9 +74,9 @@ async function submitChange() {
return return
} }
if (formData.value.new_password.length < 6) { if (formData.value.new_password.length < 8) {
uni.showToast({ uni.showToast({
title: '新密码至少6位', title: '新密码至少8位',
icon: 'none', icon: 'none',
}) })
return return
@@ -149,256 +149,126 @@ function clearForm() {
</script> </script>
<template> <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="px-4 pt-5">
<!-- 提示信息 --> <view class="bg-white rounded-12px p-4 mb-3">
<view class="bg-[#e3f2fd] rounded-12px p-4 mb-4 flex items-start gap-3"> <text class="text-12px text-[#999] block mb-2">当前密码</text>
<i class="i-mdi-information text-20px text-[#1976d2] mt-0.5" /> <view class="flex items-center">
<view class="flex-1"> <input
<text class="text-13px text-[#1976d2] block mb-1 font-600"> v-model="formData.old_password"
密码安全提示 :password="!passwordVisible.old"
</text> class="flex-1 text-16px text-[#212121]"
<text class="text-12px text-[#1976d2] leading-relaxed"> placeholder="请输入当前密码"
为了您的账号安全,建议密码长度至少8位,包含大小写字母数字和特殊字符 placeholder-class="text-[#ccc]"
</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 <image
v-for="i in 3" :src="passwordVisible.old ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
:key="i" class="w-20px h-20px"
:class="[ mode="aspectFit"
'flex-1 h-2px rounded-full', @click="togglePasswordVisible('old')"
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"
/> />
<text class="text-15px font-600 text-white"> </view>
{{ submitting ? '修改中...' : '确认修改' }} </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> </text>
</view> </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="bg-white rounded-12px p-4 mb-3">
<view <text class="text-12px text-[#999] block mb-2">确认密码</text>
class=" <view class="flex items-center">
py-3 rounded-12px center <input
bg-white shadow-[0_2px_12px_rgba(0,0,0,0.06)] v-model="formData.confirm_password"
transition-all duration-200 :password="!passwordVisible.confirm"
active:bg-[#f5f5f5] class="flex-1 text-16px text-[#212121]"
" placeholder="请再次输入新密码"
@click="clearForm" placeholder-class="text-[#ccc]"
> >
<i class="i-mdi-refresh text-18px text-[#666] mr-2" /> <image
<text class="text-15px font-600 text-[#666]"> :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> </text>
</view> </view>
</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 class="h-8" />
</view> </view>
</view> </view>
@@ -423,6 +293,10 @@ function clearForm() {
line-height: 1.6; line-height: 1.6;
} }
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);

View File

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

View File

@@ -202,16 +202,6 @@ function getCardStatusText(status: number, statusName?: string) {
return map[status] ?? '未知' 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() { function clearSearch() {
searchKeyword.value = '' searchKeyword.value = ''
@@ -257,204 +247,120 @@ function onCarrierConfirm(option: { label: string, value: any }) {
</script> </script>
<template> <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"> <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>
<!-- 内容区域 --> <view v-else>
<view v-else class="px-4 pt-4"> <view class="bg-brand px-4 py-3 mx-4 mt-4 rounded-12px">
<!-- 统计卡片 --> <text class="text-11px text-white/60">企业授权IoT卡</text>
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3"> <text class="text-24px font-700 text-white block mt-1">{{ stats.total }}</text>
<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> </view>
<!-- 搜索框 --> <view class="px-4 mt-4">
<view class="mb-3"> <view class="flex items-center gap-2 mb-3">
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"> <input
<view class="flex items-center px-4 py-3 gap-2"> v-model="searchKeyword"
<i class="i-mdi-magnify text-20px text-[#999]" /> class="flex-1 h-40px bg-[var(--bg-container)] rounded-full px-4 text-14px text-[var(--text-primary)]"
<input placeholder="搜索 ICCID / 虚拟号 / 手机号"
v-model="searchKeyword" placeholder-class="text-[var(--text-quaternary)]"
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]" @confirm="handleSearch"
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)"
> >
{{ 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>
</view> </view>
<!-- 卡片列表 --> <view class="px-4">
<view v-if="filteredCards.length > 0" class="space-y-3 pb-4"> <view class="bg-[var(--bg-container)] rounded-12px p-4 mb-3">
<view <view class="grid grid-cols-3 gap-3">
v-for="card in filteredCards" <view class="text-center">
:key="card.id" <text class="text-11px text-[var(--text-tertiary)] block">正常</text>
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden" <text class="text-18px font-700 text-[#52c41a] mt-1">{{ stats.active }}</text>
@click="viewCardDetail(card)" </view>
> <view class="text-center">
<!-- 顶部状态栏 --> <text class="text-11px text-[var(--text-tertiary)] block">停机</text>
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]"> <text class="text-18px font-700 text-[#ff4d4f] mt-1">{{ stats.inactive }}</text>
<view class="flex items-center justify-between mb-2"> </view>
<view class="flex items-center gap-2"> <view class="text-center">
<i <text class="text-11px text-[var(--text-tertiary)] block">已分销</text>
:class="[ <text class="text-18px font-700 text-brand mt-1">{{ stats.distributed }}</text>
getCarrierIcon(card.carrier_name), </view>
'text-24px text-[#212121]', </view>
]"
/> <view class="flex items-center gap-2 mt-4">
<view> <view
<text class="text-16px font-700 text-[#212121] block"> v-for="option in statusOptions"
{{ card.carrier_name || '-' }} :key="option.value"
</text> :class="[
<text class="text-11px text-[#999]"> 'flex-1 text-center py-2 rounded-full text-13px transition-all',
{{ getCardStatusText(card.status, card.status_name) }} statusFilter === option.value
</text> ? 'bg-brand text-white font-600'
</view> : '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>
<view <view
:style="{ :style="{
backgroundColor: getNetworkStatusColor(card.network_status).bg, backgroundColor: getNetworkStatusColor(card.network_status).bg,
color: getNetworkStatusColor(card.network_status).text, 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) }} {{ getNetworkStatusText(card.network_status, card.network_status_name) }}
</view> </view>
</view> </view>
<text class="text-12px text-[#999]"> <view class="mt-3 text-13px text-[var(--text-secondary)]">
{{ card.package_name || '暂无套餐' }} {{ 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>
<!-- 使用情况 --> <view class="mt-3 flex items-center justify-between text-12px text-[var(--text-tertiary)]">
<view class="px-4 pb-4"> <text>{{ card.iccid || '-' }}</text>
<view class="bg-[#f5f5f5] rounded-12px p-3"> <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"> <view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#999]"> <text class="text-12px text-[var(--text-secondary)]">流量</text>
流量使用 <text class="text-12px font-600 text-[var(--text-primary)]">
</text>
<text class="text-12px font-600 text-[#212121]">
{{ card.data_usage || '0' }} / {{ card.data_total || '0' }} {{ card.data_usage || '0' }} / {{ card.data_total || '0' }}
</text> </text>
</view> </view>
<view class="h-6px bg-[var(--border-primary)] rounded-full overflow-hidden">
<!-- 进度条 -->
<view class="h-6px bg-[#e0e0e0] rounded-full overflow-hidden">
<view <view
:style="{ :style="{
width: `${(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100}%`, width: `${(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100}%`,
@@ -462,37 +368,28 @@ function onCarrierConfirm(option: { label: string, value: any }) {
:class="[ :class="[
'h-full rounded-full transition-all duration-300', 'h-full rounded-full transition-all duration-300',
(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9 (parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9
? 'bg-[#ff6b6b]' ? 'bg-[#ff4d4f]'
: 'bg-[#3ed268]', : 'bg-[#52c41a]',
]" ]"
/> />
</view> </view>
</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.activate_time || '-' }}</text>
<text>到期: {{ card.expire_date || '-' }}</text> <text>到期: {{ card.expire_date || '-' }}</text>
</view> </view>
</view> </view>
</view> </view>
<view v-else class="pt-16 center">
<text class="text-14px text-[var(--text-secondary)]">{{ searchKeyword ? '未找到匹配的卡' : '暂无卡数据' }}</text>
</view>
</view> </view>
<!-- 空状态 --> <view class="h-16" />
<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> </view>
<!-- 运营商选择器 -->
<SimplePicker <SimplePicker
v-model:show="showCarrierPicker" v-model:show="showCarrierPicker"
:options="carrierOptions" :options="carrierOptions"

View File

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

View File

@@ -1,301 +1,283 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { getUserInfo, logout as logoutApi } from '@/api/auth' import { getUserInfo, logout as logoutApi } from '@/api/auth'
import { clearToken } from '@/utils/auth' import { clearToken } from '@/utils/auth'
import type { UserInfo } from '@/api/auth' import type { UserInfo } from '@/api/auth'
import { getCommissionSummary, getCommissionDailyStats } from '@/api/commission' import { getFundSummary, getCommissionDailyStats } from '@/api/commission'
import type { CommissionSummary, DailyCommissionStats } from '@/api/commission' import type { FundSummary, DailyCommissionStats } from '@/api/commission'
import Skeleton from '@/components/Skeleton.vue'
// 用户信息 // 用户信息
const userInfo = ref<UserInfo | null>(null) const userInfo = ref<UserInfo | null>(null)
// 佣金概览 // 佣金概览
const commissionSummary = ref<CommissionSummary | null>(null) const commissionSummary = ref<FundSummary | null>(null)
// 每日佣金统计 (最近7天) // 每日佣金统计 (最近7天)
const dailyStats = ref<DailyCommissionStats[]>([]) const dailyStats = ref<DailyCommissionStats[]>([])
// 加载状态 // 加载状态
const loading = ref(true) const loading = ref(true)
const loadingCommission = ref(false) const loadingCommission = ref(false)
// 是否为代理账号 // 店铺ID
const isAgent = computed(() => userInfo.value?.user_type === 3) const shopId = ref<number>(0)
// 格式化金额(分转元,带千分号) // 是否为代理账号
function formatAmount(amount: number) { const isAgent = computed(() => userInfo.value?.user_type === 3)
const yuan = (amount / 100).toFixed(2)
const parts = yuan.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `¥${parts.join('.')}`
}
// 格式化日期 (MM-DD) // 格式化金额(分转元,带千分号)
function formatDate(dateStr: string) { function formatAmount(amount : number) {
const date = new Date(dateStr) const yuan = (amount / 100).toFixed(2)
const month = String(date.getMonth() + 1).padStart(2, '0') const parts = yuan.split('.')
const day = String(date.getDate()).padStart(2, '0') parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `${month}-${day}` return `¥${parts.join('.')}`
} }
// 根据用户类型动态生成菜单 // 格式化日期 (MM-DD)
const menuItems = computed(() => { function formatDate(dateStr : string) {
const menus = [ const date = new Date(dateStr)
{ const month = String(date.getMonth() + 1).padStart(2, '0')
title: '资产列表', const day = String(date.getDate()).padStart(2, '0')
desc: userInfo.value?.user_type === 3 ? '代理资产列表' : '企业资产列表', return `${month}-${day}`
icon: 'i-mdi-view-list', }
path: '/pages/agent-system/assets/index',
},
]
// 企业端显示授权卡和授权设备 (仅 user_type === 4) // 根据用户类型动态生成菜单
if (userInfo.value?.user_type === 4) { const menuItems = computed(() => {
menus.push( const menus = [
{ {
title: '授权卡列表', title: '资产列表',
desc: '企业授权IoT卡', desc: userInfo.value?.user_type === 3 ? '代理资产列表' : '企业资产列表',
icon: 'i-mdi-sim', icon: '/static/icons/资产列表.png',
path: '/pages/agent-system/enterprise-cards/index', path: '/pages/agent-system/assets/index',
}, },
{ ]
title: '授权设备列表',
desc: '企业授权设备',
icon: 'i-mdi-devices',
path: '/pages/agent-system/enterprise-devices/index',
}
)
}
// 仅代理端显示佣金和提现 // 企业端显示授权卡和授权设备 (仅 user_type === 4)
if (userInfo.value?.user_type === 3) { if (userInfo.value?.user_type === 4) {
menus.push( menus.push(
{ {
title: '佣金中心', title: '授权卡列表',
desc: '收益统计', desc: '企业授权IoT卡',
icon: 'i-mdi-wallet', icon: '/static/icons/授权卡列表.png',
path: '/pages/agent-system/commission-center/index', path: '/pages/agent-system/enterprise-cards/index',
}, },
{ {
title: '提现管理', title: '授权设备列表',
desc: '申请提现', desc: '企业授权设备',
icon: 'i-mdi-cash', icon: '/static/icons/授权设备列表.png',
path: '/pages/agent-system/withdraw/index', path: '/pages/agent-system/enterprise-devices/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()
} }
}
catch (error: any) { // 仅代理端显示佣金和提现
console.error('加载首页数据失败:', error) if (userInfo.value?.user_type === 3) {
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示 menus.push(
if (error instanceof Error && error.message && !error.statusCode) { {
uni.$u.toast(error.message) 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
}
}
// 跳转页面 return menus
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() 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> </script>
<template> <template>
<view class="bg-[#fafafa] min-h-screen pb-safe"> <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 v-else>
<!-- 顶部用户信息 --> <!-- 顶部用户信息卡片 -->
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] px-6 pt-6 pb-5"> <view class="px-4 pt-4">
<view class="flex items-center justify-between"> <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="flex-1"> <!-- 左侧头像 -->
<view class="flex items-center gap-2 mb-1"> <view class="w-56px h-56px rounded-full bg-[#f5f5f5] center mr-4"
<text class="text-22px font-700 text-white"> @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 || '欢迎回来' }} {{ userInfo?.username || '欢迎回来' }}
</text> </text>
<view <text class="text-13px text-[#999]">
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">
{{ userInfo?.phone || '-' }} {{ userInfo?.phone || '-' }}
</text> </text>
</view> </view>
</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> </view>
<!-- 代理端: 佣金概览 --> <!-- 代理端: 佣金概览 -->
<view v-if="isAgent && commissionSummary" class="px-4 pt-4"> <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"> <text class="text-16px font-700 text-[#212121] block mb-3">可提现佣金</text>
<view class="flex items-center justify-between mb-3"> <view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
<view> <view class="grid grid-cols-4 gap-3">
<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">
<view class="text-center"> <view class="text-center">
<text class="text-11px text-white/70 block mb-1">累计</text> <text class="text-11px text-[#999] block mb-1">累计</text>
<text class="text-14px font-600 text-white"> <text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.total_commission) }} {{ formatAmount(commissionSummary.total_commission) }}
</text> </text>
</view> </view>
<view class="text-center"> <view class="text-center">
<text class="text-11px text-white/70 block mb-1">冻结</text> <text class="text-11px text-[#999] block mb-1">冻结</text>
<text class="text-14px font-600 text-white"> <text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.frozen_commission) }} {{ formatAmount(commissionSummary.frozen_commission) }}
</text> </text>
</view> </view>
<view class="text-center"> <view class="text-center">
<text class="text-11px text-white/70 block mb-1">已提现</text> <text class="text-11px text-[#999] block mb-1">已提现</text>
<text class="text-14px font-600 text-white"> <text class="text-14px font-600 text-[#212121]">
{{ formatAmount(commissionSummary.withdrawn_commission) }} {{ formatAmount(commissionSummary.withdrawn_commission) }}
</text> </text>
</view> </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> </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 justify-between mb-3">
<view class="flex items-center gap-2"> <text class="text-16px font-700 text-[#212121]">今日佣金</text>
<i class="i-mdi-chart-line text-20px text-[#ff6700]" /> <text class="text-13px text-[#999]" @click="navigateTo('/pages/agent-system/commission-center/index')">更多</text>
<text class="text-15px font-600 text-[#212121]">
近7日佣金
</text>
</view>
</view> </view>
<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="space-y-2"> <view class="text-center">
<view <text class="text-12px text-[#999] block mb-1">日期</text>
v-for="stat in dailyStats" <text class="text-14px font-600 text-[#212121]">
:key="stat.date" {{ formatDate(dailyStats[0].date) }}
class="flex items-center gap-2" </text>
> </view>
<text class="text-11px text-[#999] w-40px"> <view class="text-center">
{{ formatDate(stat.date) }} <text class="text-12px text-[#999] block mb-1">佣金金额</text>
</text> <text class="text-14px font-600 text-[#212121]">
<view class="flex-1 h-20px bg-[#f5f5f5] rounded-full overflow-hidden relative"> {{ formatAmount(dailyStats[0].total_amount) }}
<view </text>
:style="{ </view>
width: `${Math.min(100, (stat.total_amount / Math.max(...dailyStats.map(s => s.total_amount))) * 100)}%`, <view class="text-center">
}" <text class="text-12px text-[#999] block mb-1">订单笔数</text>
class="h-full bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-full" <text class="text-14px font-600 text-[#212121]">
/> {{ dailyStats[0].total_count }}
</text>
</view> </view>
<text class="text-11px font-600 text-[#212121] w-60px text-right">
{{ formatAmount(stat.total_amount) }}
</text>
</view> </view>
</view> </view>
</view> </view>
@@ -303,23 +285,55 @@ onMounted(() => {
<!-- 快捷功能区 --> <!-- 快捷功能区 -->
<view class="px-4 pt-4"> <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 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" <view class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/assets/index')">
:key="item.path" <image src="@/static/icons/资产列表.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
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-1"> <view class="flex-1">
<text class="text-15px text-[#212121] font-600 block">{{ item.title }}</text> <text class="text-15px text-[#212121] font-600 block">资产列表</text>
<text class="text-12px text-[#999] block mt-0.5">{{ item.desc }}</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> </view>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" /> <i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view> </view>
@@ -328,35 +342,19 @@ onMounted(() => {
<!-- 其他操作区 --> <!-- 其他操作区 -->
<view class="px-4 pt-4 pb-6"> <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="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 <view class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200" @click="navigateTo('/pages/agent-system/change-password/index')">
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" <image src="@/static/icons/修改密码.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
@click="navigateTo('/pages/agent-system/change-password/index')"
>
<i class="i-mdi-lock-reset text-20px text-[#ff6700] mr-3" />
<text class="flex-1 text-15px text-[#212121]">修改密码</text> <text class="flex-1 text-15px text-[#212121]">修改密码</text>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" /> <i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view> </view>
<!-- 退出登录 --> <!-- 退出登录 -->
<view <view class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" @click="logout">
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200" <image src="@/static/icons/退出登录.png" class="w-40px h-40px rounded-12px mr-3" mode="aspectFit" />
@click="logout"
>
<i class="i-mdi-logout text-20px text-[#999] mr-3" />
<text class="flex-1 text-15px text-[#212121]">退出登录</text> <text class="flex-1 text-15px text-[#212121]">退出登录</text>
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" /> <i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
</view> </view>
@@ -367,26 +365,27 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.center { .center {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
}
.space-y-2 > view:not(:last-child) {
margin-bottom: 8px;
}
@keyframes spin {
from {
transform: rotate(0deg);
} }
to {
transform: rotate(360deg);
}
}
.animate-spin { .space-y-2>view:not(:last-child) {
animation: spin 1s linear infinite; margin-bottom: 8px;
} }
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style> </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="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"> <view class="flex items-center justify-between mb-3">
<text class="text-16px font-600 text-[#212121]">佣金明细</text> <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>
<view class="space-y-3"> <view class="space-y-3">
@@ -660,9 +660,9 @@ const filteredPackages = computed(() => {
<view v-if="currentTab === 'my-packages'"> <view v-if="currentTab === 'my-packages'">
<!-- 总计卡片 (橙色渐变) --> <!-- 总计卡片 (橙色渐变) -->
<view class=" <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 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> <text class="text-13px text-white/70 block mb-2">套餐总计</text>
@@ -699,7 +699,7 @@ const filteredPackages = computed(() => {
<view class=" <view class="
bg-white rounded-12px p-4 bg-white rounded-12px p-4
shadow-[0_2px_8px_rgba(0,0,0,0.04)] 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"> <view class="flex items-start justify-between mb-3">
<text class="text-16px font-600 text-[#212121]"> <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" /> <i class="i-mdi-gift-outline w-48px h-48px text-[#e0e0e0] mb-2" />
<text class="text-13px text-[#9e9e9e] block mb-3">暂无加油包</text> <text class="text-13px text-[#9e9e9e] block mb-3">暂无加油包</text>
<button class=" <button class="
bg-[#ff6700] text-white bg-[var(--brand-primary)] text-white
px-5 py-2 rounded-8px px-5 py-2 rounded-8px
text-13px font-500 text-13px font-500
"> ">
@@ -872,7 +872,7 @@ const filteredPackages = computed(() => {
<view class="flex gap-2 mb-3 overflow-x-auto pb-2"> <view class="flex gap-2 mb-3 overflow-x-auto pb-2">
<button <button
:class="{ :class="{
'bg-[#fff7f0] text-[#ff6700]': filterStatus === null, 'bg-[var(--brand-primary-light)] text-[var(--brand-primary)]': filterStatus === null,
'text-[#9e9e9e]': filterStatus !== null 'text-[#9e9e9e]': filterStatus !== null
}" }"
class="px-4 py-1.5 rounded-full text-13px font-500 whitespace-nowrap" class="px-4 py-1.5 rounded-full text-13px font-500 whitespace-nowrap"
@@ -1076,7 +1076,7 @@ const filteredPackages = computed(() => {
<span <span
v-for="tag in asset.tags" v-for="tag in asset.tags"
:key="tag" :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 }} {{ tag }}
</span> </span>
@@ -1131,7 +1131,7 @@ const filteredPackages = computed(() => {
<text class="text-11px text-[#9e9e9e]">标签总数</text> <text class="text-11px text-[#9e9e9e]">标签总数</text>
</view> </view>
<view class="flex-1 text-center"> <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) }} {{ tagList.reduce((sum, t) => sum + t.asset_count, 0) }}
</text> </text>
<text class="text-11px text-[#9e9e9e]">已分配资产</text> <text class="text-11px text-[#9e9e9e]">已分配资产</text>
@@ -1179,7 +1179,7 @@ const filteredPackages = computed(() => {
已分配 {{ tag.asset_count }} 个资产 已分配 {{ tag.asset_count }} 个资产
</text> </text>
<button <button
class="text-12px text-[#ff6700] font-500" class="text-12px text-[var(--brand-primary)] font-500"
@click="enterAssignMode(tag)" @click="enterAssignMode(tag)"
> >
分配 分配
@@ -1192,11 +1192,11 @@ const filteredPackages = computed(() => {
<button class=" <button class="
w-full w-full
mt-4 mt-4
bg-[#ff6700] text-white bg-[var(--brand-primary)] text-white
py-3 rounded-12px py-3 rounded-12px
text-14px font-500 text-14px font-500
flex items-center justify-center gap-2 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" /> <i class="i-mdi-plus w-18px h-18px" />
创建新标签 创建新标签
@@ -1255,7 +1255,7 @@ const filteredPackages = computed(() => {
<!-- 勾选框 --> <!-- 勾选框 -->
<view <view
:class="{ :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) 'bg-white border-[#e0e0e0]': !asset.tags.includes(selectedTag.name)
}" }"
class=" 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]"> <view class="fixed bottom-0 left-0 right-0 bg-white p-4 pb-safe border-t border-[#eeeeee]">
<button class=" <button class="
w-full w-full
bg-[#ff6700] text-white bg-[var(--brand-primary)] text-white
py-3 rounded-8px py-3 rounded-8px
text-15px font-600 text-15px font-600
shadow-[0_2px_8px_rgba(255,103,0,0.2)] shadow-brand-xs
"> ">
保存分配 保存分配
</button> </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 justify-between p-3 bg-[#fff7f0] rounded-8px">
<view class="flex items-center gap-2"> <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> <text class="text-13px text-[#757575]">虚流量比例</text>
</view> </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) }} 1 : {{ (1 / selectedPackage.virtual_ratio).toFixed(1) }}
</text> </text>
</view> </view>
@@ -1488,10 +1488,10 @@ const filteredPackages = computed(() => {
续费套餐 续费套餐
</button> </button>
<button class=" <button class="
bg-[#ff6700] text-white bg-[var(--brand-primary)] text-white
py-3 rounded-8px py-3 rounded-8px
text-14px font-500 text-14px font-500
shadow-[0_2px_8px_rgba(255,103,0,0.2)] shadow-brand-xs
"> ">
购买加油包 购买加油包
</button> </button>

View File

@@ -125,7 +125,7 @@ onMounted(() => {
<text class="text-11px text-[#999] mt-1">总IoT卡</text> <text class="text-11px text-[#999] mt-1">总IoT卡</text>
</view> </view>
<view class="text-center"> <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> <text class="text-11px text-[#999] mt-1">已订购</text>
</view> </view>
<view class="text-center"> <view class="text-center">
@@ -153,7 +153,7 @@ onMounted(() => {
<view class="px-4 py-3 bg-[#f8fafc] border-b-1px border-[#f1f5f9]"> <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 justify-between">
<view class="flex items-center gap-2"> <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> <text class="text-15px font-600 text-[#212121]">{{ group.name }}</text>
</view> </view>
<text class="text-12px text-[#999]">{{ group.count }} </text> <text class="text-12px text-[#999]">{{ group.count }} </text>

View File

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

View File

@@ -202,7 +202,7 @@ onLoad((options: any) => {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: 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; animation: scanline 8s linear infinite;
} }
@@ -215,7 +215,7 @@ onLoad((options: any) => {
height: 800rpx; height: 800rpx;
top: -300rpx; top: -300rpx;
right: -200rpx; 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%; border-radius: 50%;
animation: pulse1 10s infinite ease-in-out; animation: pulse1 10s infinite ease-in-out;
} }
@@ -225,7 +225,7 @@ onLoad((options: any) => {
height: 600rpx; height: 600rpx;
bottom: -200rpx; bottom: -200rpx;
left: -150rpx; 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%; border-radius: 50%;
animation: pulse2 12s infinite ease-in-out; animation: pulse2 12s infinite ease-in-out;
} }
@@ -235,7 +235,7 @@ onLoad((options: any) => {
height: 400rpx; height: 400rpx;
top: 50%; top: 50%;
right: -100rpx; 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%; border-radius: 50%;
animation: pulse3 14s infinite ease-in-out; animation: pulse3 14s infinite ease-in-out;
} }
@@ -384,8 +384,8 @@ onLoad((options: any) => {
&.focused { &.focused {
background: #ffffff; background: #ffffff;
border-color: #d4af37; border-color: var(--brand-primary);
box-shadow: 0 0 0 4rpx rgba(212, 175, 55, 0.08); box-shadow: 0 0 0 4rpx rgba(22, 119, 255, 0.08);
transform: translateY(-2rpx); 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, :root,
page { page {
/* === 品牌色 Brand Colors - 只保留3个核心颜色 === */ /* === 品牌色 Brand Colors === */
--brand-primary: #d4af37; /* 主色 - 色 */ --brand-primary: #1677ff; /* 主色 - 色 */
--brand-primary-light: #4096ff; /* 主色浅 */
--brand-primary-dark: #0958d9; /* 主色深 */
--brand-secondary: #1e293b; /* 辅色 - 深灰/黑色 */ --brand-secondary: #1e293b; /* 辅色 - 深灰/黑色 */
--brand-accent: #ffffff; /* 强调 - 白色 */ --brand-accent: #ffffff; /* 强调 - 白色 */
@@ -17,7 +19,7 @@ page {
--color-warning-bg: #fffbe6; /* 警告背景 */ --color-warning-bg: #fffbe6; /* 警告背景 */
--color-error: #ff4d4f; /* 错误 - Red */ --color-error: #ff4d4f; /* 错误 - Red */
--color-error-bg: #fff2f0; /* 错误背景 */ --color-error-bg: #fff2f0; /* 错误背景 */
--color-info: #1890ff; /* 信息 - Blue */ --color-info: #1677ff; /* 信息 - Blue(同主色) */
--color-info-bg: #e6f7ff; /* 信息背景 */ --color-info-bg: #e6f7ff; /* 信息背景 */
/* === 文字色阶 Text Colors === */ /* === 文字色阶 Text Colors === */
@@ -44,7 +46,7 @@ page {
--shadow-md: 0 2px 8px 0 rgba(0, 0, 0, 0.08); --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-lg: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
--shadow-xl: 0 8px 24px 0 rgba(0, 0, 0, 0.15); --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 === */
--radius-xs: 4px; --radius-xs: 4px;
@@ -98,11 +100,13 @@ page {
/* === 暗色主题 Dark Theme === */ /* === 暗色主题 Dark Theme === */
.theme-dark { .theme-dark {
--brand-primary: #ff8533; /* 暗色下色更柔和 */ --brand-primary: #4b9cff; /* 暗色下色更柔和 */
--brand-primary-light: #69b1ff;
--brand-primary-dark: #1668dc;
--color-success: #73d13d; --color-success: #73d13d;
--color-warning: #ffc53d; --color-warning: #ffc53d;
--color-error: #ff7875; --color-error: #ff7875;
--color-info: #40a9ff; --color-info: #4b9cff;
--text-primary: #ffffff; --text-primary: #ffffff;
--text-secondary: #b0b8c1; --text-secondary: #b0b8c1;
@@ -150,6 +154,14 @@ page {
.bg-muted { background-color: var(--bg-muted); } .bg-muted { background-color: var(--bg-muted); }
.bg-brand { background-color: var(--brand-primary); } .bg-brand { background-color: var(--brand-primary); }
.bg-brand-light { background-color: var(--brand-primary-light); } .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-primary { border-color: var(--border-primary); }
.border-secondary { border-color: var(--border-secondary); } .border-secondary { border-color: var(--border-secondary); }
@@ -229,7 +241,7 @@ page {
.input:focus { .input:focus {
border-color: var(--brand-primary); border-color: var(--brand-primary);
outline: none; 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 { .input::placeholder {

View File

@@ -47,6 +47,14 @@ export function post<T = any>(url: string, config?: HttpRequestConfig): Promise<
return request({ ...config, url, method: 'POST' }); 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> { export function upload<T = any>(url: string, config?: HttpRequestConfig): Promise<T> {
return request({ ...config, url, method: 'UPLOAD' }); return request({ ...config, url, method: 'UPLOAD' });
} }