fix: apply K unit formatting to all points displays
parent
e0363a845e
commit
df20870b6b
|
|
@ -1,578 +0,0 @@
|
|||
# 积分商城后端 API 设计文档
|
||||
|
||||
## 一、整体设计概述
|
||||
|
||||
### 1.1 设计目标
|
||||
- 实现 API 调用消费金额与积分的自动累积(1 美元 = 1000 积分)
|
||||
- 提供完整的积分商城商品管理与兑换流程
|
||||
- 支持实物商品的收货地址管理
|
||||
- 积分与用户 ID 强绑定,确保数据一致性
|
||||
|
||||
### 1.2 汇率规则
|
||||
- **1 USD = 1000 积分**
|
||||
- 积分计算以实际消费金额为准,精确到小数点后 2 位
|
||||
- 消费金额四舍五入后转换为积分
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库表结构设计
|
||||
|
||||
### 2.1 用户积分表 (user_points)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | BIGINT | 主键 | PRIMARY KEY, AUTO_INCREMENT |
|
||||
| user_id | VARCHAR(64) | 用户 ID | NOT NULL, INDEX |
|
||||
| points | BIGINT | 当前积分余额 | NOT NULL, DEFAULT 0 |
|
||||
| total_earned | BIGINT | 累计获得积分 | NOT NULL, DEFAULT 0 |
|
||||
| total_spent | BIGINT | 累计消耗积分 | NOT NULL, DEFAULT 0 |
|
||||
| level | VARCHAR(32) | 用户等级 | DEFAULT 'Lv.1' |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
| updated_at | DATETIME | 更新时间 | NOT NULL |
|
||||
| version | INT | 乐观锁版本 | NOT NULL, DEFAULT 0 |
|
||||
|
||||
**索引:**
|
||||
- `uk_user_id`: UNIQUE INDEX on `user_id`
|
||||
|
||||
### 2.2 积分流水表 (point_transactions)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | BIGINT | 主键 | PRIMARY KEY, AUTO_INCREMENT |
|
||||
| user_id | VARCHAR(64) | 用户 ID | NOT NULL, INDEX |
|
||||
| type | VARCHAR(32) | 类型:earn/spend | NOT NULL |
|
||||
| points | BIGINT | 变动积分(正数获得,负数消耗) | NOT NULL |
|
||||
| balance | BIGINT | 变动后余额 | NOT NULL |
|
||||
| source_type | VARCHAR(32) | 来源类型 | NOT NULL |
|
||||
| source_id | VARCHAR(64) | 来源 ID | NULL |
|
||||
| description | VARCHAR(255) | 描述 | NULL |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
|
||||
**说明:**
|
||||
- `source_type` 枚举:`api_call`(API 调用)、`exchange`(兑换商品)、`activity`(活动奖励)
|
||||
- `source_id`:对应 API 调用记录 ID 或订单 ID 等
|
||||
|
||||
### 2.3 积分商城商品表 (point_products)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | VARCHAR(64) | 商品 ID | PRIMARY KEY |
|
||||
| category_id | VARCHAR(64) | 分类 ID | NOT NULL, INDEX |
|
||||
| name | VARCHAR(128) | 商品名称 | NOT NULL |
|
||||
| subtitle | VARCHAR(255) | 副标题 | NULL |
|
||||
| description | TEXT | 详细描述 | NULL |
|
||||
| cover_url | VARCHAR(512) | 封面图片 | NULL |
|
||||
| points_price | BIGINT | 所需积分 | NOT NULL |
|
||||
| original_price | DECIMAL(10,2) | 原价(美元) | NULL |
|
||||
| stock | INT | 库存 | NOT NULL, DEFAULT 0 |
|
||||
| sold | INT | 已售数量 | NOT NULL, DEFAULT 0 |
|
||||
| tags | JSON | 标签数组 | NULL |
|
||||
| weight | DECIMAL(8,2) | 重量(kg) | NULL |
|
||||
| is_physical | TINYINT | 是否实物商品 | NOT NULL, DEFAULT 1 |
|
||||
| is_published | TINYINT | 是否上架 | NOT NULL, DEFAULT 0 |
|
||||
| sort_order | INT | 排序权重 | NOT NULL, DEFAULT 0 |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
| updated_at | DATETIME | 更新时间 | NOT NULL |
|
||||
|
||||
### 2.4 商品分类表 (point_categories)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | VARCHAR(64) | 分类 ID | PRIMARY KEY |
|
||||
| name | VARCHAR(64) | 分类名称 | NOT NULL |
|
||||
| icon_url | VARCHAR(512) | 图标 | NULL |
|
||||
| sort_order | INT | 排序权重 | NOT NULL, DEFAULT 0 |
|
||||
| is_enabled | TINYINT | 是否启用 | NOT NULL, DEFAULT 1 |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
|
||||
### 2.5 兑换订单表 (point_orders)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | VARCHAR(64) | 订单 ID | PRIMARY KEY |
|
||||
| user_id | VARCHAR(64) | 用户 ID | NOT NULL, INDEX |
|
||||
| product_id | VARCHAR(64) | 商品 ID | NOT NULL |
|
||||
| product_name | VARCHAR(128) | 商品名称快照 | NOT NULL |
|
||||
| points_price | BIGINT | 消耗积分 | NOT NULL |
|
||||
| status | VARCHAR(32) | 订单状态 | NOT NULL, INDEX |
|
||||
| recipient_name | VARCHAR(64) | 收货人姓名 | NOT NULL |
|
||||
| phone | VARCHAR(32) | 联系电话 | NOT NULL |
|
||||
| province | VARCHAR(64) | 省份 | NOT NULL |
|
||||
| city | VARCHAR(64) | 城市 | NOT NULL |
|
||||
| district | VARCHAR(64) | 区/县 | NOT NULL |
|
||||
| address | VARCHAR(512) | 详细地址 | NOT NULL |
|
||||
| zip_code | VARCHAR(16) | 邮政编码 | NULL |
|
||||
| tracking_company | VARCHAR(64) | 物流公司 | NULL |
|
||||
| tracking_number | VARCHAR(64) | 物流单号 | NULL |
|
||||
| shipped_at | DATETIME | 发货时间 | NULL |
|
||||
| delivered_at | DATETIME | 签收时间 | NULL |
|
||||
| remark | VARCHAR(255) | 备注 | NULL |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
| updated_at | DATETIME | 更新时间 | NOT NULL |
|
||||
|
||||
**状态枚举 (status):**
|
||||
- `pending`:待处理
|
||||
- `confirmed`:已确认
|
||||
- `shipped`:已发货
|
||||
- `delivered`:已签收
|
||||
- `cancelled`:已取消
|
||||
- `refunded`:已退款
|
||||
|
||||
### 2.6 Banner 表 (point_banners)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | VARCHAR(64) | Banner ID | PRIMARY KEY |
|
||||
| title | VARCHAR(128) | 标题 | NOT NULL |
|
||||
| subtitle | VARCHAR(255) | 副标题 | NULL |
|
||||
| image_url | VARCHAR(512) | 图片 URL | NOT NULL |
|
||||
| link_url | VARCHAR(512) | 跳转链接 | NULL |
|
||||
| sort_order | INT | 排序权重 | NOT NULL, DEFAULT 0 |
|
||||
| is_enabled | TINYINT | 是否启用 | NOT NULL, DEFAULT 1 |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
|
||||
### 2.7 促销入口表 (point_promo_entries)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | VARCHAR(64) | 入口 ID | PRIMARY KEY |
|
||||
| title | VARCHAR(64) | 标题 | NOT NULL |
|
||||
| subtitle | VARCHAR(128) | 副标题 | NULL |
|
||||
| icon_url | VARCHAR(512) | 图标 URL | NULL |
|
||||
| link_url | VARCHAR(512) | 跳转链接 | NULL |
|
||||
| sort_order | INT | 排序权重 | NOT NULL, DEFAULT 0 |
|
||||
| is_enabled | TINYINT | 是否启用 | NOT NULL, DEFAULT 1 |
|
||||
| created_at | DATETIME | 创建时间 | NOT NULL |
|
||||
|
||||
---
|
||||
|
||||
## 三、API 接口设计
|
||||
|
||||
### 3.1 通用响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 积分商城概览接口
|
||||
|
||||
**GET** `/points-mall/overview`
|
||||
|
||||
**描述:** 获取积分商城首页概览数据,包括用户积分、分类、Banner、促销入口等
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"me": {
|
||||
"points": 1280,
|
||||
"level": "Lv.2",
|
||||
"totalEarned": 5680,
|
||||
"totalSpent": 4400
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "all",
|
||||
"name": "全部",
|
||||
"sort": 0
|
||||
},
|
||||
{
|
||||
"id": "digital",
|
||||
"name": "虚拟权益",
|
||||
"sort": 1
|
||||
}
|
||||
],
|
||||
"banners": [
|
||||
{
|
||||
"id": "b1",
|
||||
"title": "限时活动",
|
||||
"subtitle": "Up to 25% Off",
|
||||
"imageUrl": "https://...",
|
||||
"linkUrl": "https://..."
|
||||
}
|
||||
],
|
||||
"promoEntries": [
|
||||
{
|
||||
"id": "p1",
|
||||
"title": "促销活动",
|
||||
"subtitle": "本周精选",
|
||||
"iconUrl": "https://...",
|
||||
"linkUrl": "https://..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 商品列表接口
|
||||
|
||||
**GET** `/points-mall/products`
|
||||
|
||||
**描述:** 获取商品列表,支持分页、筛选、排序
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| categoryId | String | 否 | 分类 ID,不传则查询全部 |
|
||||
| q | String | 否 | 搜索关键词 |
|
||||
| sort | String | 否 | 排序方式:`popular`(热度)、`newest`(最新)、`price_asc`(价格升序)、`price_desc`(价格降序),默认 `popular` |
|
||||
| page | Int | 否 | 页码,默认 1 |
|
||||
| pageSize | Int | 否 | 每页数量,默认 24 |
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 24,
|
||||
"total": 120,
|
||||
"items": [
|
||||
{
|
||||
"id": "prod_001",
|
||||
"categoryId": "digital",
|
||||
"name": "商品名称",
|
||||
"subtitle": "商品简短描述",
|
||||
"coverUrl": "https://...",
|
||||
"pointsPrice": 1990,
|
||||
"stock": 99,
|
||||
"sold": 12,
|
||||
"tags": ["限时", "热卖"],
|
||||
"isPhysical": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 商品详情接口
|
||||
|
||||
**GET** `/points-mall/products/{id}`
|
||||
|
||||
**描述:** 获取单个商品的详细信息
|
||||
|
||||
**路径参数:**
|
||||
- `id`: 商品 ID
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"id": "prod_001",
|
||||
"categoryId": "digital",
|
||||
"name": "商品名称",
|
||||
"subtitle": "商品简短描述",
|
||||
"description": "商品详细描述...",
|
||||
"coverUrl": "https://...",
|
||||
"imageUrls": ["https://...", "https://..."],
|
||||
"pointsPrice": 1990,
|
||||
"originalPrice": 19.90,
|
||||
"stock": 99,
|
||||
"sold": 12,
|
||||
"tags": ["限时", "热卖"],
|
||||
"isPhysical": true,
|
||||
"weight": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 兑换商品接口
|
||||
|
||||
**POST** `/points-mall/exchange`
|
||||
|
||||
**描述:** 提交兑换订单,扣减积分,创建订单
|
||||
|
||||
**请求体:**
|
||||
```json
|
||||
{
|
||||
"productId": "prod_001",
|
||||
"recipientName": "张三",
|
||||
"phone": "13800138000",
|
||||
"province": "广东省",
|
||||
"city": "深圳市",
|
||||
"district": "南山区",
|
||||
"address": "科技园南区XX路XX号",
|
||||
"zipCode": "518000"
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| productId | String | 是 | 商品 ID |
|
||||
| recipientName | String | 是 | 收货人姓名 |
|
||||
| phone | String | 是 | 手机号码 |
|
||||
| province | String | 是 | 省份 |
|
||||
| city | String | 是 | 城市 |
|
||||
| district | String | 是 | 区/县 |
|
||||
| address | String | 是 | 详细地址 |
|
||||
| zipCode | String | 否 | 邮政编码 |
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"orderId": "order_20240601_0001",
|
||||
"pointsDeducted": 1990,
|
||||
"remainingPoints": 10810
|
||||
}
|
||||
```
|
||||
|
||||
**错误码:**
|
||||
|
||||
| code | message | 说明 |
|
||||
|------|---------|------|
|
||||
| 40001 | 积分不足 | 用户积分不足以兑换该商品 |
|
||||
| 40002 | 商品已下架 | 商品未发布或已下架 |
|
||||
| 40003 | 库存不足 | 商品库存不足 |
|
||||
| 40004 | 商品不存在 | 商品 ID 无效 |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 兑换订单列表接口
|
||||
|
||||
**GET** `/points-mall/orders`
|
||||
|
||||
**描述:** 获取用户的兑换订单列表
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| status | String | 否 | 订单状态筛选 |
|
||||
| page | Int | 否 | 页码,默认 1 |
|
||||
| pageSize | Int | 否 | 每页数量,默认 20 |
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 5,
|
||||
"items": [
|
||||
{
|
||||
"id": "order_20240601_0001",
|
||||
"productId": "prod_001",
|
||||
"productName": "商品名称",
|
||||
"coverUrl": "https://...",
|
||||
"pointsPrice": 1990,
|
||||
"status": "shipped",
|
||||
"statusText": "已发货",
|
||||
"trackingCompany": "顺丰速运",
|
||||
"trackingNumber": "SF1234567890",
|
||||
"createdAt": "2024-06-01T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 订单详情接口
|
||||
|
||||
**GET** `/points-mall/orders/{id}`
|
||||
|
||||
**描述:** 获取单个订单的详细信息
|
||||
|
||||
**路径参数:**
|
||||
- `id`: 订单 ID
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"id": "order_20240601_0001",
|
||||
"productId": "prod_001",
|
||||
"productName": "商品名称",
|
||||
"coverUrl": "https://...",
|
||||
"pointsPrice": 1990,
|
||||
"status": "shipped",
|
||||
"statusText": "已发货",
|
||||
"recipientName": "张三",
|
||||
"phone": "13800138000",
|
||||
"province": "广东省",
|
||||
"city": "深圳市",
|
||||
"district": "南山区",
|
||||
"address": "科技园南区XX路XX号",
|
||||
"zipCode": "518000",
|
||||
"trackingCompany": "顺丰速运",
|
||||
"trackingNumber": "SF1234567890",
|
||||
"shippedAt": "2024-06-02T14:00:00Z",
|
||||
"createdAt": "2024-06-01T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.8 积分流水接口
|
||||
|
||||
**GET** `/points-mall/transactions`
|
||||
|
||||
**描述:** 获取用户积分变动记录
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| type | String | 否 | 类型:`earn`(收入)、`spend`(支出) |
|
||||
| page | Int | 否 | 页码,默认 1 |
|
||||
| pageSize | Int | 否 | 每页数量,默认 20 |
|
||||
|
||||
**响应数据:**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 150,
|
||||
"items": [
|
||||
{
|
||||
"id": 1001,
|
||||
"type": "earn",
|
||||
"points": 500,
|
||||
"balance": 12800,
|
||||
"sourceType": "api_call",
|
||||
"description": "API 调用奖励",
|
||||
"createdAt": "2024-06-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": 1000,
|
||||
"type": "spend",
|
||||
"points": -1990,
|
||||
"balance": 12300,
|
||||
"sourceType": "exchange",
|
||||
"sourceId": "order_20240601_0001",
|
||||
"description": "兑换商品:XXX",
|
||||
"createdAt": "2024-06-01T09:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、积分累积逻辑
|
||||
|
||||
### 4.1 积分累积触发时机
|
||||
|
||||
积分累积在以下场景触发:
|
||||
1. **API 调用完成时**:每次成功的 LLM API 调用后,根据实际消费金额计算积分
|
||||
2. **活动奖励**:通过活动接口手动发放积分
|
||||
|
||||
### 4.2 积分计算公式
|
||||
|
||||
```
|
||||
积分 = 实际消费金额(USD) × 1000
|
||||
```
|
||||
|
||||
**示例:**
|
||||
- 消费 $0.002 → 2 积分
|
||||
- 消费 $0.5 → 500 积分
|
||||
- 消费 $1.2 → 1200 积分
|
||||
|
||||
### 4.3 积分入账流程
|
||||
|
||||
1. API 调用完成,记录 token 使用量和费用
|
||||
2. 计算积分:`points = floor(costUSD * 1000)` 或四舍五入
|
||||
3. 使用事务更新 `user_points` 表:
|
||||
- `points` += 新增积分
|
||||
- `total_earned` += 新增积分
|
||||
4. 写入 `point_transactions` 流水记录
|
||||
5. **注意**:需要使用乐观锁防止并发问题
|
||||
|
||||
---
|
||||
|
||||
## 五、兑换流程设计
|
||||
|
||||
### 5.1 兑换流程图
|
||||
|
||||
```
|
||||
用户点击兑换
|
||||
↓
|
||||
验证积分是否足够
|
||||
↓
|
||||
验证商品是否上架
|
||||
↓
|
||||
验证库存是否充足
|
||||
↓
|
||||
【事务开始】
|
||||
扣减用户积分
|
||||
↓
|
||||
扣减商品库存
|
||||
↓
|
||||
增加商品已售数量
|
||||
↓
|
||||
创建兑换订单
|
||||
↓
|
||||
写入积分消费流水
|
||||
↓
|
||||
【事务提交】
|
||||
↓
|
||||
返回订单信息
|
||||
```
|
||||
|
||||
### 5.2 并发控制
|
||||
|
||||
1. **用户积分表**:使用乐观锁(`version` 字段)防止超扣
|
||||
2. **商品库存**:使用 `UPDATE ... WHERE stock >= 1` 原子操作
|
||||
3. **事务隔离**:使用可重复读(REPEATABLE READ)级别
|
||||
|
||||
---
|
||||
|
||||
## 六、管理后台接口(可选)
|
||||
|
||||
### 6.1 商品管理
|
||||
|
||||
- **POST** `/admin/points-mall/products` - 创建商品
|
||||
- **PUT** `/admin/points-mall/products/{id}` - 更新商品
|
||||
- **DELETE** `/admin/points-mall/products/{id}` - 删除商品
|
||||
- **PATCH** `/admin/points-mall/products/{id}/publish` - 上架/下架商品
|
||||
|
||||
### 6.2 订单管理
|
||||
|
||||
- **GET** `/admin/points-mall/orders` - 订单列表
|
||||
- **PUT** `/admin/points-mall/orders/{id}/ship` - 发货(填写物流信息)
|
||||
- **PUT** `/admin/points-mall/orders/{id}/confirm` - 确认订单
|
||||
- **PUT** `/admin/points-mall/orders/{id}/cancel` - 取消订单(退还积分)
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
### 7.1 数据一致性
|
||||
- 所有涉及积分变动的操作必须使用数据库事务
|
||||
- 使用乐观锁防止并发扣减问题
|
||||
- 积分流水表是审计的重要依据,不可删除或修改
|
||||
|
||||
### 7.2 安全考虑
|
||||
- 所有接口必须经过用户认证
|
||||
- 积分扣减必须验证当前用户的积分余额
|
||||
- 防止重复提交兑换请求(可使用幂等键)
|
||||
|
||||
### 7.3 性能优化
|
||||
- 用户积分数据可考虑缓存(如 Redis)
|
||||
- 商品列表接口建议加缓存
|
||||
- 积分流水表建议按时间分区
|
||||
|
||||
### 7.4 监控告警
|
||||
- 积分扣除失败告警
|
||||
- 库存不足告警
|
||||
- 异常大额积分变动告警
|
||||
|
||||
---
|
||||
|
||||
## 八、前端已实现功能
|
||||
|
||||
前端已完成以下功能开发,等待后端接口对接:
|
||||
|
||||
1. ✅ 积分商城首页 UI,包含积分展示
|
||||
2. ✅ 商品分类筛选、搜索、排序
|
||||
3. ✅ 商品卡片列表展示
|
||||
4. ✅ 兑换弹窗,包含收货地址表单
|
||||
5. ✅ 积分不足提示
|
||||
6. ✅ 统计页面展示消费金额与积分对应关系
|
||||
7. ✅ 1 USD = 1000 积分汇率展示
|
||||
|
|
@ -1,371 +0,0 @@
|
|||
# 积分商城(Points Mall)接口需求说明
|
||||
|
||||
本文档用于对接 aura-web 的「积分商城」页面数据与兑换流程,供后端实现接口与数据表结构时参考。
|
||||
|
||||
## 页面结构对应的数据
|
||||
|
||||
页面 UI 由 4 个区域组成:
|
||||
|
||||
1. 头部商品分类导航栏(横向)
|
||||
2. 公告栏
|
||||
3. banner 区 + 促销活动入口(2 个入口卡片)
|
||||
4. 商品内容区(商品列表 + 搜索/排序/分页)
|
||||
|
||||
## 统一约定
|
||||
|
||||
- BaseURL:`https://api.hoyidata.com/aura/v1`
|
||||
- 鉴权:沿用当前登录态(Cookie / Session)或 `Authorization: Bearer <token>`(以现有登录实现为准),接口需校验登录态
|
||||
- 图片字段:返回绝对 URL(推荐 CDN 域名),前端不拼接
|
||||
- 时间字段:建议统一使用毫秒时间戳(number)
|
||||
- 失败格式:沿用现有 aura 接口的错误格式(HTTP Status + JSON message/error)
|
||||
- `GET /points-mall/overview` 已废弃(HTTP 410),前端需按数据维度分别调用 /me、/categories、/announcements、/banners、/promo-entries
|
||||
|
||||
## 接口列表(前端必需)
|
||||
|
||||
### 1) 我的积分
|
||||
|
||||
`GET /points-mall/me`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"points": 1280,
|
||||
"level": "Lv.2",
|
||||
"totalSpentUSD": 0.174
|
||||
}
|
||||
```
|
||||
|
||||
### 2) 分类
|
||||
|
||||
`GET /points-mall/categories`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
[
|
||||
{ "id": "all", "name": "全部", "sort": 0 },
|
||||
{ "id": "digital", "name": "虚拟权益", "sort": 1 }
|
||||
]
|
||||
```
|
||||
|
||||
### 3) 公告
|
||||
|
||||
`GET /points-mall/announcements`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "a1",
|
||||
"title": "公告:积分规则升级中",
|
||||
"content": "本期暂不开放兑换,页面仅用于 UI 预览。",
|
||||
"linkUrl": "https://..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4) Banner
|
||||
|
||||
`GET /points-mall/banners`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "b1",
|
||||
"title": "限时上新",
|
||||
"subtitle": "Up to 25% Off",
|
||||
"imageUrl": "https://cdn.../banner.png",
|
||||
"linkUrl": "https://..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 5) 促销入口
|
||||
|
||||
`GET /points-mall/promo-entries`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "p1",
|
||||
"title": "促销活动",
|
||||
"subtitle": "本周精选",
|
||||
"iconUrl": "https://cdn.../promo.png",
|
||||
"linkUrl": "https://..."
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"title": "积分任务",
|
||||
"subtitle": "快速涨积分",
|
||||
"iconUrl": "https://cdn.../tasks.png",
|
||||
"linkUrl": "https://..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 6) 商品列表(搜索/筛选/排序/分页)
|
||||
|
||||
`GET /points-mall/products`
|
||||
|
||||
**Query**
|
||||
- `categoryId`:可选(分类 ID;不传代表全部)
|
||||
- `q`:可选(搜索关键字;匹配 name/subtitle)
|
||||
- `sort`:可选,默认 `popular`
|
||||
- `popular`:热度优先(建议按 sold 或近期兑换量)
|
||||
- `newest`:最新上架
|
||||
- `price_asc`:积分从低到高
|
||||
- `price_desc`:积分从高到低
|
||||
- `page`:可选,默认 `1`
|
||||
- `pageSize`:可选,默认 `24`
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 24,
|
||||
"total": 120,
|
||||
"items": [
|
||||
{
|
||||
"id": "prd_001",
|
||||
"categoryId": "digital",
|
||||
"name": "Claude Pro 月卡",
|
||||
"subtitle": "兑换后获得激活码",
|
||||
"coverUrl": "https://cdn.../cover.png",
|
||||
"pointsPrice": 1999,
|
||||
"stock": 99,
|
||||
"sold": 12,
|
||||
"tags": ["热卖", "限时"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7) 商品详情
|
||||
|
||||
`GET /points-mall/products/{productId}`
|
||||
|
||||
**Response(建议)**
|
||||
```json
|
||||
{
|
||||
"id": "prd_001",
|
||||
"categoryId": "digital",
|
||||
"name": "Claude Pro 月卡",
|
||||
"subtitle": "兑换后获得激活码",
|
||||
"coverUrl": "https://cdn.../cover.png",
|
||||
"imageUrls": ["https://cdn.../1.png", "https://cdn.../2.png"],
|
||||
"pointsPrice": 1999,
|
||||
"stock": 99,
|
||||
"sold": 12,
|
||||
"tags": ["热卖", "限时"],
|
||||
"description": "富文本/Markdown 均可,建议 Markdown",
|
||||
"type": "virtual",
|
||||
"delivery": {
|
||||
"mode": "code",
|
||||
"tips": "兑换成功后在【订单详情】查看兑换码"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8) 创建兑换订单(扣积分)
|
||||
|
||||
`POST /points-mall/orders`
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{
|
||||
"productId": "prd_001",
|
||||
"quantity": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"orderId": "ord_001",
|
||||
"status": "paid",
|
||||
"pointsCost": 1999,
|
||||
"remainingPoints": 281
|
||||
}
|
||||
```
|
||||
|
||||
**关键规则**
|
||||
- 需要幂等:建议支持 `Idempotency-Key` header,避免重复扣积分
|
||||
- 校验库存与用户积分
|
||||
- 虚拟商品/实物商品可共用该接口,但实物商品需要补充收货信息(可扩展字段)
|
||||
|
||||
### 9) 订单列表 / 订单详情
|
||||
|
||||
`GET /points-mall/orders`
|
||||
|
||||
**Query(建议)**
|
||||
- `page` / `pageSize`
|
||||
- `status`:可选
|
||||
|
||||
`GET /points-mall/orders/{orderId}`
|
||||
|
||||
**Response(建议)**
|
||||
```json
|
||||
{
|
||||
"id": "ord_001",
|
||||
"status": "paid",
|
||||
"createdAt": 1730000000000,
|
||||
"product": {
|
||||
"id": "prd_001",
|
||||
"name": "Claude Pro 月卡",
|
||||
"coverUrl": "https://cdn.../cover.png"
|
||||
},
|
||||
"quantity": 1,
|
||||
"pointsCost": 1999,
|
||||
"delivery": {
|
||||
"mode": "code",
|
||||
"code": "XXXX-YYYY-ZZZZ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10) 积分流水(可选,但强烈建议)
|
||||
|
||||
用于解释积分变动与对账。
|
||||
|
||||
`GET /points-mall/me/points-ledger`
|
||||
|
||||
**Query(建议)**
|
||||
- `page` / `pageSize`
|
||||
|
||||
**Response(建议)**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 300,
|
||||
"items": [
|
||||
{
|
||||
"id": "pl_001",
|
||||
"createdAt": 1730000000000,
|
||||
"change": -1999,
|
||||
"balance": 281,
|
||||
"reason": "兑换 Claude Pro 月卡",
|
||||
"bizType": "points_mall_order",
|
||||
"bizId": "ord_001"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 数据表结构建议(后端实现参考)
|
||||
|
||||
说明:下述为建议表结构,字段类型可按现有数据库规范调整;建议均包含 `created_at / updated_at` 与必要索引。
|
||||
|
||||
### 1) `points_mall_categories`(分类)
|
||||
- `id`(PK, string/uuid)
|
||||
- `name`(varchar)
|
||||
- `sort`(int)
|
||||
- `enabled`(bool)
|
||||
|
||||
索引:
|
||||
- `enabled, sort`
|
||||
|
||||
### 2) `points_mall_announcements`(公告)
|
||||
- `id`(PK)
|
||||
- `title`(varchar)
|
||||
- `content`(text)
|
||||
- `link_url`(varchar, nullable)
|
||||
- `start_at`(bigint, nullable)
|
||||
- `end_at`(bigint, nullable)
|
||||
- `enabled`(bool)
|
||||
- `sort`(int)
|
||||
|
||||
索引:
|
||||
- `enabled, sort`
|
||||
- `start_at, end_at`
|
||||
|
||||
### 3) `points_mall_banners`(banner)
|
||||
- `id`(PK)
|
||||
- `title`(varchar)
|
||||
- `subtitle`(varchar, nullable)
|
||||
- `image_url`(varchar)
|
||||
- `link_url`(varchar, nullable)
|
||||
- `start_at`(bigint, nullable)
|
||||
- `end_at`(bigint, nullable)
|
||||
- `enabled`(bool)
|
||||
- `sort`(int)
|
||||
|
||||
索引:
|
||||
- `enabled, sort`
|
||||
- `start_at, end_at`
|
||||
|
||||
### 4) `points_mall_promo_entries`(促销入口)
|
||||
- `id`(PK)
|
||||
- `title`(varchar)
|
||||
- `subtitle`(varchar, nullable)
|
||||
- `icon_url`(varchar, nullable)
|
||||
- `link_url`(varchar, nullable)
|
||||
- `enabled`(bool)
|
||||
- `sort`(int)
|
||||
|
||||
索引:
|
||||
- `enabled, sort`
|
||||
|
||||
### 5) `points_mall_products`(商品)
|
||||
- `id`(PK)
|
||||
- `category_id`(FK -> categories.id)
|
||||
- `name`(varchar)
|
||||
- `subtitle`(varchar, nullable)
|
||||
- `description`(text / markdown)
|
||||
- `cover_url`(varchar)
|
||||
- `image_urls`(json/text,数组)
|
||||
- `type`(enum: `virtual` | `physical`)
|
||||
- `points_price`(int)
|
||||
- `stock`(int)
|
||||
- `sold`(int,累计兑换量,或可由订单聚合)
|
||||
- `tags`(json/text,数组)
|
||||
- `enabled`(bool)
|
||||
- `start_at`(bigint, nullable)
|
||||
- `end_at`(bigint, nullable)
|
||||
- `sort`(int)
|
||||
|
||||
索引:
|
||||
- `enabled, start_at, end_at`
|
||||
- `category_id, enabled, sort`
|
||||
- 搜索:`name`/`subtitle` 建议全文索引或 LIKE(视数据库而定)
|
||||
|
||||
### 6) `points_mall_orders`(订单)
|
||||
- `id`(PK)
|
||||
- `user_id`(FK)
|
||||
- `product_id`(FK)
|
||||
- `quantity`(int)
|
||||
- `points_cost`(int)
|
||||
- `status`(enum: `created` | `paid` | `delivered` | `canceled` | `refunded`)
|
||||
- `delivery_mode`(enum: `code` | `shipping`)
|
||||
- `delivery_payload`(json,虚拟码/物流信息等)
|
||||
- `idempotency_key`(varchar, nullable,建议唯一索引 + user_id 组合)
|
||||
|
||||
索引:
|
||||
- `user_id, created_at desc`
|
||||
- `status, created_at desc`
|
||||
- `user_id, idempotency_key`(唯一)
|
||||
|
||||
### 7) `points_ledger`(积分流水)
|
||||
- `id`(PK)
|
||||
- `user_id`(FK)
|
||||
- `change`(int,正负)
|
||||
- `balance`(int)
|
||||
- `reason`(varchar)
|
||||
- `biz_type`(varchar)
|
||||
- `biz_id`(varchar)
|
||||
|
||||
索引:
|
||||
- `user_id, created_at desc`
|
||||
- `biz_type, biz_id`
|
||||
|
||||
## 前端实现已对齐的字段
|
||||
|
||||
前端已按以下字段读取:
|
||||
- 分类:`id/name/sort`
|
||||
- 公告:`title/content/linkUrl`
|
||||
- banner:`title/subtitle/imageUrl/linkUrl`
|
||||
- 促销入口:`title/subtitle/iconUrl/linkUrl`
|
||||
- 商品列表:`id/categoryId/name/subtitle/coverUrl/pointsPrice/stock/sold/tags`
|
||||
|
||||
如后端字段命名需要用 snake_case,可在接口层做映射,但建议直接使用上述 camelCase,减少前端 mapping。
|
||||
|
|
@ -51,7 +51,7 @@ const NAV_GROUPS: Array<{
|
|||
{
|
||||
label: '商城',
|
||||
items: [
|
||||
{ to: '/points-mall', icon: <CompassOutlined />, label: 'Token商城' }
|
||||
{ to: '/points-mall', icon: <CompassOutlined />, label: '积分商城' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
justify-content: initial;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 1.25rem 1rem 1.5rem;
|
||||
padding: 20px 16px 24px;
|
||||
background: var(--gradient-hero);
|
||||
}
|
||||
|
||||
|
|
@ -40,42 +40,42 @@
|
|||
min-height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.login-page-web .login-brand-panel {
|
||||
padding: 0.25rem 0 0;
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.login-page-web .login-brand-header {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-page-web .login-title {
|
||||
max-width: none;
|
||||
font-size: 2rem;
|
||||
font-size: 32px;
|
||||
line-height: 1.18;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.login-page-web .login-subtitle {
|
||||
max-width: none;
|
||||
margin-top: 0.875rem;
|
||||
font-size: 0.9375rem;
|
||||
margin-top: 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page-web .login-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.625rem;
|
||||
margin-top: 1rem;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.login-page-web .login-feature-item {
|
||||
min-height: 2.75rem;
|
||||
min-height: 44px;
|
||||
border: 1px solid rgba(17, 103, 255, 0.14);
|
||||
border-radius: 0.875rem;
|
||||
padding: 0.625rem;
|
||||
border-radius: 14px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export function usePointsMallPageLogic() {
|
|||
...rawMe,
|
||||
points: Number((rawMe as any)?.points ?? 0),
|
||||
};
|
||||
const cats = categoriesRes.status === 'fulfilled' ? categoriesRes.value : MOCK_OVERVIEW.categories;
|
||||
const loadedCats = categoriesRes.status === 'fulfilled' ? categoriesRes.value : MOCK_OVERVIEW.categories;
|
||||
const cats = loadedCats?.length ? loadedCats : MOCK_OVERVIEW.categories;
|
||||
const announcements = announcementsRes.status === 'fulfilled' ? announcementsRes.value : MOCK_OVERVIEW.announcements;
|
||||
const banners = bannersRes.status === 'fulfilled' ? bannersRes.value : MOCK_OVERVIEW.banners;
|
||||
const promoEntries = promoEntriesRes.status === 'fulfilled' ? promoEntriesRes.value : MOCK_OVERVIEW.promoEntries;
|
||||
|
|
@ -53,10 +54,10 @@ export function usePointsMallPageLogic() {
|
|||
setCategoryId(cats[0].id);
|
||||
}
|
||||
if (meRes.status === 'rejected') {
|
||||
message.error('获取Token信息失败,请稍后重试');
|
||||
message.error('获取积分信息失败,请稍后重试');
|
||||
}
|
||||
} catch {
|
||||
message.error('获取Token信息失败,请稍后重试');
|
||||
message.error('获取积分信息失败,请稍后重试');
|
||||
setOverview({ ...MOCK_OVERVIEW, me: { points: 0, level: 'Lv.0' } });
|
||||
setCategories(MOCK_OVERVIEW.categories);
|
||||
} finally {
|
||||
|
|
@ -74,7 +75,19 @@ export function usePointsMallPageLogic() {
|
|||
page,
|
||||
pageSize,
|
||||
});
|
||||
if (Array.isArray(res.items) && res.items.length > 0) {
|
||||
setProductsRes(res);
|
||||
return;
|
||||
}
|
||||
const fallback = MOCK_PRODUCTS.filter((p) => (categoryId === 'all' ? true : p.categoryId === categoryId)).filter((p) =>
|
||||
q ? (p.name + p.subtitle).toLowerCase().includes(q.toLowerCase()) : true
|
||||
);
|
||||
setProductsRes({
|
||||
page,
|
||||
pageSize,
|
||||
total: fallback.length,
|
||||
items: fallback.slice((page - 1) * pageSize, page * pageSize),
|
||||
});
|
||||
} catch {
|
||||
const filtered = MOCK_PRODUCTS.filter((p) => (categoryId === 'all' ? true : p.categoryId === categoryId)).filter((p) =>
|
||||
q ? (p.name + p.subtitle).toLowerCase().includes(q.toLowerCase()) : true
|
||||
|
|
@ -135,7 +148,7 @@ export function usePointsMallPageLogic() {
|
|||
return { ...prev, me: { ...prev.me, points: remainingPoints } };
|
||||
});
|
||||
}
|
||||
message.success('已冻结Token并预扣库存,请继续填写收件信息');
|
||||
message.success('已冻结积分并预扣库存,请继续填写收件信息');
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.message || e?.response?.data?.error || e?.message || '兑换失败,请稍后重试';
|
||||
message.error(msg);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { SearchOutlined } from '@ant-design/icons';
|
|||
import { Button, Card, Empty, Input, Select, Space, Spin, Tag } from 'antd';
|
||||
import type { PointsMallProduct } from '../../../api';
|
||||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import '../styles/points-mall-h5-products.css';
|
||||
|
||||
interface Props {
|
||||
logic: PointsMallPageLogicOutput;
|
||||
|
|
@ -52,8 +53,8 @@ export default function PointsMallH5Products({ logic }: Props) {
|
|||
options={[
|
||||
{ value: 'popular', label: '热度优先' },
|
||||
{ value: 'newest', label: '最新上架' },
|
||||
{ value: 'price_asc', label: 'Token从低到高' },
|
||||
{ value: 'price_desc', label: 'Token从高到低' }
|
||||
{ value: 'price_asc', label: '积分从低到高' },
|
||||
{ value: 'price_desc', label: '积分从高到低' }
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
|
|
@ -79,24 +80,21 @@ export default function PointsMallH5Products({ logic }: Props) {
|
|||
) : (
|
||||
<div className="points-mall-products-grid h5-points-mall-products-grid">
|
||||
{products.map((p: PointsMallProduct) => (
|
||||
<div key={p.id} className="points-mall-product-card h5-points-mall-product-card">
|
||||
<div className="points-mall-product-cover" style={{ backgroundImage: `url(${p.coverUrl})` }} />
|
||||
<div className="points-mall-product-body">
|
||||
<div className="points-mall-product-header">
|
||||
<div className="points-mall-product-info">
|
||||
<div className="points-mall-product-name">{p.name}</div>
|
||||
<div className="points-mall-product-desc">{p.subtitle}</div>
|
||||
</div>
|
||||
<div key={p.id} className="points-mall-product-card h5-points-mall-product-card h5-product-tile">
|
||||
<div className="points-mall-product-cover h5-product-tile-cover">
|
||||
{p.coverUrl ? <img src={p.coverUrl} alt={p.name} className="h5-product-tile-image" /> : null}
|
||||
{p.tags?.length ? (
|
||||
<Tag bordered={false} className="points-mall-product-tag">
|
||||
<Tag bordered={false} className="points-mall-product-tag h5-product-tile-tag">
|
||||
{p.tags[0]}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="points-mall-product-body">
|
||||
<div className="points-mall-product-name">{p.name}</div>
|
||||
<div className="points-mall-product-price-row h5-points-mall-product-price-row">
|
||||
<div>
|
||||
<span className="points-mall-product-price">{p.pointsPrice >= 1000 ? `${(p.pointsPrice / 1000).toFixed(1)} K` : Number(p.pointsPrice).toLocaleString()}</span>
|
||||
<span className="points-mall-product-price-label">Token</span>
|
||||
<span className="points-mall-product-price-label">分</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -104,13 +102,9 @@ export default function PointsMallH5Products({ logic }: Props) {
|
|||
disabled={userPoints < p.pointsPrice || exchangeLoading}
|
||||
onClick={() => handleExchangeClick(p)}
|
||||
>
|
||||
{userPoints < p.pointsPrice ? 'Token不足' : '兑换'}
|
||||
{userPoints < p.pointsPrice ? '不足' : '兑'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="points-mall-product-footer">
|
||||
<span>库存 {p.stock}</span>
|
||||
<span>已兑 {p.sold}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,16 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
setCategoryId,
|
||||
setPage
|
||||
} = logic;
|
||||
const visiblePromoEntries = promoEntries.filter((p) => p.title !== '暂未配置' && p.subtitle !== '未配置活动位');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="points-mall-hero h5-points-mall-hero">
|
||||
<div className="points-mall-header h5-points-mall-header">
|
||||
<div className="points-mall-title-section">
|
||||
<h1 className="page-title stats-page-title h5-page-title">Token商城</h1>
|
||||
<h1 className="page-title stats-page-title h5-page-title">积分商城</h1>
|
||||
<p className="page-subtitle stats-page-subtitle h5-page-subtitle">
|
||||
使用token兑换权益、工具和活动礼包。token通过 API 调用消费自动累积。
|
||||
使用积分兑换权益、工具和活动礼包。<br />积分通过 API 调用消费自动累积,1 美元 = 1000 积分。
|
||||
</p>
|
||||
</div>
|
||||
<div className="points-mall-balance-card h5-points-mall-balance-card">
|
||||
|
|
@ -35,7 +36,7 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
) : (
|
||||
<div className="points-balance-row h5-points-balance-row">
|
||||
<div className="points-balance-main">
|
||||
<div className="points-balance-label">我的Token</div>
|
||||
<div className="points-balance-label">我的积分</div>
|
||||
<div className="points-balance-value">{userPoints >= 1000 ? `${(userPoints / 1000).toFixed(1)} K` : userPoints.toLocaleString()}</div>
|
||||
<div className="points-balance-subtext">
|
||||
累计消费 ${typeof totalSpentUSD === 'number' ? totalSpentUSD.toFixed(2) : '--'}
|
||||
|
|
@ -50,6 +51,7 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{categories.length > 0 && (
|
||||
<Card className="points-mall-category-card h5-points-mall-category-card">
|
||||
<div className="points-mall-category-body">
|
||||
<div className="points-mall-category-row h5-points-mall-category-row">
|
||||
|
|
@ -71,6 +73,7 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="points-mall-banner-section h5-points-mall-banner-section">
|
||||
<div className="points-mall-banner-card h5-points-mall-banner-card">
|
||||
|
|
@ -86,8 +89,9 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
<div className="points-mall-banner-footer">banner 图片与跳转链接由后端配置</div>
|
||||
</div>
|
||||
|
||||
{visiblePromoEntries.length > 0 && (
|
||||
<div className="points-mall-promo-grid h5-points-mall-promo-grid">
|
||||
{promoEntries.slice(0, 2).map((p) => (
|
||||
{visiblePromoEntries.slice(0, 2).map((p) => (
|
||||
<div key={p.id} className="points-mall-promo-card h5-points-mall-promo-card">
|
||||
<div className="points-mall-promo-body">
|
||||
<div className="points-mall-promo-title">{p.title}</div>
|
||||
|
|
@ -98,8 +102,8 @@ export default function PointsMallH5Top({ logic }: Props) {
|
|||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{promoEntries.length < 2 && <div className="points-mall-promo-empty">促销入口由后端配置</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
<div className="points-mall-hero">
|
||||
<div className="points-mall-header">
|
||||
<div className="points-mall-title-section">
|
||||
<h1 className="page-title stats-page-title">Token商城</h1>
|
||||
<p className="page-subtitle stats-page-subtitle">使用token兑换权益、工具和活动礼包。token通过 API 调用消费自动累积。</p>
|
||||
<h1 className="page-title stats-page-title">积分商城</h1>
|
||||
<p className="page-subtitle stats-page-subtitle">使用积分兑换权益、工具和活动礼包。积分通过 API 调用消费自动累积,1 美元 = 1000 积分。</p>
|
||||
</div>
|
||||
<div className="points-mall-balance-card">
|
||||
{overviewLoading ? (
|
||||
|
|
@ -58,7 +58,7 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
) : (
|
||||
<div className="points-balance-row">
|
||||
<div>
|
||||
<div className="points-balance-label">我的Token</div>
|
||||
<div className="points-balance-label">我的积分</div>
|
||||
<div className="points-balance-value">{userPoints >= 1000 ? `${(userPoints / 1000).toFixed(1)} K` : userPoints.toLocaleString()}</div>
|
||||
<div className="points-balance-subtext">
|
||||
累计消费 ${typeof totalSpentUSD === 'number' ? totalSpentUSD.toFixed(2) : '--'}
|
||||
|
|
@ -152,8 +152,8 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
options={[
|
||||
{ value: 'popular', label: '热度优先' },
|
||||
{ value: 'newest', label: '最新上架' },
|
||||
{ value: 'price_asc', label: 'Token从低到高' },
|
||||
{ value: 'price_desc', label: 'Token从高到低' },
|
||||
{ value: 'price_asc', label: '积分从低到高' },
|
||||
{ value: 'price_desc', label: '积分从高到低' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
|
|
@ -181,7 +181,7 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
<div className="points-mall-products-grid">
|
||||
{products.map((p: PointsMallProduct) => (
|
||||
<div key={p.id} className="points-mall-product-card">
|
||||
<div className="points-mall-product-cover" style={{ backgroundImage: `url(${p.coverUrl})` }} />
|
||||
<div className="points-mall-product-cover" />
|
||||
<div className="points-mall-product-body">
|
||||
<div className="points-mall-product-header">
|
||||
<div className="points-mall-product-info">
|
||||
|
|
@ -197,7 +197,7 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
<div className="points-mall-product-price-row">
|
||||
<div>
|
||||
<span className="points-mall-product-price">{p.pointsPrice >= 1000 ? `${(p.pointsPrice / 1000).toFixed(1)} K` : Number(p.pointsPrice).toLocaleString()}</span>
|
||||
<span className="points-mall-product-price-label">Token</span>
|
||||
<span className="points-mall-product-price-label">积分</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -205,7 +205,7 @@ export default function PointsMallPageWeb({ logic }: Props) {
|
|||
disabled={userPoints < p.pointsPrice || exchangeLoading}
|
||||
onClick={() => handleExchangeClick(p)}
|
||||
>
|
||||
{userPoints < p.pointsPrice ? 'Token不足' : '兑换'}
|
||||
{userPoints < p.pointsPrice ? '积分不足' : '兑换'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="points-mall-product-footer">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const MOCK_OVERVIEW: PointsMallOverview = {
|
|||
banners: [{ id: 'b1', title: '本期活动', subtitle: 'Up to 25% Off', imageUrl: '', linkUrl: '' }],
|
||||
promoEntries: [
|
||||
{ id: 'p1', title: '促销活动', subtitle: '本周精选', linkUrl: '' },
|
||||
{ id: 'p2', title: 'Token任务', subtitle: '快速涨Token', linkUrl: '' }
|
||||
{ id: 'p2', title: '积分任务', subtitle: '快速涨积分', linkUrl: '' }
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
.points-mall-page-h5 .h5-points-mall-products-card .ant-card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
background:
|
||||
radial-gradient(circle at 28% 24%, rgba(30, 134, 255, 0.22), transparent 34%),
|
||||
linear-gradient(135deg, rgba(17, 103, 255, 0.14), rgba(104, 185, 255, 0.12));
|
||||
}
|
||||
|
||||
.h5-product-tile-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile-tag {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
max-width: calc(100% - 0.5rem);
|
||||
margin: 0;
|
||||
padding: 0 0.3125rem;
|
||||
font-size: 0.625rem;
|
||||
line-height: 1.25rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .points-mall-product-body {
|
||||
padding: 0.4375rem;
|
||||
gap: 0.3125rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .points-mall-product-name {
|
||||
min-height: 2.125rem;
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.38;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .h5-points-mall-product-price-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 1.875rem;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .h5-points-mall-product-price-row > div {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .points-mall-product-price {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .points-mall-product-price-label {
|
||||
margin-left: 0.125rem;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-product-tile .points-mall-product-exchange-btn {
|
||||
width: 1.875rem;
|
||||
min-width: 1.875rem;
|
||||
height: 1.625rem;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
.points-mall-page-h5 .h5-points-mall-products-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,12 @@
|
|||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-hero {
|
||||
padding: 1rem 0.75rem;
|
||||
padding: 0.875rem 0.75rem;
|
||||
border-radius: 1.125rem;
|
||||
background: var(--gradient-hero);
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-header,
|
||||
.points-mall-page-h5 .h5-points-balance-row,
|
||||
.points-mall-page-h5 .h5-points-mall-banner-header,
|
||||
.points-mall-page-h5 .h5-points-mall-product-price-row {
|
||||
flex-direction: column;
|
||||
|
|
@ -23,28 +22,27 @@
|
|||
}
|
||||
|
||||
.points-mall-page-h5 .h5-page-title {
|
||||
margin-bottom: 0.375rem;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-page-subtitle {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-balance-card,
|
||||
.points-mall-page-h5 .points-balance-main,
|
||||
.points-mall-page-h5 .points-mall-banner-action,
|
||||
.points-mall-page-h5 .points-mall-product-exchange-btn {
|
||||
.points-mall-page-h5 .points-mall-banner-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-balance-card,
|
||||
.points-mall-page-h5 .h5-points-mall-product-card {
|
||||
padding: 0.75rem;
|
||||
.points-mall-page-h5 .h5-points-mall-balance-card {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-balance-row {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +55,7 @@
|
|||
}
|
||||
|
||||
.points-mall-page-h5 .points-balance-value {
|
||||
font-size: 1.75rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.points-mall-level-tag {
|
||||
|
|
@ -73,34 +71,41 @@
|
|||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-category-row {
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.375rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-category-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-category-label {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.points-mall-category-button {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-banner-section,
|
||||
.points-mall-page-h5 .h5-points-mall-promo-grid,
|
||||
.points-mall-page-h5 .h5-points-mall-products-grid {
|
||||
.points-mall-page-h5 .h5-points-mall-promo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-banner-card {
|
||||
width: 100%;
|
||||
min-height: 10rem;
|
||||
padding: 1rem;
|
||||
min-height: auto;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-banner-header,
|
||||
.points-mall-page-h5 .h5-points-mall-product-price-row {
|
||||
.points-mall-page-h5 .h5-points-mall-banner-header {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +142,7 @@
|
|||
|
||||
.points-mall-page-h5 .h5-points-mall-filters-row {
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.points-mall-filter-search,
|
||||
|
|
@ -150,7 +155,7 @@
|
|||
|
||||
.points-mall-filter-selects {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) 5.875rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -175,26 +180,6 @@
|
|||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-products-grid {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .points-mall-product-name {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .points-mall-product-desc {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .points-mall-product-tag {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .points-mall-product-price {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.points-mall-page-h5 .h5-points-mall-pagination {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function StatsPageWeb({ logic }: { logic: StatsPageLogic }) {
|
|||
<h1 className="page-title stats-page-title">调用统计</h1>
|
||||
<p className="page-subtitle stats-page-subtitle">
|
||||
不只是查看调用数量,而是帮助你感知哪些智能体正在被频繁使用、最近的消息趋势如何,以及整体会话是否健康增长。
|
||||
每消费 1 美元可获得 1000 积分。
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-page-summary-card">
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ export default function StatsPointsCard({ logic }: { logic: StatsPageLogic }) {
|
|||
return (
|
||||
<div className="points-integration-section">
|
||||
<Card
|
||||
title="消费与Token"
|
||||
title="消费与积分"
|
||||
className="points-integration-card"
|
||||
extra={<Tag bordered={false} className="stats-soft-tag">1 USD = 1000 Token</Tag>}
|
||||
extra={<Tag bordered={false} className="stats-soft-tag">1 USD = 1000 积分</Tag>}
|
||||
>
|
||||
<div className="points-integration-row">
|
||||
<div className="points-integration-item">
|
||||
|
|
@ -22,7 +22,7 @@ export default function StatsPointsCard({ logic }: { logic: StatsPageLogic }) {
|
|||
<div className="points-integration-item">
|
||||
<div className="points-integration-label">
|
||||
<GiftOutlined className="stats-tone-brand-text" />
|
||||
累计Token
|
||||
累计积分
|
||||
</div>
|
||||
<div className="points-integration-value">{logic.formatPoints(logic.totalPoints)}</div>
|
||||
<div className="points-integration-desc">可用于兑换商城商品</div>
|
||||
|
|
@ -30,10 +30,10 @@ export default function StatsPointsCard({ logic }: { logic: StatsPageLogic }) {
|
|||
<div className="points-integration-item">
|
||||
<div className="points-integration-label">
|
||||
<BarChartOutlined className="stats-tone-success-text" />
|
||||
Token兑换汇率
|
||||
积分汇率
|
||||
</div>
|
||||
{/* <div className="points-integration-value">100:1</div> */}
|
||||
{/* <div className="points-integration-desc">每消费 100 token 获得 1 Token</div> */}
|
||||
<div className="points-integration-value">1:1000</div>
|
||||
<div className="points-integration-desc">每消费 1 美元获得 1000 积分</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@
|
|||
}
|
||||
|
||||
.stats-page-web .stats-page-title {
|
||||
margin-bottom: 0.625rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-page-web .stats-page-subtitle {
|
||||
margin-top: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.stats-page-web .stats-page-main-grid {
|
||||
margin-top: 1.125rem;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.stats-page-web .stats-page-card-icon {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
}
|
||||
|
||||
.workflows-hero {
|
||||
border-radius: 1.5rem;
|
||||
padding: 1.875rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 24px;
|
||||
padding: 30px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--gradient-hero);
|
||||
}
|
||||
|
||||
|
|
@ -13,78 +13,78 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.workflows-hero-copy {
|
||||
max-width: 42.5rem;
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.workflows-title {
|
||||
margin: 1rem 0 0.625rem;
|
||||
margin: 16px 0 10px;
|
||||
}
|
||||
|
||||
.workflows-subtitle {
|
||||
margin-top: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.workflows-create-btn {
|
||||
height: 2.875rem;
|
||||
padding: 0 1.125rem;
|
||||
height: 46px;
|
||||
padding: 0 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflows-state-spin {
|
||||
display: block;
|
||||
margin-top: 2rem;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.workflows-empty-card {
|
||||
border-radius: 1.375rem;
|
||||
padding: 3.375rem 1.5rem;
|
||||
border-radius: 22px;
|
||||
padding: 54px 24px;
|
||||
}
|
||||
|
||||
.workflows-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(21.25rem, 1fr));
|
||||
gap: 1.125rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.workflow-card-schedule {
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(180deg, rgba(248, 251, 255, 0.92), rgba(255, 255, 255, 0.96));
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workflow-card-schedule-label {
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
margin-bottom: 0.625rem;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workflow-card-schedule-time {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 0.78125rem;
|
||||
margin-top: 0.625rem;
|
||||
font-size: 12.5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.workflow-card-actions {
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 1rem;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.workflow-action-primary,
|
||||
.workflow-action-secondary {
|
||||
height: 2.5rem;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.workflow-action-primary {
|
||||
|
|
|
|||
|
|
@ -1960,9 +1960,6 @@ span.mention {
|
|||
.points-mall-product-cover {
|
||||
height: 156px;
|
||||
background: linear-gradient(135deg, rgba(8,145,178,0.12) 0%, rgba(59,130,246,0.10) 55%, rgba(34,197,94,0.10) 100%);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.points-mall-product-body {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
padding: 0 0.625rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(18px);
|
||||
backdrop-filter: blur(1.125rem);
|
||||
}
|
||||
|
||||
.mobile-topbar-title {
|
||||
|
|
@ -47,24 +47,24 @@
|
|||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
background: var(--color-surface-2);
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-search-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.sidebar-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-right: -4px;
|
||||
padding-right: 4px;
|
||||
margin-right: -0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
margin-top: 8px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-user-avatar {
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
|
||||
.sidebar-user-name {
|
||||
font-size: 13px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
|
|
@ -82,6 +82,6 @@
|
|||
}
|
||||
|
||||
.sidebar-user-role {
|
||||
font-size: 11px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue