Compare commits
20 Commits
368c0d75e7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90a51ebf4 | ||
|
|
5b239c13b7 | ||
|
|
8397a51f8c | ||
|
|
a9fbbea1e6 | ||
|
|
6756ad5700 | ||
|
|
f581ccaa9b | ||
|
|
832c2852c7 | ||
|
|
3e81a216ad | ||
|
|
85cdeac804 | ||
|
|
59e4e1203e | ||
|
|
4dc86889c5 | ||
| 60fa91ea25 | |||
|
|
50e4be4976 | ||
|
|
5d78f24366 | ||
|
|
069eab0462 | ||
|
|
c265dacafd | ||
|
|
83c32279eb | ||
|
|
169d2bdaad | ||
|
|
427b211295 | ||
|
|
874ff41ce3 |
@@ -2,7 +2,8 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(npm list:*)"
|
||||
"Bash(npm list:*)",
|
||||
"Bash(find . -type f \\\\\\(-name *.openapi.* -o -name *-spec.yaml -o -name *-spec.yml \\\\\\))"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_BASE_URL=https://cmp-api.boss160.cn
|
||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_BASE_URL=https://cmp-api.xm-iot.cn
|
||||
@@ -58,6 +58,7 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /build/dist/build/h5 /usr/share/nginx/html
|
||||
COPY MP_verify_yU2Z8mp831jh6QX7.txt /usr/share/nginx/html/MP_verify_yU2Z8mp831jh6QX7.txt
|
||||
COPY MP_verify_0UoX8yClVgREjgPj.txt /usr/share/nginx/html/MP_verify_0UoX8yClVgREjgPj.txt
|
||||
COPY MP_verify_4E1dVbOcZ9KzTzoc.txt /usr/share/nginx/html/MP_verify_4E1dVbOcZ9KzTzoc.txt
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
@@ -21,19 +21,26 @@ export const orderApi = {
|
||||
});
|
||||
},
|
||||
|
||||
create(identifier, package_ids) {
|
||||
create(identifier, package_ids, payment_method) {
|
||||
const data = { identifier, package_ids };
|
||||
if (payment_method === 'alipay') {
|
||||
data.payment_method = payment_method;
|
||||
} else {
|
||||
data.app_type = APP_TYPE;
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/api/c/v1/orders/create',
|
||||
method: 'POST',
|
||||
data: { identifier, package_ids, app_type: APP_TYPE }
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
pay(order_id, payment_method, password) {
|
||||
const data = {
|
||||
payment_method,
|
||||
app_type: APP_TYPE
|
||||
};
|
||||
const data = { payment_method };
|
||||
if (payment_method === 'wechat') {
|
||||
data.app_type = APP_TYPE;
|
||||
}
|
||||
if (password) {
|
||||
data.password = password;
|
||||
}
|
||||
@@ -43,4 +50,4 @@ export const orderApi = {
|
||||
data
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from '@/utils/request.js';
|
||||
|
||||
export const realnameApi = {
|
||||
getLink(identifier, iccid) {
|
||||
getLink(identifier, iccid, options = {}) {
|
||||
return request({
|
||||
url: '/api/c/v1/realname/link',
|
||||
method: 'GET',
|
||||
data: { identifier, iccid }
|
||||
data: { identifier, iccid },
|
||||
...options
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,10 +11,15 @@ export const walletApi = {
|
||||
},
|
||||
|
||||
recharge(identifier, amount, payment_method) {
|
||||
const data = { identifier, amount, payment_method };
|
||||
if (payment_method === 'wechat') {
|
||||
data.app_type = APP_TYPE;
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/api/c/v1/wallet/recharge',
|
||||
method: 'POST',
|
||||
data: { identifier, amount, payment_method, app_type: APP_TYPE }
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
@@ -59,4 +64,4 @@ export const walletApi = {
|
||||
data: { identifier, page, page_size, ...params }
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -86,17 +86,17 @@
|
||||
</view>
|
||||
<view class="function-name">{{ walletText }}</view>
|
||||
</view>
|
||||
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'recover')"
|
||||
role="button" tabindex="0">
|
||||
<view class="function-icon">
|
||||
<image src="/static/recover.png" mode="aspectFit" alt="重启设备"></image>
|
||||
</view>
|
||||
<view class="function-name">重启设备</view>
|
||||
</view>
|
||||
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'restart')"
|
||||
role="button" tabindex="0">
|
||||
<view class="function-icon">
|
||||
<image src="/static/restart.png" mode="aspectFit" alt="恢复出厂"></image>
|
||||
<image src="/static/restart.png" mode="aspectFit" alt="重启设备"></image>
|
||||
</view>
|
||||
<view class="function-name">重启设备</view>
|
||||
</view>
|
||||
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'recover')"
|
||||
role="button" tabindex="0">
|
||||
<view class="function-icon">
|
||||
<image src="/static/recover.png" mode="aspectFit" alt="恢复出厂"></image>
|
||||
</view>
|
||||
<view class="function-name">恢复出厂</view>
|
||||
</view>
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
<view class="flex-row-g20">
|
||||
<view class="title">{{ currentCardNo || '-' }}</view>
|
||||
</view>
|
||||
<view class="caption">ICCID: {{ deviceInfo.iccid || '-' }}</view>
|
||||
<view class="iccid-row caption">
|
||||
<text>ICCID: {{ deviceInfo.iccid || '-' }}</text>
|
||||
<button
|
||||
v-if="deviceInfo.iccid && deviceInfo.iccid !== '-'"
|
||||
class="copy-btn"
|
||||
@tap.stop="$emit('copy', deviceInfo.iccid)"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</view>
|
||||
<view class="caption">套餐名称:{{ deviceInfo.packageName || '-' }}</view>
|
||||
<view class="caption">套餐到期时间:{{ deviceInfo.expireDate || '-' }}</view>
|
||||
</view>
|
||||
@@ -27,6 +36,8 @@
|
||||
networkStatus: { type: [String, Number], default: '离线' },
|
||||
isDevice: { type: Boolean, default: true }
|
||||
});
|
||||
|
||||
defineEmits(['copy']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -36,5 +47,29 @@
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.iccid-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
margin: 0;
|
||||
padding: 0 12rpx;
|
||||
min-height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
border: none;
|
||||
border-radius: 6rpx;
|
||||
background: rgba(10, 132, 255, 0.12);
|
||||
color: var(--primary);
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copy-btn::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,8 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
alias /usr/share/nginx/html/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
|
||||
436
docs/换货.md
Normal file
436
docs/换货.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# 换货流程升级接口变更说明
|
||||
|
||||
## 一、数据库字段新增
|
||||
|
||||
### 表:`tb_exchange_order`
|
||||
|
||||
新增以下字段,所有换货单相关接口响应均会返回:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------ | ------------- | ---------------------------------------------------------- |
|
||||
| flow_type | string | 流程类型:`shipping`(物流换货)或 `direct`(直接换货),历史数据为空时视为 `shipping` |
|
||||
| shipped_at | string | null | 发货时间,仅 `shipping` 流程发货后有值 |
|
||||
| completed_at | string | null | 换货完成时间,换货完成后有值 |
|
||||
|
||||
---
|
||||
|
||||
# 二、后台接口变更
|
||||
|
||||
## 1. 创建换货单
|
||||
|
||||
### POST /api/admin/exchanges
|
||||
|
||||
### 新增请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"old_asset_type": "iot_card",
|
||||
"old_identifier": "xxx",
|
||||
"exchange_reason": "设备损坏",
|
||||
"flow_type": "shipping",
|
||||
"new_identifier": "yyy",
|
||||
"migrate_data": false,
|
||||
"remark": null
|
||||
}
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------------- | ---------- | ---------------------------------------- |
|
||||
| flow_type | string | 否 | 流程类型:`shipping` / `direct`,默认 `shipping` |
|
||||
| new_identifier | string | direct 时必填 | 新资产标识符 |
|
||||
| migrate_data | boolean | 否 | 是否迁移数据,默认 false |
|
||||
| remark | string | null | 否 | 备注 |
|
||||
|
||||
### 不同流程行为差异
|
||||
|
||||
| flow_type | 创建后状态(status) | completed_at |
|
||||
| --------- | ------------- | ------------ |
|
||||
| shipping | 1(待填写信息) | null |
|
||||
| direct | 4(已完成) | 当前时间 |
|
||||
|
||||
### 说明
|
||||
|
||||
* shipping 为原有流程
|
||||
* direct 创建后直接完成
|
||||
* direct 不需要填写地址
|
||||
* direct 不需要发货
|
||||
* direct 不需要确认完成
|
||||
* 非法 flow_type 返回参数错误
|
||||
|
||||
---
|
||||
|
||||
## 2. 换货单列表
|
||||
|
||||
### GET /api/admin/exchanges
|
||||
|
||||
### 新增查询参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---------- | ------ | ------------------------ |
|
||||
| flow_type | string | 按流程类型筛选(shipping/direct) |
|
||||
| identifier | string | 资产标识符模糊查询(同时匹配旧资产、新资产) |
|
||||
|
||||
### 响应新增字段
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_type": "shipping",
|
||||
"flow_type_name": "物流换货",
|
||||
"shipped_at": null,
|
||||
"completed_at": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 换货单详情
|
||||
|
||||
### GET /api/admin/exchanges/:id
|
||||
|
||||
### 响应新增字段
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_type": "shipping",
|
||||
"flow_type_name": "物流换货",
|
||||
"shipped_at": null,
|
||||
"completed_at": null
|
||||
}
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
对于 direct 类型:
|
||||
|
||||
* 收货信息为空
|
||||
* 物流信息为空
|
||||
|
||||
属于正常情况,前端需根据 flow_type 控制相关区块显示。
|
||||
|
||||
---
|
||||
|
||||
## 4. 发货
|
||||
|
||||
### POST /api/admin/exchanges/:id/ship
|
||||
|
||||
### 新增限制
|
||||
|
||||
仅支持:
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
```
|
||||
|
||||
### 不允许情况
|
||||
|
||||
```text
|
||||
flow_type = direct
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "流程类型不支持该操作"
|
||||
}
|
||||
```
|
||||
|
||||
### 前端处理
|
||||
|
||||
```javascript
|
||||
exchange.flow_type === 'shipping'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 确认完成
|
||||
|
||||
### POST /api/admin/exchanges/:id/complete
|
||||
|
||||
### 新增限制
|
||||
|
||||
仅支持:
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
status = 3
|
||||
```
|
||||
|
||||
### 不允许情况
|
||||
|
||||
```text
|
||||
flow_type = direct
|
||||
```
|
||||
|
||||
返回错误。
|
||||
|
||||
---
|
||||
|
||||
## 6. 取消换货
|
||||
|
||||
### POST /api/admin/exchanges/:id/cancel
|
||||
|
||||
### 新增限制
|
||||
|
||||
允许取消:
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
status IN (1, 2)
|
||||
```
|
||||
|
||||
### 不允许取消
|
||||
|
||||
#### direct 单据
|
||||
|
||||
```text
|
||||
flow_type = direct
|
||||
```
|
||||
|
||||
#### 已发货单据
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
status = 3
|
||||
```
|
||||
|
||||
返回错误。
|
||||
|
||||
---
|
||||
|
||||
# 三、客户端接口变更
|
||||
|
||||
## 1. 查询进行中的换货单
|
||||
|
||||
### GET /api/c/v1/exchange/pending
|
||||
|
||||
### 变更后返回规则
|
||||
|
||||
仅返回:
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
status IN (1, 2, 3)
|
||||
```
|
||||
|
||||
### 不返回
|
||||
|
||||
```text
|
||||
flow_type = direct
|
||||
```
|
||||
|
||||
客户端无需特殊处理。
|
||||
|
||||
---
|
||||
|
||||
## 2. 填写收货地址
|
||||
|
||||
### POST /api/c/v1/exchange/:id/shipping-info
|
||||
|
||||
### 新增限制
|
||||
|
||||
仅支持:
|
||||
|
||||
```text
|
||||
flow_type = shipping
|
||||
status = 1
|
||||
```
|
||||
|
||||
### 不允许
|
||||
|
||||
```text
|
||||
flow_type = direct
|
||||
```
|
||||
|
||||
返回错误。
|
||||
|
||||
---
|
||||
|
||||
# 四、前端 UI 改造建议
|
||||
|
||||
## 换货单详情页
|
||||
|
||||
### 新增展示字段
|
||||
|
||||
* 流程类型(flow_type_name)
|
||||
* 完成时间(completed_at)
|
||||
* 发货时间(shipped_at)
|
||||
|
||||
### 按流程类型控制按钮
|
||||
|
||||
#### direct
|
||||
|
||||
不显示:
|
||||
|
||||
* 发货
|
||||
* 确认完成
|
||||
* 取消
|
||||
|
||||
#### shipping + status = 1
|
||||
|
||||
显示:
|
||||
|
||||
* 取消
|
||||
|
||||
#### shipping + status = 2
|
||||
|
||||
显示:
|
||||
|
||||
* 发货
|
||||
* 取消
|
||||
|
||||
#### shipping + status = 3
|
||||
|
||||
显示:
|
||||
|
||||
* 确认完成
|
||||
|
||||
### 信息区块控制
|
||||
|
||||
#### direct
|
||||
|
||||
隐藏:
|
||||
|
||||
* 收货信息
|
||||
* 物流信息
|
||||
|
||||
或者显示:
|
||||
|
||||
```text
|
||||
不适用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 创建换货单弹窗
|
||||
|
||||
### 新增字段
|
||||
|
||||
流程类型:
|
||||
|
||||
* 物流换货(shipping)
|
||||
* 直接换货(direct)
|
||||
|
||||
### 选择 direct 时
|
||||
|
||||
显示:
|
||||
|
||||
* 新资产标识符(必填)
|
||||
* 是否迁移数据(开关)
|
||||
|
||||
对应参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"new_identifier": "xxx",
|
||||
"migrate_data": true
|
||||
}
|
||||
```
|
||||
|
||||
### 选择 shipping 时
|
||||
|
||||
隐藏:
|
||||
|
||||
* 新资产标识符
|
||||
* 是否迁移数据
|
||||
|
||||
---
|
||||
|
||||
## 换货单列表页
|
||||
|
||||
### 新增表格列
|
||||
|
||||
```text
|
||||
流程类型
|
||||
```
|
||||
|
||||
显示:
|
||||
|
||||
```text
|
||||
物流换货
|
||||
直接换货
|
||||
```
|
||||
|
||||
### 新增筛选项
|
||||
|
||||
```text
|
||||
流程类型
|
||||
```
|
||||
|
||||
下拉选项:
|
||||
|
||||
```text
|
||||
全部
|
||||
物流换货(shipping)
|
||||
直接换货(direct)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 五、流程状态说明
|
||||
|
||||
## shipping(物流换货)
|
||||
|
||||
```text
|
||||
创建
|
||||
↓
|
||||
待填写信息(1)
|
||||
↓
|
||||
待发货(2)
|
||||
↓
|
||||
已发货(3)
|
||||
↓
|
||||
已完成(4)
|
||||
```
|
||||
|
||||
## direct(直接换货)
|
||||
|
||||
```text
|
||||
创建
|
||||
↓
|
||||
已完成(4)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 六、状态与操作矩阵
|
||||
|
||||
| flow_type | status | 发货 | 确认完成 | 取消 |
|
||||
| --------- | ------ | -- | ---- | -- |
|
||||
| shipping | 1 | ❌ | ❌ | ✅ |
|
||||
| shipping | 2 | ✅ | ❌ | ✅ |
|
||||
| shipping | 3 | ❌ | ✅ | ❌ |
|
||||
| shipping | 4 | ❌ | ❌ | ❌ |
|
||||
| direct | 4 | ❌ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
# 七、枚举定义
|
||||
|
||||
## flow_type
|
||||
|
||||
```json
|
||||
{
|
||||
"shipping": "物流换货",
|
||||
"direct": "直接换货"
|
||||
}
|
||||
```
|
||||
|
||||
## flow_type_name 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_type": "shipping",
|
||||
"flow_type_name": "物流换货"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_type": "direct",
|
||||
"flow_type_name": "直接换货"
|
||||
}
|
||||
```
|
||||
307
docs/新增支付宝支付.md
Normal file
307
docs/新增支付宝支付.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 支付宝支付链接前端对接说明
|
||||
|
||||
## 1. 对接范围
|
||||
|
||||
本次新增的是 **C 端支付宝支付链接能力**。前端无需接入支付宝 SDK,后端会返回已签名的支付宝 WAP 支付链接。
|
||||
|
||||
覆盖场景:
|
||||
|
||||
1. 套餐订单支付
|
||||
2. 钱包充值
|
||||
3. 强制充值购包场景
|
||||
|
||||
微信支付原逻辑保持不变:微信返回 `pay_config`,支付宝返回 `payment_link`。
|
||||
|
||||
具体接口路径、完整请求字段、响应结构、错误码请以前端接口文档 / OpenAPI 文档为准。
|
||||
|
||||
## 2. 支付方式取值
|
||||
|
||||
前端统一使用 `payment_method` 区分支付方式:
|
||||
|
||||
| 值 | 含义 | 前端处理 |
|
||||
| --- | --- | --- |
|
||||
| `wallet` | 钱包支付 | 仅订单支付支持,无第三方跳转 |
|
||||
| `wechat` | 微信支付 | 使用返回的 `pay_config` 调起微信支付 |
|
||||
| `alipay` | 支付宝支付 | 使用返回的 `payment_link` 打开或展示支付链接 |
|
||||
|
||||
注意事项:
|
||||
|
||||
- `app_type` 只在微信支付时需要。
|
||||
- 支付宝支付时不需要传 `app_type`。
|
||||
- 支付宝支付返回的是支付链接,不是 JSAPI 参数。
|
||||
|
||||
## 3. 前端需要关注的响应字段
|
||||
|
||||
当选择支付宝支付时,接口响应中会返回 `payment_link`:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_link": {
|
||||
"payment_no": "支付单号",
|
||||
"qr_link": "支付宝支付链接,可用于生成二维码",
|
||||
"copy_link": "支付宝支付链接,可复制或直接打开",
|
||||
"pay_expire_at": "支付链接过期时间,RFC3339 格式"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 用途 |
|
||||
| --- | --- |
|
||||
| `payment_no` | 支付单号,问题排查 / 客服查询使用 |
|
||||
| `qr_link` | 前端可用该链接生成二维码 |
|
||||
| `copy_link` | 用户复制或浏览器打开的支付链接,当前与 `qr_link` 一致 |
|
||||
| `pay_expire_at` | 支付链接过期时间,前端可用于倒计时展示 |
|
||||
|
||||
## 4. 推荐交互流程
|
||||
|
||||
### 4.1 支付宝支付流程
|
||||
|
||||
前端拿到 `payment_link` 后:
|
||||
|
||||
1. H5 / 浏览器场景:直接跳转 `copy_link`。
|
||||
|
||||
支付成功后:
|
||||
|
||||
- 不要只依赖支付宝返回页判断支付成功。
|
||||
- 最终支付状态以服务端订单 / 充值记录状态为准。
|
||||
- 用户返回页面后,前端应轮询订单详情、订单列表或充值记录接口。
|
||||
|
||||
## 5. 各业务场景说明
|
||||
|
||||
### 5.1 套餐订单支付
|
||||
|
||||
普通套餐下单后,如果需要支付,前端调用订单支付接口,并传:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
支付宝支付响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay",
|
||||
"payment_link": {
|
||||
"payment_no": "...",
|
||||
"qr_link": "...",
|
||||
"copy_link": "...",
|
||||
"pay_expire_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端根据 `payment_link` 发起支付宝支付。
|
||||
|
||||
### 5.2 钱包充值
|
||||
|
||||
创建钱包充值单时传:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
支付宝场景返回 `payment_link`;微信场景返回 `pay_config`。
|
||||
|
||||
### 5.3 强制充值购包
|
||||
|
||||
创建订单时,如果后端判断需要强制充值,前端可指定:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
如果是支付宝强充,响应中会返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"order_type": "recharge",
|
||||
"recharge": {},
|
||||
"payment_link": {}
|
||||
}
|
||||
```
|
||||
|
||||
前端按支付宝支付链接流程处理。
|
||||
|
||||
## 6. 前端展示建议
|
||||
|
||||
支付宝支付页面建议展示:
|
||||
|
||||
- 支付金额
|
||||
- 支付宝二维码或“去支付宝支付”按钮
|
||||
- 支付链接过期倒计时
|
||||
- “我已完成支付”按钮
|
||||
- “重新获取支付链接”按钮
|
||||
|
||||
点击“我已完成支付”时,不直接判定成功,应查询服务端状态。
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. 金额单位仍然是 **分**。
|
||||
2. 支付宝支付不需要 `app_type`。
|
||||
3. `pay_config` 只用于微信支付。
|
||||
4. `payment_link` 只用于支付宝支付。
|
||||
5. 支付状态以服务端为准,不以前端跳转结果为准。
|
||||
6. 支付链接过期后,前端应重新调用对应支付 / 充值接口获取新链接。
|
||||
7. 具体接口路径、完整字段、错误码请以前端接口文档 / OpenAPI 文档为准。
|
||||
|
||||
# 支付宝支付链接前端对接说明
|
||||
|
||||
## 1. 对接范围
|
||||
|
||||
本次新增的是 **C 端支付宝支付链接能力**。前端无需接入支付宝 SDK,后端会返回已签名的支付宝 WAP 支付链接。
|
||||
|
||||
覆盖场景:
|
||||
|
||||
1. 套餐订单支付
|
||||
2. 钱包充值
|
||||
3. 强制充值购包场景
|
||||
|
||||
微信支付原逻辑保持不变:微信返回 `pay_config`,支付宝返回 `payment_link`。
|
||||
|
||||
具体接口路径、完整请求字段、响应结构、错误码请以前端接口文档 / OpenAPI 文档为准。
|
||||
|
||||
## 2. 支付方式取值
|
||||
|
||||
前端统一使用 `payment_method` 区分支付方式:
|
||||
|
||||
| 值 | 含义 | 前端处理 |
|
||||
| --- | --- | --- |
|
||||
| `wallet` | 钱包支付 | 仅订单支付支持,无第三方跳转 |
|
||||
| `wechat` | 微信支付 | 使用返回的 `pay_config` 调起微信支付 |
|
||||
| `alipay` | 支付宝支付 | 使用返回的 `payment_link` 打开或展示支付链接 |
|
||||
|
||||
注意事项:
|
||||
|
||||
- `app_type` 只在微信支付时需要。
|
||||
- 支付宝支付时不需要传 `app_type`。
|
||||
- 支付宝支付返回的是支付链接,不是 JSAPI 参数。
|
||||
|
||||
## 3. 前端需要关注的响应字段
|
||||
|
||||
当选择支付宝支付时,接口响应中会返回 `payment_link`:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_link": {
|
||||
"payment_no": "支付单号",
|
||||
"qr_link": "支付宝支付链接,可用于生成二维码",
|
||||
"copy_link": "支付宝支付链接,可复制或直接打开",
|
||||
"pay_expire_at": "支付链接过期时间,RFC3339 格式"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 用途 |
|
||||
| --- | --- |
|
||||
| `payment_no` | 支付单号,问题排查 / 客服查询使用 |
|
||||
| `qr_link` | 前端可用该链接生成二维码 |
|
||||
| `copy_link` | 用户复制或浏览器打开的支付链接,当前与 `qr_link` 一致 |
|
||||
| `pay_expire_at` | 支付链接过期时间,前端可用于倒计时展示 |
|
||||
|
||||
## 4. 推荐交互流程
|
||||
|
||||
### 4.1 支付宝支付流程
|
||||
|
||||
前端拿到 `payment_link` 后:
|
||||
|
||||
1. H5 / 浏览器场景:直接跳转 `copy_link`。
|
||||
|
||||
支付成功后:
|
||||
|
||||
- 不要只依赖支付宝返回页判断支付成功。
|
||||
- 最终支付状态以服务端订单 / 充值记录状态为准。
|
||||
- 用户返回页面后,前端应轮询订单详情、订单列表或充值记录接口。
|
||||
|
||||
## 5. 各业务场景说明
|
||||
|
||||
### 5.1 套餐订单支付
|
||||
|
||||
普通套餐下单后,如果需要支付,前端调用订单支付接口,并传:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
支付宝支付响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay",
|
||||
"payment_link": {
|
||||
"payment_no": "...",
|
||||
"qr_link": "...",
|
||||
"copy_link": "...",
|
||||
"pay_expire_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端根据 `payment_link` 发起支付宝支付。
|
||||
|
||||
### 5.2 钱包充值
|
||||
|
||||
创建钱包充值单时传:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
支付宝场景返回 `payment_link`;微信场景返回 `pay_config`。
|
||||
|
||||
### 5.3 强制充值购包
|
||||
|
||||
创建订单时,如果后端判断需要强制充值,前端可指定:
|
||||
|
||||
```json
|
||||
{
|
||||
"payment_method": "alipay"
|
||||
}
|
||||
```
|
||||
|
||||
如果是支付宝强充,响应中会返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"order_type": "recharge",
|
||||
"recharge": {},
|
||||
"payment_link": {}
|
||||
}
|
||||
```
|
||||
|
||||
前端按支付宝支付链接流程处理。
|
||||
|
||||
## 6. 前端展示建议
|
||||
|
||||
支付宝支付页面建议展示:
|
||||
|
||||
- 支付金额
|
||||
- 支付宝二维码或“去支付宝支付”按钮
|
||||
- 支付链接过期倒计时
|
||||
- “我已完成支付”按钮
|
||||
- “重新获取支付链接”按钮
|
||||
|
||||
点击“我已完成支付”时,不直接判定成功,应查询服务端状态。
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. 金额单位仍然是 **分**。
|
||||
2. 支付宝支付不需要 `app_type`。
|
||||
3. `pay_config` 只用于微信支付。
|
||||
4. `payment_link` 只用于支付宝支付。
|
||||
5. 支付状态以服务端为准,不以前端跳转结果为准。
|
||||
6. 支付链接过期后,前端应重新调用对应支付 / 充值接口获取新链接。
|
||||
7. 具体接口路径、完整字段、错误码请以前端接口文档 / OpenAPI 文档为准。
|
||||
@@ -6,6 +6,7 @@
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"/>
|
||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
<script>window.__WX_ENTRY_URL__ = window.location.href.split('#')[0]</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
|
||||
25
openspec/changes/add-alipay-payment-link-flow/proposal.md
Normal file
25
openspec/changes/add-alipay-payment-link-flow/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
The front end currently supports WeChat payment and wallet balance payment, but it does not support the new backend Alipay WAP payment-link flow. This prevents users from completing package purchase and wallet recharge scenarios when Alipay is the selected method.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add Alipay as a supported payment method in package purchase, order list immediate payment, and wallet recharge flows
|
||||
- Handle backend `payment_link` responses for Alipay while preserving existing WeChat `pay_config` handling
|
||||
- Keep wallet balance payment behavior unchanged for package purchase
|
||||
- Refresh payment-related page state from backend status after the user returns from the Alipay payment link flow
|
||||
- Preserve current order creation, strong recharge, validation, toast, and list refresh behavior except where Alipay-specific branching is required
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `alipay-payment-link`: Support backend-provided Alipay payment links across package, order, and wallet payment entry points
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `pages/package-order/package-order.vue`, `pages/order-list/order-list.vue`, `pages/my-wallet/my-wallet.vue`, `utils/payment.js`, `api/modules/wallet.js`
|
||||
- Affected UI flows: package payment method popup, order list immediate payment, wallet recharge popup, wallet recharge order immediate payment
|
||||
- No backend API path changes expected
|
||||
- Existing WeChat and wallet payment behavior must remain available
|
||||
@@ -0,0 +1,58 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Package payment SHALL support Alipay payment links
|
||||
The system SHALL allow users to choose Alipay during package purchase and SHALL handle backend Alipay responses through `payment_link` while preserving the existing WeChat and wallet payment branches.
|
||||
|
||||
#### Scenario: User pays a package order with Alipay
|
||||
- **WHEN** the user selects `alipay` in the package payment popup and the backend returns `payment_method: "alipay"` with `payment_link`
|
||||
- **THEN** the system SHALL open or present the returned Alipay payment link for the user
|
||||
- **AND** the system SHALL NOT require WeChat `pay_config` fields for this branch
|
||||
|
||||
#### Scenario: User pays a package order with wallet balance
|
||||
- **WHEN** the user selects `wallet` and the backend completes the package payment without a third-party redirect
|
||||
- **THEN** the system SHALL preserve the existing wallet payment success flow
|
||||
- **AND** the system SHALL refresh relevant wallet-related page state after success
|
||||
|
||||
#### Scenario: Strong recharge branch returns an Alipay payment link
|
||||
- **WHEN** package order creation enters the strong recharge branch and the backend returns `order_type: "recharge"` with `payment_link`
|
||||
- **THEN** the system SHALL use the Alipay payment-link flow for that recharge branch
|
||||
- **AND** the system SHALL preserve the existing strong recharge messaging and follow-up refresh behavior
|
||||
|
||||
### Requirement: Pending order payment SHALL support Alipay payment links
|
||||
The system SHALL allow users to continue payment for pending package orders from the order list through an Alipay payment link flow.
|
||||
|
||||
#### Scenario: User continues a pending order with Alipay
|
||||
- **WHEN** the user starts payment for a pending order from the order list and the backend returns `payment_method: "alipay"` with `payment_link`
|
||||
- **THEN** the system SHALL open or present the returned payment link
|
||||
- **AND** the system SHALL refresh the order list from backend status after the user completes or retries the flow
|
||||
|
||||
### Requirement: Wallet recharge flows SHALL support Alipay payment links
|
||||
The system SHALL support Alipay both when creating a new wallet recharge order and when retrying payment for a pending recharge order.
|
||||
|
||||
#### Scenario: User creates a new wallet recharge with Alipay
|
||||
- **WHEN** the user confirms a wallet recharge with `payment_method: "alipay"` and the backend returns `payment_link`
|
||||
- **THEN** the system SHALL open or present the returned payment link
|
||||
- **AND** the system SHALL preserve the existing recharge validation rules and amount checks
|
||||
|
||||
#### Scenario: User retries a pending recharge order with Alipay
|
||||
- **WHEN** the user continues payment for a pending recharge order and the backend returns `payment_link`
|
||||
- **THEN** the system SHALL open or present the returned Alipay payment link
|
||||
- **AND** the system SHALL refresh wallet detail and recharge list state from backend data after the user completes or retries the flow
|
||||
|
||||
### Requirement: Payment method branching SHALL distinguish WeChat and Alipay contracts
|
||||
The system SHALL treat WeChat and Alipay payment preparation responses as different contracts and SHALL determine final payment success from backend order or recharge status instead of the redirect result alone.
|
||||
|
||||
#### Scenario: WeChat payment returns pay_config
|
||||
- **WHEN** the backend returns `payment_method: "wechat"` with `pay_config`
|
||||
- **THEN** the system SHALL continue to use the existing WeChat H5 payment invocation flow
|
||||
- **AND** the system SHALL preserve the current `app_type` behavior for WeChat requests
|
||||
|
||||
#### Scenario: Alipay payment returns payment_link
|
||||
- **WHEN** the backend returns `payment_method: "alipay"` with `payment_link`
|
||||
- **THEN** the system SHALL use the returned link fields instead of expecting `pay_config`
|
||||
- **AND** the system SHALL NOT depend on `app_type` for the Alipay branch
|
||||
|
||||
#### Scenario: User returns after Alipay jump
|
||||
- **WHEN** the user comes back to the application after opening the Alipay payment link
|
||||
- **THEN** the system SHALL refresh the relevant order or recharge data from backend APIs
|
||||
- **AND** the system SHALL determine whether the payment succeeded from refreshed backend status
|
||||
29
openspec/changes/add-alipay-payment-link-flow/tasks.md
Normal file
29
openspec/changes/add-alipay-payment-link-flow/tasks.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## 1. Shared Payment Branching
|
||||
|
||||
- [x] 1.1 Add shared front-end handling for Alipay `payment_link` responses alongside the existing WeChat `pay_config` flow
|
||||
- [x] 1.2 Ensure Alipay requests do not depend on `app_type` and existing WeChat requests continue to work unchanged
|
||||
|
||||
## 2. Package Purchase Flow
|
||||
|
||||
- [x] 2.1 Add Alipay as a selectable payment method in `pages/package-order/package-order.vue`
|
||||
- [x] 2.2 Handle normal package order payment responses for `wechat`, `wallet`, and `alipay`
|
||||
- [x] 2.3 Handle strong recharge branches that return `payment_link` for Alipay
|
||||
|
||||
## 3. Order List Flow
|
||||
|
||||
- [x] 3.1 Add an order-list payment entry that lets the user continue pending orders with Alipay
|
||||
- [x] 3.2 Refresh order list state from backend data after the user completes or retries the Alipay flow
|
||||
|
||||
## 4. Wallet Flow
|
||||
|
||||
- [x] 4.1 Add Alipay as a selectable payment method in the wallet recharge popup
|
||||
- [x] 4.2 Handle Alipay recharge creation responses in `confirmRecharge`
|
||||
- [x] 4.3 Handle Alipay payment retries for pending recharge orders in `handleRechargePayment`
|
||||
- [x] 4.4 Refresh wallet detail and recharge list state from backend data after the Alipay flow
|
||||
|
||||
## 5. Regression Verification
|
||||
|
||||
- [x] 5.1 Verify WeChat package payment still uses `pay_config`
|
||||
- [x] 5.2 Verify wallet-balance package payment remains unchanged
|
||||
- [x] 5.3 Verify Alipay package payment, order-list payment, wallet recharge, and recharge retry can all open a valid payment link flow
|
||||
- [x] 5.4 Verify payment success is determined by refreshed backend status instead of the link jump result
|
||||
@@ -0,0 +1,136 @@
|
||||
# Exchange Flow Type Support - Implementation Summary
|
||||
|
||||
## 实施完成 ✅
|
||||
|
||||
### 变更概述
|
||||
|
||||
根据后端换货流程升级,前端已完成对新增字段的支持:
|
||||
- `flow_type`: 流程类型(shipping/direct)
|
||||
- `flow_type_name`: 流程类型名称
|
||||
- `shipped_at`: 发货时间
|
||||
- `completed_at`: 完成时间
|
||||
|
||||
### 已完成的工作
|
||||
|
||||
#### 1. 创建 OpenSpec 文档
|
||||
- ✅ `proposal.md` - 提案说明
|
||||
- ✅ `specs/exchange-flow-type-display/spec.md` - 详细规格
|
||||
- ✅ `tasks.md` - 任务清单
|
||||
|
||||
#### 2. 代码实现
|
||||
**修改文件:** `pages/device-exchange/device-exchange.vue`
|
||||
|
||||
**新增显示内容:**
|
||||
```vue
|
||||
<!-- 换货类型 - 始终显示 -->
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">换货类型</view>
|
||||
<view class="info-value">{{ exchangeData.flow_type_name || '物流换货' }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 发货时间 - 条件显示 -->
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.shipped_at">
|
||||
<view class="info-label">发货时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.shipped_at) }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 完成时间 - 条件显示 -->
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.completed_at && exchangeData.status === 4">
|
||||
<view class="info-label">完成时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.completed_at) }}</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
#### 1. 后端保证的行为
|
||||
- `GET /api/c/v1/exchange/pending` 仅返回 `shipping` 类型的进行中换货单
|
||||
- `direct` 类型创建后立即完成,不会出现在待处理列表
|
||||
- 前端无需额外过滤或判断
|
||||
|
||||
#### 2. 向后兼容
|
||||
- 历史数据没有 `flow_type_name` 时,默认显示"物流换货"
|
||||
- 新增字段都是条件显示,不影响现有数据展示
|
||||
- 原有的按钮和表单逻辑保持不变
|
||||
|
||||
#### 3. 无需修改的部分
|
||||
- ✅ API 模块 (`api/modules/exchange.js`) - 自动透传所有响应字段
|
||||
- ✅ 表单提交逻辑 - 后端负责流程类型校验
|
||||
- ✅ 按钮显示逻辑 - 因为 direct 类型不会进入待处理状态
|
||||
|
||||
### 显示逻辑
|
||||
|
||||
| 字段 | 显示条件 | 默认值 |
|
||||
|------|---------|--------|
|
||||
| 换货类型 | 始终显示 | "物流换货"(当 `flow_type_name` 为空时) |
|
||||
| 发货时间 | `shipped_at` 不为空 | 不显示 |
|
||||
| 完成时间 | `completed_at` 不为空 且 `status === 4` | 不显示 |
|
||||
|
||||
### 测试场景
|
||||
|
||||
需要测试的场景(待后端接口就绪后验证):
|
||||
|
||||
1. **待填写信息** (`status: 1`)
|
||||
- 显示:换货类型、换货原因、申请时间
|
||||
- 显示"填写信息"按钮
|
||||
|
||||
2. **待发货** (`status: 2`)
|
||||
- 显示:换货类型、换货原因、申请时间、收件信息
|
||||
- 不显示发货时间
|
||||
|
||||
3. **已发货** (`status: 3`)
|
||||
- 显示:换货类型、换货原因、申请时间、发货时间、收件信息
|
||||
- 不显示按钮
|
||||
|
||||
4. **已完成** (`status: 4`)
|
||||
- 显示:换货类型、换货原因、申请时间、发货时间、完成时间、收件信息
|
||||
- 不显示按钮
|
||||
|
||||
5. **无待处理换货单**
|
||||
- 显示空状态:"暂无换货记录"
|
||||
|
||||
### API 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"exchange_no": "EXC20260603001",
|
||||
"old_asset_type": "iot_card",
|
||||
"old_identifier": "8986012345678901234",
|
||||
"exchange_reason": "设备损坏",
|
||||
"status": 2,
|
||||
"status_text": "待发货",
|
||||
"flow_type": "shipping",
|
||||
"flow_type_name": "物流换货",
|
||||
"shipped_at": null,
|
||||
"completed_at": null,
|
||||
"created_at": "2026-06-03T10:00:00+08:00",
|
||||
"recipient_name": "张三",
|
||||
"recipient_phone": "13800138000",
|
||||
"recipient_address": "北京市朝阳区xxx路xxx号"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 影响范围
|
||||
|
||||
- **修改文件**: 1 个 (`pages/device-exchange/device-exchange.vue`)
|
||||
- **新增文档**: 3 个(proposal, spec, tasks)
|
||||
- **影响用户**: 使用换货功能的客户端用户
|
||||
- **兼容性**: 完全向后兼容,不影响现有流程
|
||||
|
||||
### 后续工作
|
||||
|
||||
- [ ] 等待后端接口部署
|
||||
- [ ] 在测试环境验证所有测试场景
|
||||
- [ ] 确认不同状态下的显示效果
|
||||
- [ ] 验证历史数据的兼容性
|
||||
|
||||
---
|
||||
|
||||
**实施日期**: 2026-06-03
|
||||
**实施人员**: Claude
|
||||
**状态**: ✅ 代码实现完成,待测试验证
|
||||
@@ -0,0 +1,29 @@
|
||||
## Why
|
||||
|
||||
The backend exchange system now supports two flow types: **shipping** (logistics-based exchange requiring address collection and shipment) and **direct** (immediate completion without logistics). The current frontend client only handles the original shipping flow and does not process the new `flow_type`, `flow_type_name`, `shipped_at`, and `completed_at` response fields.
|
||||
|
||||
This prevents the frontend from correctly displaying exchange records that may include both flow types and from adapting UI behavior based on flow type constraints.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Update the exchange pending query and shipping info submission to handle new response fields: `flow_type`, `flow_type_name`, `shipped_at`, `completed_at`
|
||||
- Preserve existing behavior where the pending exchange API only returns `shipping` type exchanges (status 1/2/3), as `direct` exchanges are completed immediately and never appear in pending state
|
||||
- Display flow type information in the exchange detail view
|
||||
- Ensure shipping info submission validates that the exchange is `flow_type = shipping` and `status = 1` (backend enforces this, frontend should handle error gracefully)
|
||||
- No UI flow changes required since `direct` type exchanges will not appear in the pending list by design
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `exchange-flow-type-display`: Display flow type and completion timestamps in exchange records
|
||||
|
||||
### Modified Capabilities
|
||||
- `exchange-pending-query`: Handle new response fields from `/api/c/v1/exchange/pending`
|
||||
- `exchange-shipping-submission`: Handle new response fields from `/api/c/v1/exchange/{id}/shipping-info`
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `pages/device-exchange/device-exchange.vue`, `api/modules/exchange.js`
|
||||
- Affected API usage: `/api/c/v1/exchange/pending`, `/api/c/v1/exchange/{id}/shipping-info`
|
||||
- No breaking changes: existing shipping flow behavior remains unchanged
|
||||
- Backend ensures `direct` type exchanges are not returned by pending query, so frontend maintains current UX
|
||||
@@ -0,0 +1,195 @@
|
||||
# Exchange Flow Type Display Specification
|
||||
|
||||
## Overview
|
||||
|
||||
Update the exchange display to show the new flow type and timestamp fields returned by the backend exchange APIs.
|
||||
|
||||
## API Response Changes
|
||||
|
||||
### GET /api/c/v1/exchange/pending
|
||||
|
||||
**New Response Fields:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"timestamp": "2026-06-03T10:00:00+08:00",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"exchange_no": "EXC20260603001",
|
||||
"order_no": "EXC20260603001",
|
||||
"old_asset_type": "iot_card",
|
||||
"old_identifier": "8986012345678901234",
|
||||
"exchange_reason": "设备损坏",
|
||||
"status": 2,
|
||||
"status_text": "待发货",
|
||||
"flow_type": "shipping",
|
||||
"flow_type_name": "物流换货",
|
||||
"shipped_at": null,
|
||||
"completed_at": null,
|
||||
"created_at": "2026-06-03T10:00:00+08:00",
|
||||
"recipient_name": "张三",
|
||||
"recipient_phone": "13800138000",
|
||||
"recipient_address": "北京市朝阳区xxx路xxx号"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `flow_type` | string | 流程类型: `shipping` (物流换货) or `direct` (直接换货)。历史数据为空时视为 `shipping` |
|
||||
| `flow_type_name` | string | 流程类型名称: "物流换货" or "直接换货" |
|
||||
| `shipped_at` | string\|null | 发货时间,仅 `shipping` 流程发货后有值 |
|
||||
| `completed_at` | string\|null | 换货完成时间,换货完成后有值 |
|
||||
|
||||
**Backend Behavior:**
|
||||
|
||||
- Only returns exchanges where `flow_type = shipping` AND `status IN (1, 2, 3)`
|
||||
- `direct` type exchanges are never returned (they complete immediately on creation)
|
||||
- If no pending exchange exists, returns `data: null`
|
||||
|
||||
### POST /api/c/v1/exchange/{id}/shipping-info
|
||||
|
||||
**Response includes the same new fields as above**
|
||||
|
||||
**Backend Constraints:**
|
||||
|
||||
- Only accepts `flow_type = shipping` AND `status = 1`
|
||||
- Returns 400 error if `flow_type = direct`: `{"code": 400, "message": "流程类型不支持该操作"}`
|
||||
- Returns 400 error if status is not 1: `{"code": 400, "message": "当前状态不允许填写收货信息"}`
|
||||
|
||||
## UI Changes
|
||||
|
||||
### Exchange Detail Card
|
||||
|
||||
Add flow type display in the exchange info section:
|
||||
|
||||
**Before:**
|
||||
```
|
||||
换货原因: 设备损坏
|
||||
申请时间: 2026-06-03 10:00:00
|
||||
收件人: 张三
|
||||
联系电话: 13800138000
|
||||
收货地址: 北京市朝阳区xxx路xxx号
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
换货类型: 物流换货
|
||||
换货原因: 设备损坏
|
||||
申请时间: 2026-06-03 10:00:00
|
||||
发货时间: 2026-06-04 15:30:00 (仅已发货时显示)
|
||||
完成时间: 2026-06-05 10:00:00 (仅已完成时显示)
|
||||
收件人: 张三
|
||||
联系电话: 13800138000
|
||||
收货地址: 北京市朝阳区xxx路xxx号
|
||||
```
|
||||
|
||||
**Display Rules:**
|
||||
|
||||
1. **换货类型**: Always display `flow_type_name` if present, fallback to "物流换货" if empty
|
||||
2. **发货时间**: Only display if `shipped_at` is not null
|
||||
3. **完成时间**: Only display if `completed_at` is not null and `status === 4`
|
||||
|
||||
### Error Handling
|
||||
|
||||
When submitting shipping info fails with flow type error:
|
||||
|
||||
```javascript
|
||||
// Existing error handling should catch and display backend error message
|
||||
catch (error) {
|
||||
console.error('提交换货信息失败', error);
|
||||
// Backend will return: {"code": 400, "message": "流程类型不支持该操作"}
|
||||
// Default error toast will show this message
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Update API Module Types
|
||||
|
||||
No changes needed to `api/modules/exchange.js` - it already passes through all response fields.
|
||||
|
||||
### 2. Update Exchange Page Template
|
||||
|
||||
File: `pages/device-exchange/device-exchange.vue`
|
||||
|
||||
Add flow type and timestamp displays:
|
||||
|
||||
```vue
|
||||
<view class="exchange-info">
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">换货类型</view>
|
||||
<view class="info-value">{{ exchangeData.flow_type_name || '物流换货' }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">换货原因</view>
|
||||
<view class="info-value">{{ exchangeData.exchange_reason }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">申请时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.created_at) }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.shipped_at">
|
||||
<view class="info-label">发货时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.shipped_at) }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.completed_at && exchangeData.status === 4">
|
||||
<view class="info-label">完成时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.completed_at) }}</view>
|
||||
</view>
|
||||
<!-- Existing recipient fields -->
|
||||
</view>
|
||||
```
|
||||
|
||||
### 3. No Button Logic Changes
|
||||
|
||||
Since `direct` type exchanges never appear in pending (they complete immediately), the existing button visibility logic remains valid:
|
||||
|
||||
```vue
|
||||
<view class="exchange-actions" v-if="exchangeData.status === 1">
|
||||
<button class="btn-apple btn-primary" @tap="openPopup">填写信息</button>
|
||||
</view>
|
||||
```
|
||||
|
||||
Backend enforces that only `shipping` + status 1 can submit shipping info, so frontend doesn't need additional flow type checks.
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### 1. Shipping Flow Exchange (Existing Behavior)
|
||||
|
||||
- Load pending exchange with `flow_type: "shipping"`, `status: 1`
|
||||
- Verify "换货类型: 物流换货" displays
|
||||
- Verify "填写信息" button shows
|
||||
- Submit shipping info successfully
|
||||
- Verify response includes new fields
|
||||
|
||||
### 2. Shipped Exchange
|
||||
|
||||
- Load exchange with `flow_type: "shipping"`, `status: 3`, `shipped_at: "2026-06-04T15:30:00+08:00"`
|
||||
- Verify "发货时间" row displays
|
||||
- Verify no action buttons show
|
||||
|
||||
### 3. Completed Exchange
|
||||
|
||||
- Load exchange with `flow_type: "shipping"`, `status: 4`, `completed_at: "2026-06-05T10:00:00+08:00"`
|
||||
- Verify "完成时间" row displays
|
||||
- Verify no action buttons show
|
||||
|
||||
### 4. No Pending Exchange
|
||||
|
||||
- Call API with no pending exchange
|
||||
- Verify empty state shows: "暂无换货记录"
|
||||
|
||||
### 5. Error Handling (Edge Case)
|
||||
|
||||
If somehow a `direct` type exchange appeared and user tried to submit shipping info (should not happen), backend returns 400 error which existing error handler will toast.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- Historical data without `flow_type` will be treated as `shipping` (per backend spec)
|
||||
- All new fields are optional in frontend display logic
|
||||
- Existing shipping submission flow is unchanged
|
||||
17
openspec/changes/update-exchange-flow-type-support/tasks.md
Normal file
17
openspec/changes/update-exchange-flow-type-support/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Update Exchange Detail Display
|
||||
- [x] Add flow type display row in exchange info section
|
||||
- [x] Add shipped_at display row (conditional on value)
|
||||
- [x] Add completed_at display row (conditional on value and status)
|
||||
- [x] Ensure backward compatibility for historical data without flow_type
|
||||
|
||||
## 2. Testing
|
||||
- [ ] Test with shipping flow exchange (status 1, 2, 3)
|
||||
- [ ] Test with completed exchange showing completion time
|
||||
- [ ] Test with shipped exchange showing ship time
|
||||
- [ ] Test empty state when no pending exchange
|
||||
- [ ] Test shipping info submission success with new response fields
|
||||
|
||||
## Files Modified
|
||||
- [x] `pages/device-exchange/device-exchange.vue`: Added new field displays
|
||||
44
package-lock.json
generated
44
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "c-xmot-iot",
|
||||
"name": "device-voice-h5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -10,6 +10,7 @@
|
||||
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
|
||||
"clipboard": "^2.0.11",
|
||||
"dayjs": "^1.11.19",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"jweixin-module": "^1.6.0",
|
||||
"uview-plus": "^3.6.29",
|
||||
"vue": "^3.4.0"
|
||||
@@ -65,6 +66,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.25.2.tgz",
|
||||
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
@@ -1916,7 +1918,8 @@
|
||||
"version": "3.4.19",
|
||||
"resolved": "https://registry.npmmirror.com/@dcloudio/types/-/types-3.4.19.tgz",
|
||||
"integrity": "sha512-1foayOFEAQ+jnQLt3ACsovCNjer3/fXn1I2VBpmDOzs2nk/n4UHwRLAxZV/RpxRqaGOPEvKrO/Pq+VI6sAmuRw==",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@dcloudio/uni-app": {
|
||||
"version": "3.0.0-4080720251210001",
|
||||
@@ -3267,18 +3270,6 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-legacy": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-legacy/-/plugin-legacy-5.3.2.tgz",
|
||||
@@ -3922,6 +3913,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4068,6 +4060,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/china-division": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/china-division/-/china-division-2.7.0.tgz",
|
||||
"integrity": "sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -4396,6 +4394,15 @@
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/element-china-area-data": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/element-china-area-data/-/element-china-area-data-6.1.0.tgz",
|
||||
"integrity": "sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"china-division": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -5708,6 +5715,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6114,6 +6122,7 @@
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -6216,6 +6225,7 @@
|
||||
"integrity": "sha512-u56TU0AIFqMtauKl/OJ1AeFsXqRHkgO7nCWmHaDwfxDo9GUMSqBA4NEh6GMuh1CYVM7zuROYtZrHzPc2ixK+ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
@@ -6604,6 +6614,7 @@
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -6680,8 +6691,7 @@
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.1",
|
||||
@@ -6951,6 +6961,7 @@
|
||||
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
@@ -7006,6 +7017,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.4.21.tgz",
|
||||
"integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.21",
|
||||
"@vue/compiler-sfc": "3.4.21",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev:h5": "uni",
|
||||
"build:h5": "cross-env UNI_INPUT_DIR=. uni build -p h5"
|
||||
"build:h5": "cross-env UNI_INPUT_DIR=. uni build -p h5 --mode development",
|
||||
"build:h5:production": "cross-env UNI_INPUT_DIR=. uni build -p h5 --mode production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "3.4.19",
|
||||
|
||||
@@ -75,6 +75,12 @@
|
||||
"navigationBarTitleText": "我的钱包"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/alipay-payment/alipay-payment",
|
||||
"style": {
|
||||
"navigationBarTitleText": "支付宝支付"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/error/error",
|
||||
"style": {
|
||||
@@ -89,4 +95,4 @@
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
}
|
||||
|
||||
173
pages/alipay-payment/alipay-payment.vue
Normal file
173
pages/alipay-payment/alipay-payment.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<view class="container alipay-page">
|
||||
<view class="pay-content">
|
||||
<view class="method-card">
|
||||
<view class="method-header">
|
||||
<view class="method-index">方式一</view>
|
||||
<view class="method-title">浏览器链接支付宝支付</view>
|
||||
</view>
|
||||
<view class="browser-tip">点击右上角三个点,使用浏览器打开</view>
|
||||
<image class="tips-image" src="/static/tips.png" mode="widthFix"></image>
|
||||
</view>
|
||||
|
||||
<view class="method-card">
|
||||
<view class="method-header">
|
||||
<view class="method-index">方式二</view>
|
||||
<view class="method-title">支付宝扫码支付</view>
|
||||
</view>
|
||||
<view class="qr-wrap">
|
||||
<u-qrcode v-if="qrUrl" :val="qrUrl" :size="220" unit="px" :show-loading="false"></u-qrcode>
|
||||
</view>
|
||||
<view class="qr-desc">截图保存二维码,打开支付宝扫一扫即可支付</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { getPendingAlipayPaymentData } from '@/utils/payment.js';
|
||||
|
||||
const browserUrl = ref('');
|
||||
const qrUrl = ref('');
|
||||
const hasRedirected = ref(false);
|
||||
|
||||
const isInWechat = () => /micromessenger/i.test(navigator.userAgent);
|
||||
|
||||
const decodePaymentUrl = (value) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padding = '='.repeat((4 - normalized.length % 4) % 4);
|
||||
return decodeURIComponent(escape(atob(normalized + padding)));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getQueryParam = (key) => {
|
||||
// #ifdef H5
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(key) || '';
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return '';
|
||||
// #endif
|
||||
};
|
||||
|
||||
const getPayUrlFromQuery = () => {
|
||||
return decodePaymentUrl(getQueryParam('pay64'));
|
||||
};
|
||||
|
||||
const getQrUrlFromQuery = () => {
|
||||
return decodePaymentUrl(getQueryParam('qr64'));
|
||||
};
|
||||
|
||||
const redirectToAlipayInBrowser = () => {
|
||||
// #ifdef H5
|
||||
if (hasRedirected.value || isInWechat() || !browserUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasRedirected.value = true;
|
||||
window.location.href = browserUrl.value;
|
||||
// #endif
|
||||
};
|
||||
|
||||
const loadPaymentData = () => {
|
||||
const data = getPendingAlipayPaymentData();
|
||||
browserUrl.value = getPayUrlFromQuery() || data?.copyUrl || data?.qrUrl || '';
|
||||
qrUrl.value = getQrUrlFromQuery() || data?.qrUrl || data?.copyUrl || browserUrl.value;
|
||||
redirectToAlipayInBrowser();
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
loadPaymentData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alipay-page {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
background: linear-gradient(180deg, #eaf4ff 0%, var(--bg-secondary) 42%);
|
||||
}
|
||||
|
||||
.pay-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(10, 132, 255, 0.08);
|
||||
border: 1rpx solid rgba(10, 132, 255, 0.08);
|
||||
}
|
||||
|
||||
.qr-desc {
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.method-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.method-index {
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--primary);
|
||||
color: var(--text-inverse);
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.method-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.browser-tip {
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(10, 132, 255, 0.08);
|
||||
color: var(--primary);
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tips-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 28rpx;
|
||||
border-radius: 18rpx;
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.qr-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
border: 1rpx dashed rgba(10, 132, 255, 0.24);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -15,30 +15,14 @@
|
||||
</view>
|
||||
|
||||
<view class="package-info">
|
||||
<!-- <view class="info-row flex-row-sb">
|
||||
<view class="info-label">套餐类型</view>
|
||||
<view class="info-value">{{ item.package_type === 'formal' ? '普通套餐' : '加油包' }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">使用类型</view>
|
||||
<view class="info-value">{{ item.usage_type === 'single_card' ? '单卡' : '设备' }}</view>
|
||||
</view> -->
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">激活时间</view>
|
||||
<view class="info-value">{{ item.activated_at || '-' }}</view>
|
||||
</view>
|
||||
<!-- <view class="info-row flex-row-sb">
|
||||
<view class="info-label">创建时间</view>
|
||||
<view class="info-value">{{ item.created_at }}</view>
|
||||
</view> -->
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">到期时间</view>
|
||||
<view class="info-value">{{ item.expires_at || '-' }}</view>
|
||||
</view>
|
||||
<!-- <view class="info-row flex-row-sb">
|
||||
<view class="info-label">优先级</view>
|
||||
<view class="info-value">{{ item.priority }}</view>
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<view class="divider"></view>
|
||||
@@ -124,7 +108,9 @@
|
||||
|
||||
// 获取剩余流量
|
||||
const getRemainFlow = (item) => {
|
||||
const remain = (item.real_total_mb || 0) - (item.real_used_mb || 0);
|
||||
const total = item.real_total_mb || 0;
|
||||
const used = item.enable_virtual_data ? (item.virtual_used_mb || 0) : (item.real_used_mb || 0);
|
||||
const remain = Math.max(total - used, 0);
|
||||
return formatMB(remain);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<view class="card" v-for="item in list" :key="item.iccid">
|
||||
<view class="flex-row-g20">
|
||||
<view class="logo">
|
||||
<image :src="getLogo(item.category).logo" mode="aspectFit"></image>
|
||||
<image :src="getCarrier(item.carrier_type).logo" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="flex-col-g20">
|
||||
<view class="iccid">ICCID: {{ item.iccid }}</view>
|
||||
<view class="operator">运营商: {{ getLogo(item.category).name }}</view>
|
||||
<view class="slot">卡槽位: {{ item.slot_position || '-' }}</view>
|
||||
<view class="operator">运营商: {{ getCarrier(item.carrier_type).name }}</view>
|
||||
<view v-if="item.isDevice" class="slot">卡槽位: {{ item.slot_position || '-' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { assetApi, deviceApi, realnameApi } from '@/api/index.js';
|
||||
import { assetApi, realnameApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -50,15 +50,25 @@
|
||||
let currentModalIccid = ref('');
|
||||
let currentCard = ref(null);
|
||||
|
||||
let opratorList = reactive([
|
||||
{ category: '124', logo: 'https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500', name: '中国电信' },
|
||||
{ category: '125', logo: 'https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500', name: '中国联通' },
|
||||
{ category: '126', logo: 'https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182', name: '中国移动' }
|
||||
]);
|
||||
const carrierMap = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
CBN: '中国广电'
|
||||
};
|
||||
|
||||
const getLogo = (category) => {
|
||||
const operator = opratorList.find(item => item.category === category);
|
||||
return operator || opratorList[0];
|
||||
const carrierLogoMap = {
|
||||
CMCC: 'https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182',
|
||||
CUCC: 'https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500',
|
||||
CTCC: 'https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500',
|
||||
CBN: 'https://img1.baidu.com/it/u=3160680953,3401650303&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
|
||||
};
|
||||
|
||||
const getCarrier = (carrierType) => {
|
||||
return {
|
||||
name: carrierMap[carrierType] || '-',
|
||||
logo: carrierLogoMap[carrierType] || carrierLogoMap.CMCC
|
||||
};
|
||||
};
|
||||
|
||||
const loadCards = async () => {
|
||||
@@ -66,28 +76,23 @@
|
||||
try {
|
||||
const assetData = await assetApi.getInfo(userStore.state.identifier);
|
||||
|
||||
// 判断是否为设备
|
||||
if (assetData.asset_type === 'device') {
|
||||
// 是设备,调用设备卡列表接口
|
||||
const data = await deviceApi.getCards(userStore.state.identifier);
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
list.splice(0, list.length, ...data.cards.map(card => ({
|
||||
iccid: card.iccid,
|
||||
category: getCarrierCategory(card.carrier_name),
|
||||
slot_position: card.slot_position,
|
||||
isRealName: card.real_name_status === 1
|
||||
})));
|
||||
}
|
||||
list.splice(0, list.length, ...(assetData.cards || []).map(card => ({
|
||||
iccid: card.iccid,
|
||||
carrier_type: card.carrier_type,
|
||||
slot_position: card.slot_position,
|
||||
isDevice: true,
|
||||
isRealName: card.real_name_status === 1
|
||||
})));
|
||||
} else if (assetData.iccid) {
|
||||
list.splice(0, list.length, {
|
||||
iccid: assetData.iccid,
|
||||
carrier_type: assetData.carrier_type,
|
||||
isDevice: false,
|
||||
isRealName: assetData.real_name_status === 1
|
||||
});
|
||||
} else {
|
||||
// 不是设备(单卡),直接使用 asset info 数据
|
||||
if (assetData.identifier) {
|
||||
list.splice(0, list.length, {
|
||||
iccid: assetData.identifier,
|
||||
category: getCarrierCategory(assetData.carrier_name),
|
||||
slot_position: 1,
|
||||
isRealName: assetData.real_name_status === 1
|
||||
});
|
||||
}
|
||||
list.splice(0, list.length);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载卡列表失败', e);
|
||||
@@ -95,14 +100,6 @@
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const getCarrierCategory = (carrierName) => {
|
||||
if (!carrierName) return '126';
|
||||
if (carrierName.includes('电信')) return '124';
|
||||
if (carrierName.includes('联通')) return '125';
|
||||
if (carrierName.includes('移动')) return '126';
|
||||
return '126';
|
||||
};
|
||||
|
||||
const toReal = async (card) => {
|
||||
currentCard.value = card;
|
||||
currentModalIccid.value = card.iccid;
|
||||
@@ -114,22 +111,25 @@
|
||||
};
|
||||
|
||||
const doRealName = async () => {
|
||||
const iccid = currentModalIccid.value;
|
||||
closeModal();
|
||||
|
||||
try {
|
||||
const data = await realnameApi.getLink(userStore.state.identifier, currentModalIccid.value);
|
||||
const data = await realnameApi.getLink(userStore.state.identifier, iccid, { showError: false });
|
||||
if (data.realname_url) {
|
||||
uni.setClipboardData({
|
||||
data: currentModalIccid.value,
|
||||
data: iccid,
|
||||
success: () => {
|
||||
uni.showToast({ title: 'ICCID已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
closeModal();
|
||||
setTimeout(() => {
|
||||
window.location.href = data.realname_url;
|
||||
}, 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取实名链接失败', e);
|
||||
uni.showToast({ title: e?.msg || '获取实名链接失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
</view>
|
||||
|
||||
<view class="exchange-info">
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">换货类型</view>
|
||||
<view class="info-value">{{ exchangeData.flow_type_name || '物流换货' }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb">
|
||||
<view class="info-label">换货原因</view>
|
||||
<view class="info-value">{{ exchangeData.exchange_reason }}</view>
|
||||
@@ -23,6 +27,14 @@
|
||||
<view class="info-label">申请时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.created_at) }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.shipped_at">
|
||||
<view class="info-label">发货时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.shipped_at) }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.completed_at && exchangeData.status === 4">
|
||||
<view class="info-label">完成时间</view>
|
||||
<view class="info-value">{{ formatTime(exchangeData.completed_at) }}</view>
|
||||
</view>
|
||||
<view class="info-row flex-row-sb" v-if="exchangeData.recipient_name">
|
||||
<view class="info-label">收件人</view>
|
||||
<view class="info-value">{{ exchangeData.recipient_name }}</view>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<UserInfoCard :currentCardNo="currentCardNo" :deviceInfo="deviceInfo" :onlineStatus="onlineStatus"
|
||||
:isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status" />
|
||||
:isDevice="userInfo.isDevice" :networkStatus="deviceInfo.network_status" @copy="copy" />
|
||||
|
||||
<DeviceStatusCard v-if="userInfo.isDevice" :deviceInfo="deviceInfo" :isRealName="isRealName"
|
||||
:isDevice="userInfo.isDevice" @authentication="enterDetail('authentication')" />
|
||||
@@ -243,9 +243,8 @@
|
||||
deviceInfo.iccid = data.iccid || '-';
|
||||
deviceInfo.walletBalance = data.wallet_balance ?? 0;
|
||||
const currentCard = resolveCurrentCard(data.cards || []);
|
||||
const realNameStatusCode = currentCard?.real_name_status ?? data.real_name_status;
|
||||
isRealName.value = realNameStatusCode === 1;
|
||||
realNameStatus.value = realNameStatusCode === 1 ? '已实名' : '未实名';
|
||||
isRealName.value = !!currentCard?.real_name_at || currentCard?.real_name_status === 1 || data.real_name_status === 1;
|
||||
realNameStatus.value = isRealName.value ? '已实名' : '未实名';
|
||||
boundPhone.value = data.bound_phone || '';
|
||||
alreadyBindPhone.value = !!data.bound_phone;
|
||||
|
||||
@@ -757,7 +756,7 @@
|
||||
});
|
||||
break;
|
||||
case 'recover':
|
||||
restartShow.value = true;
|
||||
recoverShow.value = true;
|
||||
break;
|
||||
case 'restart':
|
||||
restartShow.value = true;
|
||||
|
||||
@@ -186,6 +186,23 @@
|
||||
doLogin();
|
||||
};
|
||||
|
||||
const parseScanIdentifier = (resultStr) => {
|
||||
const scanValue = (resultStr || '').trim();
|
||||
if (!scanValue) return '';
|
||||
|
||||
const barcodeMatch = scanValue.match(/^[A-Z0-9_]+,(.+)$/);
|
||||
if (barcodeMatch) return barcodeMatch[1].trim();
|
||||
|
||||
if (scanValue.includes('?')) {
|
||||
const params = new URLSearchParams(scanValue.split('?')[1]);
|
||||
const queryValue = params.get('cardNum') || params.get('identifier');
|
||||
if (queryValue) return queryValue;
|
||||
}
|
||||
|
||||
const match = scanValue.match(/=([^=]+)$/);
|
||||
return match ? match[1].trim() : scanValue;
|
||||
};
|
||||
|
||||
const handleScanLogin = async () => {
|
||||
// #ifdef H5
|
||||
try {
|
||||
@@ -199,24 +216,13 @@
|
||||
const resultStr = await wxScan();
|
||||
console.log('扫码结果:', resultStr);
|
||||
|
||||
let value = '';
|
||||
|
||||
// 解析二维码内容
|
||||
if (resultStr.includes('?')) {
|
||||
const params = new URLSearchParams(resultStr.split('?')[1]);
|
||||
value = params.get('cardNum') || '';
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
const match = resultStr.match(/=([^=]+)$/);
|
||||
value = match ? match[1] : '';
|
||||
}
|
||||
const value = parseScanIdentifier(resultStr);
|
||||
|
||||
if (value) {
|
||||
identifier.value = value;
|
||||
handleLogin();
|
||||
} else {
|
||||
uni.showToast({ title: '无效的二维码', icon: 'none' });
|
||||
uni.showToast({ title: '无效的扫码内容', icon: 'none' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('扫码错误:', err);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +1,66 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="filter-tabs">
|
||||
<view v-for="(item, index) in filterOptions" :key="index" class="tab-item"
|
||||
:class="{ active: filterIndex === index }" @tap="onFilterChange(index)">
|
||||
<view
|
||||
v-for="(item, index) in filterOptions"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: filterIndex === index }"
|
||||
@tap="onFilterChange(index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<view v-if="filterIndex === index" class="tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-container" @scrolltolower="loadMore">
|
||||
|
||||
<view v-if="orderList.length === 0 && !loading" class="empty-state">
|
||||
<view class="empty-icon">📋</view>
|
||||
<view class="empty-icon">ORD</view>
|
||||
<view class="empty-title">暂无订单</view>
|
||||
<view class="empty-desc">当前账号下暂无订单信息</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="order-list">
|
||||
<view class="order-card" v-for="(item, index) in orderList" :key="index">
|
||||
<view class="card-header">
|
||||
<view class="header-left">
|
||||
<view class="header-row">
|
||||
<text class="field-tag">订单号</text>
|
||||
<text class="order-no">{{ item.order_no }}</text>
|
||||
<view class="order-card" v-for="item in orderList" :key="item.order_id">
|
||||
<view class="card-header">
|
||||
<view class="header-left">
|
||||
<view class="header-row">
|
||||
<text class="field-tag">订单号</text>
|
||||
<text class="order-no">{{ item.order_no }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="status-badge" :class="getStatusClass(item.payment_status)">
|
||||
{{ item.payment_status_name }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="status-badge" :class="getStatusClass(item.payment_status)">
|
||||
{{ item.payment_status_name }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-grid">
|
||||
<view class="info-item">
|
||||
<view class="info-label">套餐名称</view>
|
||||
<view class="info-value">
|
||||
<view class="package-item" v-for="(pkgName, pIndex) in item.package_names" :key="pIndex">
|
||||
<text class="package-name">{{ pkgName }}</text>
|
||||
<view class="card-body">
|
||||
<view class="info-grid">
|
||||
<view class="info-item">
|
||||
<view class="info-label">套餐名称</view>
|
||||
<view class="info-value">
|
||||
<view class="package-item" v-for="(pkgName, pkgIndex) in item.package_names" :key="pkgIndex">
|
||||
<text class="package-name">{{ pkgName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">订单金额</view>
|
||||
<view class="info-value amount">¥{{ formatMoney(item.total_amount) }}</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">下单时间</view>
|
||||
<view class="info-value">{{ item.created_at }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <view class="divider"></view>
|
||||
|
||||
<view class="package-section">
|
||||
<view class="section-label">套餐名称</view>
|
||||
<view class="package-list">
|
||||
<view class="package-item" v-for="(pkgName, pIndex) in item.package_names" :key="pIndex">
|
||||
<text class="package-name">{{ pkgName }}</text>
|
||||
<view class="info-item">
|
||||
<view class="info-label">订单金额</view>
|
||||
<view class="info-value amount">¥{{ formatMoney(item.total_amount) }}</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">下单时间</view>
|
||||
<view class="info-value">{{ item.created_at }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view v-if="item.payment_status === 1" class="card-footer">
|
||||
<button class="btn-pay" :disabled="orderPayingId !== null" @tap="handleOrderPayment(item)">
|
||||
{{ isOrderPaying(item) ? '处理中...' : '立即支付' }}
|
||||
</button>
|
||||
<view v-if="item.payment_status === 1" class="card-footer">
|
||||
<button class="btn-pay" :disabled="orderPayingId !== null" @tap="showOrderPaymentMethods(item)">
|
||||
{{ isOrderPaying(item) ? '处理中...' : '立即支付' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="load-more" v-if="orderList.length > 0">
|
||||
<text v-if="loading">加载中...</text>
|
||||
@@ -80,48 +73,44 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { assetApi, orderApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted
|
||||
} from 'vue';
|
||||
import {
|
||||
orderApi
|
||||
} from '@/api/index.js';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/store/index.js';
|
||||
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
|
||||
consumePendingPaymentRefresh,
|
||||
handlePaymentError,
|
||||
isValidAlipayPaymentLink,
|
||||
isValidWechatPayConfig,
|
||||
openAlipayPayment,
|
||||
PAYMENT_REFRESH_TARGETS,
|
||||
showPaymentToast,
|
||||
wechatH5Pay
|
||||
} from '@/utils/payment.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
let orderList = reactive([]);
|
||||
let loading = ref(false);
|
||||
let noMore = ref(false);
|
||||
let page = ref(1);
|
||||
let orderPayingId = ref(null);
|
||||
const orderList = reactive([]);
|
||||
const loading = ref(false);
|
||||
const noMore = ref(false);
|
||||
const page = ref(1);
|
||||
const orderPayingId = ref(null);
|
||||
const pageSize = 10;
|
||||
let filterIndex = ref(0);
|
||||
let filterOptions = [{
|
||||
label: '全部',
|
||||
value: null
|
||||
},
|
||||
{
|
||||
label: '待支付',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '已支付',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: '已取消',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: '已退款',
|
||||
value: 4
|
||||
}
|
||||
const filterIndex = ref(0);
|
||||
const isDeviceAsset = ref(true);
|
||||
let assetTypePromise = null;
|
||||
const paymentMethodOptions = computed(() => [
|
||||
isDeviceAsset.value
|
||||
? { label: '微信支付', value: 'wechat' }
|
||||
: { label: '支付宝支付', value: 'alipay' },
|
||||
{ label: '钱包支付', value: 'wallet' }
|
||||
]);
|
||||
const filterOptions = [
|
||||
{ label: '全部', value: null },
|
||||
{ label: '待支付', value: 1 },
|
||||
{ label: '已支付', value: 2 },
|
||||
{ label: '已取消', value: 3 },
|
||||
{ label: '已退款', value: 4 }
|
||||
];
|
||||
|
||||
const formatMoney = (amount) => {
|
||||
@@ -139,17 +128,53 @@
|
||||
return classMap[status] || '';
|
||||
};
|
||||
|
||||
const onFilterChange = (index) => {
|
||||
filterIndex.value = index;
|
||||
const loadAssetType = async () => {
|
||||
if (!userStore.state.identifier) return;
|
||||
if (assetTypePromise) return assetTypePromise;
|
||||
|
||||
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
|
||||
.then((data) => {
|
||||
isDeviceAsset.value = data.asset_type === 'device';
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载资产类型失败', error);
|
||||
})
|
||||
.finally(() => {
|
||||
assetTypePromise = null;
|
||||
});
|
||||
|
||||
return assetTypePromise;
|
||||
};
|
||||
|
||||
const resetOrderListAndLoad = () => {
|
||||
page.value = 1;
|
||||
noMore.value = false;
|
||||
loadOrderList();
|
||||
};
|
||||
|
||||
const loadOrderList = async (append = false) => {
|
||||
if (loading.value || noMore.value) return;
|
||||
loading.value = true;
|
||||
const scheduleOrderStatusRefresh = () => {
|
||||
uni.showToast({
|
||||
title: '正在同步支付状态',
|
||||
icon: 'none'
|
||||
});
|
||||
resetOrderListAndLoad();
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 1500);
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const onFilterChange = (index) => {
|
||||
filterIndex.value = index;
|
||||
resetOrderListAndLoad();
|
||||
};
|
||||
|
||||
const loadOrderList = async (append = false) => {
|
||||
if (loading.value || (append && noMore.value)) return;
|
||||
|
||||
loading.value = true;
|
||||
const paymentStatus = filterOptions[filterIndex.value].value;
|
||||
|
||||
try {
|
||||
@@ -160,11 +185,17 @@
|
||||
paymentStatus
|
||||
);
|
||||
|
||||
const newData = (data.items || []).map(item => ({
|
||||
const newData = (data.items || []).map((item) => ({
|
||||
...item,
|
||||
payment_status_name: item.payment_status === 1 ? '待支付' : item.payment_status === 2 ?
|
||||
'已支付' : item.payment_status === 3 ? '已取消' : item.payment_status === 4 ? '已退款' :
|
||||
'未知',
|
||||
payment_status_name: item.payment_status === 1
|
||||
? '待支付'
|
||||
: item.payment_status === 2
|
||||
? '已支付'
|
||||
: item.payment_status === 3
|
||||
? '已取消'
|
||||
: item.payment_status === 4
|
||||
? '已退款'
|
||||
: '未知',
|
||||
created_at: item.created_at ? item.created_at.split('T').join(' ').slice(0, 19) : ''
|
||||
}));
|
||||
|
||||
@@ -177,11 +208,12 @@
|
||||
if (newData.length < pageSize) {
|
||||
noMore.value = true;
|
||||
} else {
|
||||
page.value++;
|
||||
page.value += 1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载订单列表失败', e);
|
||||
} catch (error) {
|
||||
console.error('加载订单列表失败', error);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
@@ -193,49 +225,78 @@
|
||||
|
||||
const isOrderPaying = (order) => orderPayingId.value === order.order_id;
|
||||
|
||||
const handleOrderPayment = async (order) => {
|
||||
const showOrderPaymentMethods = async (order) => {
|
||||
if (orderPayingId.value !== null || !order?.order_id) return;
|
||||
orderPayingId.value = order.order_id;
|
||||
await loadAssetType();
|
||||
const options = paymentMethodOptions.value;
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: options.map((item) => item.label),
|
||||
success: ({ tapIndex }) => {
|
||||
const selectedMethod = options[tapIndex]?.value;
|
||||
if (selectedMethod) {
|
||||
handleOrderPayment(order, selectedMethod);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrderPayment = async (order, paymentMethod) => {
|
||||
if (orderPayingId.value !== null || !order?.order_id) return;
|
||||
|
||||
orderPayingId.value = order.order_id;
|
||||
uni.showLoading({
|
||||
title: '处理中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
const payData = await orderApi.pay(order.order_id, 'wechat');
|
||||
|
||||
const payData = await orderApi.pay(order.order_id, paymentMethod);
|
||||
uni.hideLoading();
|
||||
|
||||
if (!payData.pay_config || !payData.pay_config.package) {
|
||||
if (paymentMethod === 'wallet') {
|
||||
showPaymentToast(true, '支付成功');
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (paymentMethod === 'wechat') {
|
||||
if (!isValidWechatPayConfig(payData?.pay_config)) {
|
||||
uni.showToast({
|
||||
title: '支付参数获取失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await wechatH5Pay(payData.pay_config);
|
||||
showPaymentToast(true, '支付成功');
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
handlePaymentError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidAlipayPaymentLink(payData?.payment_link)) {
|
||||
uni.showToast({
|
||||
title: '支付参数获取失败',
|
||||
title: '支付链接获取失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 调起微信支付
|
||||
try {
|
||||
await wechatH5Pay(payData.pay_config);
|
||||
// 支付成功
|
||||
showPaymentToast(true, '支付成功');
|
||||
setTimeout(() => {
|
||||
page.value = 1;
|
||||
noMore.value = false;
|
||||
loadOrderList();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
// 支付失败或取消
|
||||
handlePaymentError(error);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
await openAlipayPayment(payData.payment_link, PAYMENT_REFRESH_TARGETS.ORDER_LIST);
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('支付失败', e);
|
||||
const errorMsg = e.msg || e.message || '支付失败,请稍后重试';
|
||||
console.error('订单支付失败', error);
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
title: error.msg || error.message || '支付失败,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
@@ -243,7 +304,16 @@
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.ORDER_LIST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleOrderStatusRefresh();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadAssetType();
|
||||
loadOrderList();
|
||||
});
|
||||
</script>
|
||||
@@ -315,9 +385,10 @@
|
||||
padding: 200rpx 40rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
font-size: 56rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
@@ -374,12 +445,6 @@
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -410,32 +475,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background: #f5f5f5;
|
||||
margin: 24rpx 0;
|
||||
.package-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.package-section {
|
||||
.section-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.package-list {
|
||||
.package-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 0;
|
||||
|
||||
.package-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
.package-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@@ -453,7 +501,7 @@
|
||||
padding: 16rpx 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
width:100%;
|
||||
width: 100%;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view v-if="packageList.length === 0 && !loading" class="empty-state">
|
||||
<view class="empty-icon">📦</view>
|
||||
<view class="empty-title">暂无可购套餐,请联系客服</view>
|
||||
<view class="empty-icon">PKG</view>
|
||||
<view class="empty-title">暂无可购套餐,请联系客服</view>
|
||||
<view class="empty-desc">当前资产暂无适用的套餐</view>
|
||||
</view>
|
||||
|
||||
<view class="card package-card" v-for="item in packageList" :key="item.package_id">
|
||||
<view class="package-header flex-row-sb">
|
||||
<view class="package-header">
|
||||
<view class="package-name">{{ item.package_name }}</view>
|
||||
<view class="header-right">
|
||||
<view class="validity-tag">有效期{{ item.validity_days }}天</view>
|
||||
<view class="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
|
||||
{{ item.is_addon ? '加油包' : '正式套餐' }}
|
||||
</view>
|
||||
<view class="tag-apple" :class="item.is_addon ? 'tag-warning' : 'tag-primary'">
|
||||
{{ item.is_addon ? '加油包' : '正式套餐' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="package-data">{{ formatData(item.data_allowance, item.data_unit) }}</view>
|
||||
<view class="package-desc">{{ item.description }}</view>
|
||||
<view class="package-footer flex-row-sb">
|
||||
<view class="package-price">¥{{ formatMoney(item.retail_price) }}</view>
|
||||
<view class="package-main">
|
||||
<view class="data-block">
|
||||
<view class="data-label">套餐流量</view>
|
||||
<view class="package-data">{{ formatData(item.data_allowance, item.data_unit) }}</view>
|
||||
</view>
|
||||
<view class="validity-box">
|
||||
<view class="validity-value">{{ item.validity_days }}</view>
|
||||
<view class="validity-label">天有效期</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="package-desc" v-if="item.description">{{ item.description }}</view>
|
||||
<view class="package-footer">
|
||||
<view class="price-block">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="package-price">{{ formatMoney(item.retail_price) }}</text>
|
||||
</view>
|
||||
<view class="btn">
|
||||
<up-button type="primary" @click="buyPackage(item)">立即订购</up-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式选择弹窗 -->
|
||||
<up-popup :show="showModal" mode="center" @close="showModal=false">
|
||||
<up-popup :show="showModal" mode="center" @close="showModal = false">
|
||||
<view class="payment-popup">
|
||||
<view class="popup-header">
|
||||
<view class="popup-title">选择支付方式</view>
|
||||
<view class="popup-close" @tap="showModal=false">×</view>
|
||||
<view class="popup-close" @tap="showModal = false">×</view>
|
||||
</view>
|
||||
|
||||
<view class="package-summary">
|
||||
@@ -45,13 +53,13 @@
|
||||
<view class="detail-divider"></view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">有效期</text>
|
||||
<text class="detail-value">{{ currentPackage?.validity_days }}天</text>
|
||||
<text class="detail-value">{{ currentPackage?.validity_days }} 天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="payment-methods">
|
||||
<view class="method-item" :class="{ active: paymentMethod === 'wechat' }" @tap="selectPaymentMethod('wechat')">
|
||||
<view v-if="isDeviceAsset" class="method-item" :class="{ active: paymentMethod === 'wechat' }" @tap="selectPaymentMethod('wechat')">
|
||||
<view class="method-left">
|
||||
<image class="method-icon" src="/static/wechat.png" mode="aspectFit"></image>
|
||||
<text class="method-name">微信支付</text>
|
||||
@@ -59,6 +67,14 @@
|
||||
<view class="method-radio" :class="{ checked: paymentMethod === 'wechat' }"></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="method-item" :class="{ active: paymentMethod === 'alipay' }" @tap="selectPaymentMethod('alipay')">
|
||||
<view class="method-left">
|
||||
<view class="method-icon method-badge method-badge-alipay">支</view>
|
||||
<text class="method-name">支付宝支付</text>
|
||||
</view>
|
||||
<view class="method-radio" :class="{ checked: paymentMethod === 'alipay' }"></view>
|
||||
</view>
|
||||
|
||||
<view class="method-item" :class="{ active: paymentMethod === 'wallet' }" @tap="selectPaymentMethod('wallet')">
|
||||
<view class="method-left">
|
||||
<image class="method-icon" src="/static/wallet.png" mode="aspectFit"></image>
|
||||
@@ -69,7 +85,7 @@
|
||||
</view>
|
||||
|
||||
<view class="popup-footer">
|
||||
<button class="btn-apple btn-secondary" @tap="showModal=false">取消</button>
|
||||
<button class="btn-apple btn-secondary" @tap="showModal = false">取消</button>
|
||||
<button class="btn-apple btn-primary" :disabled="paySubmitting" @tap="confirmPay">
|
||||
{{ paySubmitting ? '处理中...' : '确认支付' }}
|
||||
</button>
|
||||
@@ -81,19 +97,31 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { assetApi, orderApi, walletApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import { wechatH5Pay, handlePaymentError, showPaymentToast } from '@/utils/payment.js';
|
||||
import {
|
||||
consumePendingPaymentRefresh,
|
||||
handlePaymentError,
|
||||
isValidAlipayPaymentLink,
|
||||
isValidWechatPayConfig,
|
||||
openAlipayPayment,
|
||||
PAYMENT_REFRESH_TARGETS,
|
||||
showPaymentToast,
|
||||
wechatH5Pay
|
||||
} from '@/utils/payment.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
let showModal = ref(false);
|
||||
let currentPackage = ref(null);
|
||||
let packageList = reactive([]);
|
||||
let loading = ref(false);
|
||||
let paymentMethod = ref('wechat');
|
||||
let walletBalance = ref(0);
|
||||
let paySubmitting = ref(false);
|
||||
const showModal = ref(false);
|
||||
const currentPackage = ref(null);
|
||||
const packageList = reactive([]);
|
||||
const loading = ref(false);
|
||||
const paymentMethod = ref('wechat');
|
||||
const walletBalance = ref(0);
|
||||
const paySubmitting = ref(false);
|
||||
const isDeviceAsset = ref(true);
|
||||
let assetTypePromise = null;
|
||||
|
||||
const formatMoney = (amount) => {
|
||||
if (!amount && amount !== 0) return '0.00';
|
||||
@@ -102,9 +130,43 @@
|
||||
|
||||
const formatData = (allowance, unit) => {
|
||||
if (unit === 'MB') {
|
||||
return allowance >= 1024 ? (allowance / 1024).toFixed(0) + ' GB' : allowance + ' MB';
|
||||
return allowance >= 1024 ? `${(allowance / 1024).toFixed(0)} GB` : `${allowance} MB`;
|
||||
}
|
||||
return allowance + ' ' + unit;
|
||||
return `${allowance} ${unit}`;
|
||||
};
|
||||
|
||||
const getDefaultPaymentMethod = () => isDeviceAsset.value ? 'wechat' : 'alipay';
|
||||
|
||||
const isPaymentMethodAvailable = (method) => {
|
||||
if (method === 'wallet') return true;
|
||||
if (method === 'wechat') return isDeviceAsset.value;
|
||||
if (method === 'alipay') return !isDeviceAsset.value;
|
||||
return false;
|
||||
};
|
||||
|
||||
const normalizePaymentMethod = () => {
|
||||
if (!isPaymentMethodAvailable(paymentMethod.value)) {
|
||||
paymentMethod.value = getDefaultPaymentMethod();
|
||||
}
|
||||
};
|
||||
|
||||
const loadAssetType = async () => {
|
||||
if (!userStore.state.identifier) return;
|
||||
if (assetTypePromise) return assetTypePromise;
|
||||
|
||||
assetTypePromise = assetApi.getInfo(userStore.state.identifier)
|
||||
.then((data) => {
|
||||
isDeviceAsset.value = data.asset_type === 'device';
|
||||
normalizePaymentMethod();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载资产类型失败', error);
|
||||
})
|
||||
.finally(() => {
|
||||
assetTypePromise = null;
|
||||
});
|
||||
|
||||
return assetTypePromise;
|
||||
};
|
||||
|
||||
const loadPackages = async () => {
|
||||
@@ -112,8 +174,8 @@
|
||||
try {
|
||||
const data = await assetApi.getPackages(userStore.state.identifier);
|
||||
packageList.splice(0, packageList.length, ...(data.packages || []));
|
||||
} catch (e) {
|
||||
console.error('加载套餐列表失败', e);
|
||||
} catch (error) {
|
||||
console.error('加载套餐列表失败', error);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
@@ -122,42 +184,91 @@
|
||||
try {
|
||||
const data = await walletApi.getDetail(userStore.state.identifier);
|
||||
walletBalance.value = data.balance || 0;
|
||||
} catch (e) {
|
||||
console.error('加载钱包余额失败', e);
|
||||
} catch (error) {
|
||||
console.error('加载钱包余额失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const buyPackage = (item) => {
|
||||
const syncPackagePageState = () => {
|
||||
loadAssetType();
|
||||
loadPackages();
|
||||
loadWalletBalance();
|
||||
};
|
||||
|
||||
const buyPackage = async (item) => {
|
||||
await loadAssetType();
|
||||
currentPackage.value = item;
|
||||
paymentMethod.value = 'wechat';
|
||||
paymentMethod.value = getDefaultPaymentMethod();
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const selectPaymentMethod = (method) => {
|
||||
if (!isPaymentMethodAvailable(method)) return;
|
||||
paymentMethod.value = method;
|
||||
};
|
||||
|
||||
const hasPreparedPaymentData = (paymentData) => {
|
||||
return isValidWechatPayConfig(paymentData?.pay_config) ||
|
||||
isValidAlipayPaymentLink(paymentData?.payment_link);
|
||||
};
|
||||
|
||||
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
|
||||
try {
|
||||
await wechatH5Pay(payConfig);
|
||||
showPaymentToast(true, isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功');
|
||||
setTimeout(() => {
|
||||
loadWalletBalance();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
handlePaymentError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreparedPayment = async (paymentData, isForceRecharge = false) => {
|
||||
if (isValidWechatPayConfig(paymentData?.pay_config)) {
|
||||
await handleWechatPay(paymentData.pay_config, isForceRecharge);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidAlipayPaymentLink(paymentData?.payment_link)) {
|
||||
await openAlipayPayment(paymentData.payment_link, PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER);
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '支付参数获取失败',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
const getCreateOrderPaymentMethod = () => {
|
||||
if (paymentMethod.value === 'alipay') return 'alipay';
|
||||
if (paymentMethod.value === 'wallet' && !isDeviceAsset.value) return 'alipay';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const confirmPay = async () => {
|
||||
if (paySubmitting.value || !currentPackage.value) return;
|
||||
|
||||
paySubmitting.value = true;
|
||||
let releaseInModalCallback = false;
|
||||
|
||||
uni.showLoading({ title: '创建订单...', mask: true });
|
||||
uni.showLoading({
|
||||
title: '创建订单...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
try {
|
||||
// 第一步:创建订单
|
||||
const orderResult = await orderApi.create(
|
||||
userStore.state.identifier,
|
||||
[currentPackage.value.package_id]
|
||||
[currentPackage.value.package_id],
|
||||
getCreateOrderPaymentMethod()
|
||||
);
|
||||
|
||||
// 判断是否为强充订单
|
||||
if (orderResult.order_type === 'recharge' && orderResult.recharge) {
|
||||
const recharge = orderResult.recharge;
|
||||
|
||||
// 检查充值订单状态
|
||||
if (recharge.status === 2 || recharge.status === 3) {
|
||||
// 已关闭或已退款的订单
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
uni.showToast({
|
||||
@@ -168,9 +279,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查充值订单是否已存在(待支付状态但无支付配置)
|
||||
if (recharge.status === 1 && (!orderResult.pay_config || !orderResult.pay_config.package)) {
|
||||
// 待支付订单已存在,引导用户跳转到订单页面
|
||||
if (recharge.status === 1 && !hasPreparedPaymentData(orderResult)) {
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
uni.showModal({
|
||||
@@ -187,36 +296,33 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有支付配置(新订单应该有)
|
||||
if (!orderResult.pay_config || !orderResult.pay_config.package) {
|
||||
if (!hasPreparedPaymentData(orderResult)) {
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||
uni.showToast({
|
||||
title: '支付参数获取失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取强充信息
|
||||
const forceRechargeAmount = orderResult.linked_package_info?.force_recharge_amount || recharge.amount;
|
||||
const packageNames = orderResult.linked_package_info?.package_names || [];
|
||||
const packageNameStr = packageNames.length > 0 ? packageNames.join('、') : '套餐';
|
||||
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
|
||||
// 提示用户需要强充
|
||||
const packageNameStr = packageNames.length > 0 ? packageNames.join('、') : '套餐';
|
||||
const message = `购买${packageNameStr}需要先充值 ¥${formatMoney(forceRechargeAmount)},充值成功后将自动购买套餐`;
|
||||
releaseInModalCallback = true;
|
||||
|
||||
uni.showModal({
|
||||
title: '需要充值',
|
||||
content: message,
|
||||
content: `购买${packageNameStr}需要先充值 ¥${formatMoney(forceRechargeAmount)},充值成功后将自动购买套餐`,
|
||||
confirmText: '去充值',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
try {
|
||||
if (res.confirm) {
|
||||
// 调起微信支付
|
||||
await handleWechatPay(orderResult.pay_config, true);
|
||||
await handlePreparedPayment(orderResult, true);
|
||||
}
|
||||
} finally {
|
||||
paySubmitting.value = false;
|
||||
@@ -229,47 +335,44 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有订单信息(非强充订单)
|
||||
if (!orderResult || !orderResult.order) {
|
||||
if (!orderResult?.order?.order_id) {
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
uni.showToast({ title: '订单创建失败', icon: 'none' });
|
||||
uni.showToast({
|
||||
title: '订单创建失败',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const orderId = orderResult.order.order_id;
|
||||
uni.showLoading({
|
||||
title: '准备支付...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 第二步:获取支付参数
|
||||
uni.showLoading({ title: '准备支付...', mask: true });
|
||||
|
||||
const payResult = await orderApi.pay(orderId, paymentMethod.value);
|
||||
const payResult = await orderApi.pay(orderResult.order.order_id, paymentMethod.value);
|
||||
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
|
||||
// 处理不同支付方式
|
||||
if (paymentMethod.value === 'wechat') {
|
||||
// 微信支付
|
||||
if (!payResult.pay_config || !payResult.pay_config.package) {
|
||||
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
await handleWechatPay(payResult.pay_config);
|
||||
} else if (paymentMethod.value === 'wallet') {
|
||||
// 钱包支付成功
|
||||
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||
if (paymentMethod.value === 'wallet') {
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
setTimeout(() => {
|
||||
loadWalletBalance();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
await handlePreparedPayment(payResult);
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
showModal.value = false;
|
||||
console.error('创建订单或支付失败', e);
|
||||
console.error('创建订单或支付失败', error);
|
||||
|
||||
// 特殊错误处理:订单正在创建中
|
||||
if (e.code === 1008) {
|
||||
if (error.code === 1008) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '订单正在创建中,是否跳转到我的订单页面进行支付?',
|
||||
@@ -283,9 +386,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他错误提示
|
||||
const errorMsg = e.msg || e.message || '操作失败,请稍后重试';
|
||||
uni.showToast({ title: errorMsg, icon: 'none' });
|
||||
uni.showToast({
|
||||
title: error.msg || error.message || '操作失败,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
if (!releaseInModalCallback) {
|
||||
paySubmitting.value = false;
|
||||
@@ -293,24 +397,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleWechatPay = async (payConfig, isForceRecharge = false) => {
|
||||
try {
|
||||
await wechatH5Pay(payConfig);
|
||||
// 支付成功
|
||||
const successMsg = isForceRecharge ? '充值成功,套餐将自动购买' : '支付成功';
|
||||
showPaymentToast(true, successMsg);
|
||||
setTimeout(() => {
|
||||
loadWalletBalance();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
// 支付失败或取消
|
||||
handlePaymentError(error);
|
||||
onShow(() => {
|
||||
if (!consumePendingPaymentRefresh(PAYMENT_REFRESH_TARGETS.PACKAGE_ORDER)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
syncPackagePageState();
|
||||
uni.showModal({
|
||||
title: '支付状态确认',
|
||||
content: '已返回页面,支付结果以订单状态为准。是否前往我的订单查看最新状态?',
|
||||
confirmText: '查看订单',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/order-list/order-list' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadPackages();
|
||||
loadWalletBalance();
|
||||
syncPackagePageState();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -323,50 +430,141 @@
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
min-height: 400rpx;
|
||||
.empty-icon { font-size: 120rpx; margin-bottom: 30rpx; opacity: 0.6; }
|
||||
.empty-title { font-size: 32rpx; font-weight: 600; color: var(--text-primary); margin-bottom: 16rpx; }
|
||||
.empty-desc { font-size: 26rpx; color: var(--text-tertiary); text-align: center; }
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.package-card {
|
||||
margin-bottom: var(--space-md);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid rgba(0, 122, 255, 0.08);
|
||||
box-shadow: 0 12rpx 36rpx rgba(15, 23, 42, 0.06);
|
||||
|
||||
.package-header {
|
||||
margin-bottom: var(--space-sm);
|
||||
.package-name { font-size: 32rpx; font-weight: 600; color: var(--text-primary); }
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
.validity-tag {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
.package-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.35;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.package-main {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
padding: 26rpx;
|
||||
margin-bottom: 22rpx;
|
||||
background: linear-gradient(135deg, rgba(0, 122, 255, 0.08) 0%, rgba(0, 122, 255, 0.02) 100%);
|
||||
border-radius: 24rpx;
|
||||
|
||||
.data-block {
|
||||
flex: 1;
|
||||
|
||||
.data-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 4rpx 12rpx;
|
||||
background: var(--gray-100);
|
||||
border-radius: 8rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.package-data {
|
||||
font-size: 52rpx;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.validity-box {
|
||||
min-width: 132rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 18rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(0, 122, 255, 0.08);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.validity-value {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.validity-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.package-data {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.package-desc {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-md);
|
||||
line-height: 1.55;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.package-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.package-price {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid var(--border-light);
|
||||
|
||||
.price-block {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: var(--warning);
|
||||
white-space: nowrap;
|
||||
|
||||
.price-symbol {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.package-price {
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex-shrink: 0;
|
||||
min-width: 180rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,6 +690,21 @@
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 18rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.method-badge-alipay {
|
||||
background: linear-gradient(135deg, #1677ff 0%, #45a5ff 100%);
|
||||
box-shadow: 0 8rpx 20rpx rgba(22, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
@@ -532,24 +745,24 @@
|
||||
padding: 30rpx;
|
||||
border-top: 1rpx solid var(--border-light);
|
||||
|
||||
.btn-apple {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
.btn-apple {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-medium);
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
&[disabled] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: var(--gray-200);
|
||||
color: var(--text-primary);
|
||||
&.btn-secondary {
|
||||
background: var(--gray-200);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
|
||||
@@ -10,19 +10,22 @@
|
||||
<view class="flex-row-sb mt-30">
|
||||
<view class="flex-row-g20">
|
||||
<view class="logo">
|
||||
<image :src="getLogo(item.category).logo" mode="aspectFit"></image>
|
||||
<image :src="getCarrier(item.carrier_type).logo" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="flex-col-g20">
|
||||
<view class="flex-row-g20">
|
||||
<view class="iccid">{{ item.iccid }}</view>
|
||||
<view class="operator">
|
||||
<up-tag type="success" size="mini">{{ getLogo(item.category).name }}</up-tag>
|
||||
<up-tag type="success" size="mini">{{ getCarrier(item.carrier_type).name }}</up-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row-g20">
|
||||
<view class="operator">
|
||||
<up-tag :type="item.real_name_status === 1 ? 'primary' : 'success'" size="mini">{{ item.real_name_status === 1 ? '已实名' : '未实名' }}</up-tag>
|
||||
</view>
|
||||
<view v-if="item.network_status" class="operator">
|
||||
<up-tag type="info" size="mini">{{ item.network_status }}</up-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="slot">卡槽位: {{ item.slot_position || '-' }}</view>
|
||||
</view>
|
||||
@@ -35,6 +38,20 @@
|
||||
<up-button class="btn-apple btn-success" v-else type="success" @tap="switchOperator(item)" :loading="switching">
|
||||
切换此运营商
|
||||
</up-button>
|
||||
<up-button class="btn-apple btn-success" v-if="item.real_name_status !== 1" type="success" @tap="toReal(item)">
|
||||
去实名
|
||||
</up-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-overlay" v-if="showIccidModal" @tap="closeModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-close" @tap="closeModal">✕</view>
|
||||
<view class="modal-body">
|
||||
<view class="modal-title">实名认证</view>
|
||||
<view class="iccid-value">{{ currentModalIccid }}</view>
|
||||
<view class="modal-button" @tap="doRealName">点击复制ICCID并跳转实名</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -42,7 +59,7 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { assetApi, deviceApi } from '@/api/index.js';
|
||||
import { assetApi, deviceApi, realnameApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -50,16 +67,28 @@
|
||||
let mchList = reactive([]);
|
||||
let loading = ref(false);
|
||||
let switching = ref(false);
|
||||
let showIccidModal = ref(false);
|
||||
let currentModalIccid = ref('');
|
||||
|
||||
let opratorList = reactive([
|
||||
{ category: '124', logo: 'https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500', name: '中国电信' },
|
||||
{ category: '125', logo: 'https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500', name: '中国联通' },
|
||||
{ category: '126', logo: 'https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182', name: '中国移动' }
|
||||
]);
|
||||
const carrierMap = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
CBN: '中国广电'
|
||||
};
|
||||
|
||||
const getLogo = (category) => {
|
||||
const operator = opratorList.find(item => item.category === category);
|
||||
return operator || opratorList[0];
|
||||
const carrierLogoMap = {
|
||||
CMCC: 'https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182',
|
||||
CUCC: 'https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500',
|
||||
CTCC: 'https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500',
|
||||
CBN: 'https://img1.baidu.com/it/u=3160680953,3401650303&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
|
||||
};
|
||||
|
||||
const getCarrier = (carrierType) => {
|
||||
return {
|
||||
name: carrierMap[carrierType] || '-',
|
||||
logo: carrierLogoMap[carrierType] || carrierLogoMap.CMCC
|
||||
};
|
||||
};
|
||||
|
||||
const loadCards = async () => {
|
||||
@@ -74,11 +103,12 @@
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
mchList.splice(0, mchList.length, ...data.cards.map(card => ({
|
||||
iccid: card.iccid,
|
||||
carrier_type: card.carrier_type,
|
||||
carrier_name: card.carrier_name,
|
||||
is_current: card.is_active,
|
||||
real_name_status: card.real_name_status,
|
||||
slot_position: card.slot_position,
|
||||
category: getCarrierCategory(card.carrier_name)
|
||||
network_status: card.network_status,
|
||||
slot_position: card.slot_position
|
||||
})));
|
||||
}
|
||||
} else {
|
||||
@@ -86,11 +116,12 @@
|
||||
if (assetData.identifier) {
|
||||
mchList.splice(0, mchList.length, {
|
||||
iccid: assetData.identifier,
|
||||
carrier_type: assetData.carrier_type,
|
||||
carrier_name: assetData.carrier_name,
|
||||
is_current: true,
|
||||
real_name_status: assetData.real_name_status,
|
||||
slot_position: 1,
|
||||
category: getCarrierCategory(assetData.carrier_name)
|
||||
network_status: assetData.network_status,
|
||||
slot_position: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -100,14 +131,6 @@
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const getCarrierCategory = (carrierName) => {
|
||||
if (!carrierName) return '126';
|
||||
if (carrierName.includes('电信')) return '124';
|
||||
if (carrierName.includes('联通')) return '125';
|
||||
if (carrierName.includes('移动')) return '126';
|
||||
return '126';
|
||||
};
|
||||
|
||||
const switchOperator = async (item) => {
|
||||
switching.value = true;
|
||||
try {
|
||||
@@ -120,6 +143,38 @@
|
||||
switching.value = false;
|
||||
};
|
||||
|
||||
const toReal = (card) => {
|
||||
currentModalIccid.value = card.iccid;
|
||||
showIccidModal.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
showIccidModal.value = false;
|
||||
};
|
||||
|
||||
const doRealName = async () => {
|
||||
const iccid = currentModalIccid.value;
|
||||
closeModal();
|
||||
|
||||
try {
|
||||
const data = await realnameApi.getLink(userStore.state.identifier, iccid, { showError: false });
|
||||
if (data.realname_url) {
|
||||
uni.setClipboardData({
|
||||
data: iccid,
|
||||
success: () => {
|
||||
uni.showToast({ title: 'ICCID已复制', icon: 'success' });
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = data.realname_url;
|
||||
}, 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取实名链接失败', e);
|
||||
uni.showToast({ title: e?.msg || '获取实名链接失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCards();
|
||||
});
|
||||
@@ -153,12 +208,83 @@
|
||||
background: var(--gray-100);
|
||||
color: var(--text-primary);
|
||||
|
||||
&.btn-primary {
|
||||
background: var(--primary);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
&.btn-success {
|
||||
background: var(--success);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-large);
|
||||
width: 620rpx;
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 24rpx; left: 24rpx;
|
||||
width: 48rpx; height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
color: var(--gray-600);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 60rpx 32rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iccid-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-button {
|
||||
padding: 24rpx 32rpx;
|
||||
background: var(--success);
|
||||
color: var(--text-inverse);
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: var(--radius-small);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
core-js: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
BIN
static/tips.png
Normal file
BIN
static/tips.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -1,3 +1,3 @@
|
||||
export const BASE_URL = 'https://cmp-api.boss160.cn';
|
||||
export const BASE_URL = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
export const APP_TYPE = 'official_account';
|
||||
|
||||
208
utils/payment.js
208
utils/payment.js
@@ -1,40 +1,164 @@
|
||||
/**
|
||||
* 微信H5支付工具
|
||||
* 统一处理微信JSAPI支付逻辑
|
||||
*/
|
||||
const ALIPAY_PENDING_REFRESH_KEY = 'pending_alipay_payment_refresh';
|
||||
const ALIPAY_PAYMENT_DATA_KEY = 'pending_alipay_payment_data';
|
||||
|
||||
export const PAYMENT_REFRESH_TARGETS = {
|
||||
PACKAGE_ORDER: 'package-order',
|
||||
ORDER_LIST: 'order-list',
|
||||
MY_WALLET: 'my-wallet'
|
||||
};
|
||||
|
||||
export function isValidWechatPayConfig(payConfig) {
|
||||
return !!payConfig &&
|
||||
typeof payConfig.package === 'string' &&
|
||||
payConfig.package.startsWith('prepay_id=');
|
||||
}
|
||||
|
||||
export function getAlipayPaymentUrl(paymentLink) {
|
||||
return getAlipayPaymentUrls(paymentLink).copyUrl;
|
||||
}
|
||||
|
||||
export function getAlipayPaymentUrls(paymentLink) {
|
||||
if (!paymentLink) {
|
||||
return { copyUrl: '', qrUrl: '' };
|
||||
}
|
||||
|
||||
if (typeof paymentLink === 'string') {
|
||||
return { copyUrl: paymentLink, qrUrl: paymentLink };
|
||||
}
|
||||
|
||||
return {
|
||||
copyUrl: paymentLink.copy_link || paymentLink.qr_link || '',
|
||||
qrUrl: paymentLink.qr_link || paymentLink.copy_link || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidAlipayPaymentLink(paymentLink) {
|
||||
return !!getAlipayPaymentUrl(paymentLink);
|
||||
}
|
||||
|
||||
export function markPendingPaymentRefresh(target) {
|
||||
if (!target) return;
|
||||
uni.setStorageSync(ALIPAY_PENDING_REFRESH_KEY, {
|
||||
target,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
export function consumePendingPaymentRefresh(target) {
|
||||
const pending = uni.getStorageSync(ALIPAY_PENDING_REFRESH_KEY);
|
||||
if (!pending || pending.target !== target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uni.removeStorageSync(ALIPAY_PENDING_REFRESH_KEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPendingAlipayPaymentData() {
|
||||
return uni.getStorageSync(ALIPAY_PAYMENT_DATA_KEY) || null;
|
||||
}
|
||||
|
||||
function encodePaymentUrl(value) {
|
||||
try {
|
||||
return btoa(unescape(encodeURIComponent(value)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getAlipayPaymentPageUrl(refreshTarget, payUrl, qrUrl) {
|
||||
const params = [`t=${Date.now()}`];
|
||||
if (refreshTarget) {
|
||||
params.push(`from=${encodeURIComponent(refreshTarget)}`);
|
||||
}
|
||||
if (payUrl) {
|
||||
const encodedPayUrl = encodePaymentUrl(payUrl);
|
||||
if (encodedPayUrl) {
|
||||
params.push(`pay64=${encodedPayUrl}`);
|
||||
}
|
||||
}
|
||||
if (qrUrl) {
|
||||
const encodedQrUrl = encodePaymentUrl(qrUrl);
|
||||
if (encodedQrUrl) {
|
||||
params.push(`qr64=${encodedQrUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `/pages/alipay-payment/alipay-payment?${params.join('&')}`;
|
||||
}
|
||||
|
||||
function getAlipayPaymentBrowserUrl(pageUrl) {
|
||||
if (typeof window === 'undefined' || !window.location) {
|
||||
return pageUrl;
|
||||
}
|
||||
|
||||
const { origin, pathname } = window.location;
|
||||
const pagePath = pageUrl.replace(/^\//, '');
|
||||
const basePath = pathname.includes('/pages/')
|
||||
? pathname.slice(0, pathname.indexOf('/pages/'))
|
||||
: pathname.replace(/\/[^/]*$/, '');
|
||||
|
||||
return `${origin}${basePath}/${pagePath}`;
|
||||
}
|
||||
|
||||
function navigateToAlipayPaymentPage(pageUrl, resolve, reject) {
|
||||
uni.navigateTo({
|
||||
url: pageUrl,
|
||||
success: () => resolve({ redirecting: true }),
|
||||
fail: () => {
|
||||
uni.redirectTo({
|
||||
url: pageUrl,
|
||||
success: () => resolve({ redirecting: true }),
|
||||
fail: () => {
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
window.location.href = getAlipayPaymentBrowserUrl(pageUrl);
|
||||
resolve({ redirecting: true });
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('\u6253\u5f00\u652f\u4ed8\u5b9d\u652f\u4ed8\u9875\u5931\u8d25'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function openAlipayPayment(paymentLink, refreshTarget) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { copyUrl, qrUrl } = getAlipayPaymentUrls(paymentLink);
|
||||
|
||||
if (!copyUrl && !qrUrl) {
|
||||
reject(new Error('\u652f\u4ed8\u94fe\u63a5\u65e0\u6548'));
|
||||
return;
|
||||
}
|
||||
|
||||
markPendingPaymentRefresh(refreshTarget);
|
||||
uni.setStorageSync(ALIPAY_PAYMENT_DATA_KEY, {
|
||||
copyUrl,
|
||||
qrUrl,
|
||||
refreshTarget,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
navigateToAlipayPaymentPage(getAlipayPaymentPageUrl(refreshTarget, copyUrl, qrUrl), resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起微信H5支付
|
||||
* @param {Object} payConfig - 支付配置对象
|
||||
* @param {string} payConfig.app_id - 微信公众号appid
|
||||
* @param {string} payConfig.timestamp - 时间戳
|
||||
* @param {string} payConfig.nonce_str - 随机字符串
|
||||
* @param {string} payConfig.package - 预支付交易会话标识
|
||||
* @param {string} payConfig.sign_type - 签名方式
|
||||
* @param {string} payConfig.pay_sign - 签名
|
||||
* @returns {Promise} 支付结果
|
||||
*/
|
||||
export function wechatH5Pay(payConfig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 验证支付配置
|
||||
if (!payConfig || !payConfig.package) {
|
||||
reject(new Error('支付参数无效'));
|
||||
if (!isValidWechatPayConfig(payConfig)) {
|
||||
reject(new Error('\u652f\u4ed8\u53c2\u6570\u65e0\u6548'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 package 格式
|
||||
if (!payConfig.package.startsWith('prepay_id=')) {
|
||||
reject(new Error('支付参数无效'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否在微信环境中
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
reject(new Error('请在微信中打开'));
|
||||
reject(new Error('\u8bf7\u5728\u5fae\u4fe1\u4e2d\u6253\u5f00'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调起微信支付
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||
appId: payConfig.app_id,
|
||||
timeStamp: payConfig.timestamp,
|
||||
@@ -44,22 +168,19 @@ export function wechatH5Pay(payConfig) {
|
||||
paySign: payConfig.pay_sign
|
||||
}, function(res) {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
// 支付成功
|
||||
resolve({
|
||||
success: true,
|
||||
message: '支付成功'
|
||||
message: '\u652f\u4ed8\u6210\u529f'
|
||||
});
|
||||
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||
// 用户取消支付
|
||||
reject({
|
||||
cancelled: true,
|
||||
message: '已取消支付'
|
||||
message: '\u5df2\u53d6\u6d88\u652f\u4ed8'
|
||||
});
|
||||
} else {
|
||||
// 支付失败
|
||||
reject({
|
||||
success: false,
|
||||
message: '支付失败',
|
||||
message: '\u652f\u4ed8\u5931\u8d25',
|
||||
error: res
|
||||
});
|
||||
}
|
||||
@@ -67,30 +188,19 @@ export function wechatH5Pay(payConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示支付结果提示
|
||||
* @param {boolean} success - 是否成功
|
||||
* @param {string} message - 提示消息
|
||||
*/
|
||||
export function showPaymentToast(success, message) {
|
||||
uni.showToast({
|
||||
title: message || (success ? '支付成功' : '支付失败'),
|
||||
title: message || (success ? '\u652f\u4ed8\u6210\u529f' : '\u652f\u4ed8\u5931\u8d25'),
|
||||
icon: success ? 'success' : 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付错误
|
||||
* @param {Error|Object} error - 错误对象
|
||||
*/
|
||||
export function handlePaymentError(error) {
|
||||
if (error.cancelled) {
|
||||
// 用户主动取消,提示取消
|
||||
showPaymentToast(false, '已取消支付');
|
||||
} else {
|
||||
// 其他错误
|
||||
const message = error.message || '支付失败';
|
||||
showPaymentToast(false, message);
|
||||
showPaymentToast(false, '\u5df2\u53d6\u6d88\u652f\u4ed8');
|
||||
return;
|
||||
}
|
||||
|
||||
showPaymentToast(false, error.message || '\u652f\u4ed8\u5931\u8d25');
|
||||
}
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
// utils/wxsdk.js —— 封装微信扫一扫 不用旧的了
|
||||
import { wechatApi } from '@/api/index.js'
|
||||
|
||||
const wx = window.jWeixin
|
||||
const getWx = () => window.jWeixin || window.wx
|
||||
const entryUrl = window.__WX_ENTRY_URL__ || window.location.href.split('#')[0]
|
||||
let configPromise = null
|
||||
let configUrl = ''
|
||||
let configReady = false
|
||||
|
||||
const isIOS = () => /iphone|ipad|ipod/i.test(navigator.userAgent)
|
||||
|
||||
const getConfigUrl = () => {
|
||||
// iOS 微信 WebView 的 JS-SDK 签名 URL 需要使用 SPA 首次进入的地址。
|
||||
if (isIOS()) {
|
||||
return entryUrl
|
||||
}
|
||||
|
||||
return window.location.href.split('#')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化微信 JS-SDK
|
||||
*/
|
||||
export async function initWxConfig() {
|
||||
const url = window.location.href.split('#')[0]
|
||||
const wx = getWx()
|
||||
if (!wx) {
|
||||
throw new Error('微信 SDK 未加载')
|
||||
}
|
||||
|
||||
const url = getConfigUrl()
|
||||
console.log(url);
|
||||
try {
|
||||
const data = await wechatApi.getJssdkConfig(url)
|
||||
|
||||
if (configReady && configUrl === url) {
|
||||
return
|
||||
}
|
||||
|
||||
if (configPromise && configUrl === url) {
|
||||
return configPromise
|
||||
}
|
||||
|
||||
configUrl = url
|
||||
configReady = false
|
||||
|
||||
configPromise = wechatApi.getJssdkConfig(url).then((data) => {
|
||||
const { app_id, timestamp, nonce_str, signature } = data
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -24,24 +55,37 @@ export async function initWxConfig() {
|
||||
})
|
||||
wx.ready(() => {
|
||||
console.log('微信 SDK 就绪')
|
||||
configReady = true
|
||||
resolve()
|
||||
})
|
||||
wx.error((err) => {
|
||||
console.error('wx.config 失败', err)
|
||||
configReady = false
|
||||
configPromise = null
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
}).catch((e) => {
|
||||
console.error('获取微信签名失败', e)
|
||||
configPromise = null
|
||||
configReady = false
|
||||
throw e
|
||||
}
|
||||
})
|
||||
|
||||
return configPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起微信扫一扫
|
||||
* @returns {Promise<string>} 扫码结果字符串
|
||||
*/
|
||||
export function wxScan() {
|
||||
export async function wxScan() {
|
||||
await initWxConfig()
|
||||
const wx = getWx()
|
||||
if (!wx) {
|
||||
throw new Error('微信 SDK 未加载')
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.scanQRCode({
|
||||
needResult: 1,
|
||||
@@ -63,4 +107,4 @@ export function wxScan() {
|
||||
*/
|
||||
export function isInWechat() {
|
||||
return /micromessenger/i.test(navigator.userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const mpVerifyFiles = [
|
||||
'MP_verify_0UoX8yClVgREjgPj.txt',
|
||||
'MP_verify_4E1dVbOcZ9KzTzoc.txt',
|
||||
'MP_verify_yU2Z8mp831jh6QX7.txt'
|
||||
]
|
||||
|
||||
function copyMpVerifyFiles() {
|
||||
return {
|
||||
name: 'copy-mp-verify-files',
|
||||
closeBundle() {
|
||||
const outputDir = resolve(process.cwd(), 'dist/build/h5')
|
||||
|
||||
if (!existsSync(outputDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
mkdirSync(outputDir, { recursive: true })
|
||||
|
||||
mpVerifyFiles.forEach((file) => {
|
||||
const source = resolve(process.cwd(), file)
|
||||
|
||||
if (existsSync(source)) {
|
||||
copyFileSync(source, resolve(outputDir, file))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
plugins: [uni(), copyMpVerifyFiles()],
|
||||
root: process.cwd(),
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
@@ -11,4 +42,4 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user