Compare commits
24 Commits
427b211295
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90a51ebf4 | ||
|
|
5b239c13b7 | ||
|
|
8397a51f8c | ||
|
|
a9fbbea1e6 | ||
|
|
6756ad5700 | ||
|
|
f581ccaa9b | ||
|
|
832c2852c7 | ||
|
|
3e81a216ad | ||
|
|
85cdeac804 | ||
|
|
59e4e1203e | ||
|
|
4dc86889c5 | ||
| 60fa91ea25 | |||
|
|
50e4be4976 | ||
|
|
5d78f24366 | ||
|
|
069eab0462 | ||
|
|
c265dacafd | ||
|
|
83c32279eb | ||
|
|
169d2bdaad | ||
|
|
368c0d75e7 | ||
|
|
a0b2f69a8e | ||
|
|
b566b0a5ea | ||
|
|
4d9aa59254 | ||
|
|
1edd299f1b | ||
|
|
f9fead83cf |
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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": "直接换货"
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
|
||||
47
main.js
47
main.js
@@ -1,11 +1,55 @@
|
||||
import App from './App'
|
||||
import uviewPlus from 'uview-plus'
|
||||
|
||||
const TAB_BAR_STYLE_NOT_FOUND_ERROR = 'setTabBarStyle:fail not TabBar page'
|
||||
|
||||
const shouldIgnoreTabBarStyleError = (error) => {
|
||||
const message = typeof error === 'string'
|
||||
? error
|
||||
: error?.errMsg || error?.message || ''
|
||||
return message.includes(TAB_BAR_STYLE_NOT_FOUND_ERROR)
|
||||
}
|
||||
|
||||
const patchSafeSetTabBarStyle = () => {
|
||||
if (typeof uni === 'undefined' || typeof uni.setTabBarStyle !== 'function') {
|
||||
return
|
||||
}
|
||||
|
||||
if (uni.setTabBarStyle.__upSafePatched) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalSetTabBarStyle = uni.setTabBarStyle.bind(uni)
|
||||
const safeSetTabBarStyle = (options = {}) => {
|
||||
try {
|
||||
const result = originalSetTabBarStyle(options)
|
||||
if (result && typeof result.catch === 'function') {
|
||||
return result.catch((error) => {
|
||||
if (shouldIgnoreTabBarStyleError(error)) {
|
||||
return undefined
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (shouldIgnoreTabBarStyleError(error)) {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
safeSetTabBarStyle.__upSafePatched = true
|
||||
uni.setTabBarStyle = safeSetTabBarStyle
|
||||
}
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
patchSafeSetTabBarStyle()
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
@@ -15,6 +59,7 @@ app.$mount()
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
patchSafeSetTabBarStyle()
|
||||
const app = createSSRApp(App)
|
||||
// 使用 uview-plus (版本 3.4.0+)
|
||||
app.use(uviewPlus, () => {
|
||||
@@ -34,4 +79,4 @@ export function createApp() {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// #endif
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
## 1. Shared Payment Branching
|
||||
|
||||
- [ ] 1.1 Add shared front-end handling for Alipay `payment_link` responses alongside the existing WeChat `pay_config` flow
|
||||
- [ ] 1.2 Ensure Alipay requests do not depend on `app_type` and existing WeChat requests continue to work unchanged
|
||||
- [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
|
||||
|
||||
- [ ] 2.1 Add Alipay as a selectable payment method in `pages/package-order/package-order.vue`
|
||||
- [ ] 2.2 Handle normal package order payment responses for `wechat`, `wallet`, and `alipay`
|
||||
- [ ] 2.3 Handle strong recharge branches that return `payment_link` for Alipay
|
||||
- [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
|
||||
|
||||
- [ ] 3.1 Add an order-list payment entry that lets the user continue pending orders with Alipay
|
||||
- [ ] 3.2 Refresh order list state from backend data after the user completes or retries the Alipay 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
|
||||
|
||||
- [ ] 4.1 Add Alipay as a selectable payment method in the wallet recharge popup
|
||||
- [ ] 4.2 Handle Alipay recharge creation responses in `confirmRecharge`
|
||||
- [ ] 4.3 Handle Alipay payment retries for pending recharge orders in `handleRechargePayment`
|
||||
- [ ] 4.4 Refresh wallet detail and recharge list state from backend data after the Alipay 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
|
||||
|
||||
- [ ] 5.1 Verify WeChat package payment still uses `pay_config`
|
||||
- [ ] 5.2 Verify wallet-balance package payment remains unchanged
|
||||
- [ ] 5.3 Verify Alipay package payment, order-list payment, wallet recharge, and recharge retry can all open a valid payment link flow
|
||||
- [ ] 5.4 Verify payment success is determined by refreshed backend status instead of the link jump result
|
||||
- [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
|
||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "c-xmot-iot",
|
||||
"name": "device-voice-h5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -66,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",
|
||||
@@ -1917,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",
|
||||
@@ -3268,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",
|
||||
@@ -3923,6 +3913,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5724,6 +5715,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6130,6 +6122,7 @@
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -6232,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",
|
||||
@@ -6620,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",
|
||||
@@ -6696,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",
|
||||
@@ -6967,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",
|
||||
@@ -7022,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')" />
|
||||
@@ -216,19 +216,10 @@
|
||||
return dateStr.split('T')[0] || '-';
|
||||
};
|
||||
|
||||
const resolveAssetRealName = (assetData) => {
|
||||
const cards = Array.isArray(assetData?.cards) ? assetData.cards : [];
|
||||
|
||||
if (cards.length === 1) {
|
||||
return !!cards[0]?.real_name_at;
|
||||
}
|
||||
|
||||
if (cards.length > 1) {
|
||||
const currentCard = cards.find(card => card.is_current);
|
||||
return !!currentCard?.real_name_at;
|
||||
}
|
||||
|
||||
return assetData?.real_name_status === 1;
|
||||
const resolveCurrentCard = (cards = []) => {
|
||||
if (cards.length === 1) return cards[0];
|
||||
if (cards.length > 1) return cards.find(card => card.is_current) || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
const loadAssetInfo = async () => {
|
||||
@@ -251,7 +242,8 @@
|
||||
deviceInfo.expireDate = formatDate(data.current_package_expires_at);
|
||||
deviceInfo.iccid = data.iccid || '-';
|
||||
deviceInfo.walletBalance = data.wallet_balance ?? 0;
|
||||
isRealName.value = resolveAssetRealName(data);
|
||||
const currentCard = resolveCurrentCard(data.cards || []);
|
||||
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;
|
||||
@@ -259,10 +251,8 @@
|
||||
// 流量信息已由 TrafficCard 组件独立获取,这里不再处理
|
||||
|
||||
// 当前卡信息
|
||||
let currentCard = null;
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
currentCard = data.cards.find(card => card.is_current) || '';
|
||||
deviceInfo.currentIccid = currentCard.iccid || '-';
|
||||
deviceInfo.currentIccid = currentCard?.iccid || '-';
|
||||
|
||||
// 卡列表
|
||||
deviceInfo.mchList = data.cards.map(card => ({
|
||||
@@ -766,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);
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
</view>
|
||||
|
||||
<view class="payment-methods">
|
||||
<view class="method-item" :class="{ active: rechargePaymentMethod === 'wechat' }" @tap="selectRechargePaymentMethod('wechat')">
|
||||
<view v-if="isDeviceAsset" class="method-item" :class="{ active: rechargePaymentMethod === 'wechat' }" @tap="selectRechargePaymentMethod('wechat')">
|
||||
<view class="method-left">
|
||||
<image class="method-icon" src="/static/wechat.png" mode="aspectFit"></image>
|
||||
<text class="method-name">微信支付</text>
|
||||
@@ -163,7 +163,7 @@
|
||||
<view class="method-radio" :class="{ checked: rechargePaymentMethod === 'wechat' }"></view>
|
||||
</view>
|
||||
|
||||
<view class="method-item" :class="{ active: rechargePaymentMethod === 'alipay' }" @tap="selectRechargePaymentMethod('alipay')">
|
||||
<view v-else class="method-item" :class="{ active: rechargePaymentMethod === 'alipay' }" @tap="selectRechargePaymentMethod('alipay')">
|
||||
<view class="method-left">
|
||||
<view class="method-icon method-badge method-badge-alipay">支</view>
|
||||
<text class="method-name">支付宝支付</text>
|
||||
@@ -186,9 +186,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { walletApi } from '@/api/index.js';
|
||||
import { assetApi, walletApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import {
|
||||
consumePendingPaymentRefresh,
|
||||
@@ -229,10 +229,13 @@
|
||||
const rechargePaymentMethod = ref('wechat');
|
||||
const rechargeSubmitting = ref(false);
|
||||
const rechargeOrderSubmittingKey = ref(null);
|
||||
const paymentMethodOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝支付', value: 'alipay' }
|
||||
];
|
||||
const isDeviceAsset = ref(true);
|
||||
let assetTypePromise = null;
|
||||
const paymentMethodOptions = computed(() => [
|
||||
isDeviceAsset.value
|
||||
? { label: '微信支付', value: 'wechat' }
|
||||
: { label: '支付宝支付', value: 'alipay' }
|
||||
]);
|
||||
const rechargeAmounts = [
|
||||
{ value: 1000, label: '10' },
|
||||
{ value: 2000, label: '20' },
|
||||
@@ -305,6 +308,34 @@
|
||||
return rechargeOrderSubmittingKey.value === getRechargeOrderSubmitKey(rechargeOrder);
|
||||
};
|
||||
|
||||
const getDefaultRechargePaymentMethod = () => isDeviceAsset.value ? 'wechat' : 'alipay';
|
||||
|
||||
const normalizeRechargePaymentMethod = () => {
|
||||
const defaultMethod = getDefaultRechargePaymentMethod();
|
||||
if (rechargePaymentMethod.value !== defaultMethod) {
|
||||
rechargePaymentMethod.value = defaultMethod;
|
||||
}
|
||||
};
|
||||
|
||||
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';
|
||||
normalizeRechargePaymentMethod();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载资产类型失败', error);
|
||||
})
|
||||
.finally(() => {
|
||||
assetTypePromise = null;
|
||||
});
|
||||
|
||||
return assetTypePromise;
|
||||
};
|
||||
|
||||
const resetRechargeListAndLoad = () => {
|
||||
rechargePage.value = 1;
|
||||
rechargeNoMore.value = false;
|
||||
@@ -318,6 +349,7 @@
|
||||
};
|
||||
|
||||
const syncWalletStatus = () => {
|
||||
loadAssetType();
|
||||
loadWalletDetail();
|
||||
resetRechargeListAndLoad();
|
||||
resetTransactionListAndLoad();
|
||||
@@ -337,8 +369,9 @@
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const openRechargeModal = () => {
|
||||
rechargePaymentMethod.value = 'wechat';
|
||||
const openRechargeModal = async () => {
|
||||
await loadAssetType();
|
||||
rechargePaymentMethod.value = getDefaultRechargePaymentMethod();
|
||||
showRechargeModal.value = true;
|
||||
};
|
||||
|
||||
@@ -354,6 +387,7 @@
|
||||
};
|
||||
|
||||
const selectRechargePaymentMethod = (method) => {
|
||||
if (method !== getDefaultRechargePaymentMethod()) return;
|
||||
rechargePaymentMethod.value = method;
|
||||
};
|
||||
|
||||
@@ -602,13 +636,15 @@
|
||||
}
|
||||
};
|
||||
|
||||
const showRechargePaymentMethods = (rechargeOrder) => {
|
||||
const showRechargePaymentMethods = async (rechargeOrder) => {
|
||||
if (rechargeOrderSubmittingKey.value !== null) return;
|
||||
await loadAssetType();
|
||||
const options = paymentMethodOptions.value;
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: paymentMethodOptions.map((item) => item.label),
|
||||
itemList: options.map((item) => item.label),
|
||||
success: ({ tapIndex }) => {
|
||||
const selectedMethod = paymentMethodOptions[tapIndex]?.value;
|
||||
const selectedMethod = options[tapIndex]?.value;
|
||||
if (selectedMethod) {
|
||||
handleRechargePayment(rechargeOrder, selectedMethod);
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { orderApi } from '@/api/index.js';
|
||||
import { assetApi, orderApi } from '@/api/index.js';
|
||||
import { useUserStore } from '@/store/index.js';
|
||||
import {
|
||||
consumePendingPaymentRefresh,
|
||||
@@ -97,10 +97,14 @@
|
||||
const orderPayingId = ref(null);
|
||||
const pageSize = 10;
|
||||
const filterIndex = ref(0);
|
||||
const paymentMethodOptions = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝支付', value: 'alipay' }
|
||||
];
|
||||
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 },
|
||||
@@ -124,6 +128,24 @@
|
||||
return classMap[status] || '';
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -203,13 +225,15 @@
|
||||
|
||||
const isOrderPaying = (order) => orderPayingId.value === order.order_id;
|
||||
|
||||
const showOrderPaymentMethods = (order) => {
|
||||
const showOrderPaymentMethods = async (order) => {
|
||||
if (orderPayingId.value !== null || !order?.order_id) return;
|
||||
await loadAssetType();
|
||||
const options = paymentMethodOptions.value;
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: paymentMethodOptions.map((item) => item.label),
|
||||
itemList: options.map((item) => item.label),
|
||||
success: ({ tapIndex }) => {
|
||||
const selectedMethod = paymentMethodOptions[tapIndex]?.value;
|
||||
const selectedMethod = options[tapIndex]?.value;
|
||||
if (selectedMethod) {
|
||||
handleOrderPayment(order, selectedMethod);
|
||||
}
|
||||
@@ -230,6 +254,14 @@
|
||||
const payData = await orderApi.pay(order.order_id, paymentMethod);
|
||||
uni.hideLoading();
|
||||
|
||||
if (paymentMethod === 'wallet') {
|
||||
showPaymentToast(true, '支付成功');
|
||||
setTimeout(() => {
|
||||
resetOrderListAndLoad();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (paymentMethod === 'wechat') {
|
||||
if (!isValidWechatPayConfig(payData?.pay_config)) {
|
||||
uni.showToast({
|
||||
@@ -281,6 +313,7 @@
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadAssetType();
|
||||
loadOrderList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,19 +7,28 @@
|
||||
</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>
|
||||
@@ -50,7 +59,7 @@
|
||||
</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>
|
||||
@@ -58,7 +67,7 @@
|
||||
<view class="method-radio" :class="{ checked: paymentMethod === 'wechat' }"></view>
|
||||
</view>
|
||||
|
||||
<view class="method-item" :class="{ active: paymentMethod === 'alipay' }" @tap="selectPaymentMethod('alipay')">
|
||||
<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>
|
||||
@@ -111,6 +120,8 @@
|
||||
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';
|
||||
@@ -124,6 +135,40 @@
|
||||
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 () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -145,17 +190,20 @@
|
||||
};
|
||||
|
||||
const syncPackagePageState = () => {
|
||||
loadAssetType();
|
||||
loadPackages();
|
||||
loadWalletBalance();
|
||||
};
|
||||
|
||||
const buyPackage = (item) => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -194,7 +242,9 @@
|
||||
};
|
||||
|
||||
const getCreateOrderPaymentMethod = () => {
|
||||
return paymentMethod.value === 'alipay' ? 'alipay' : undefined;
|
||||
if (paymentMethod.value === 'alipay') return 'alipay';
|
||||
if (paymentMethod.value === 'wallet' && !isDeviceAsset.value) return 'alipay';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const confirmPay = async () => {
|
||||
@@ -404,56 +454,117 @@
|
||||
|
||||
.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);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
.package-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.validity-tag {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 4rpx 12rpx;
|
||||
background: var(--gray-100);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
line-height: 1.35;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.package-data {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
letter-spacing: -1rpx;
|
||||
.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-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-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;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid var(--border-light);
|
||||
|
||||
.package-price {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
131
utils/payment.js
131
utils/payment.js
@@ -1,4 +1,5 @@
|
||||
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',
|
||||
@@ -13,8 +14,22 @@ export function isValidWechatPayConfig(payConfig) {
|
||||
}
|
||||
|
||||
export function getAlipayPaymentUrl(paymentLink) {
|
||||
if (!paymentLink) return '';
|
||||
return paymentLink.copy_link || paymentLink.qr_link || '';
|
||||
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) {
|
||||
@@ -39,40 +54,108 @@ export function consumePendingPaymentRefresh(target) {
|
||||
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 paymentUrl = getAlipayPaymentUrl(paymentLink);
|
||||
const { copyUrl, qrUrl } = getAlipayPaymentUrls(paymentLink);
|
||||
|
||||
if (!paymentUrl) {
|
||||
reject(new Error('Invalid payment link'));
|
||||
if (!copyUrl && !qrUrl) {
|
||||
reject(new Error('\u652f\u4ed8\u94fe\u63a5\u65e0\u6548'));
|
||||
return;
|
||||
}
|
||||
|
||||
markPendingPaymentRefresh(refreshTarget);
|
||||
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
window.location.href = paymentUrl;
|
||||
resolve({ redirecting: true });
|
||||
return;
|
||||
}
|
||||
|
||||
uni.setClipboardData({
|
||||
data: paymentUrl,
|
||||
success: () => resolve({ copied: true }),
|
||||
fail: () => reject(new Error('Unable to open payment link in this environment'))
|
||||
uni.setStorageSync(ALIPAY_PAYMENT_DATA_KEY, {
|
||||
copyUrl,
|
||||
qrUrl,
|
||||
refreshTarget,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
navigateToAlipayPaymentPage(getAlipayPaymentPageUrl(refreshTarget, copyUrl, qrUrl), resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function wechatH5Pay(payConfig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!isValidWechatPayConfig(payConfig)) {
|
||||
reject(new Error('Invalid payment params'));
|
||||
reject(new Error('\u652f\u4ed8\u53c2\u6570\u65e0\u6548'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
reject(new Error('Please open in WeChat'));
|
||||
reject(new Error('\u8bf7\u5728\u5fae\u4fe1\u4e2d\u6253\u5f00'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,17 +170,17 @@ export function wechatH5Pay(payConfig) {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
resolve({
|
||||
success: true,
|
||||
message: 'Payment successful'
|
||||
message: '\u652f\u4ed8\u6210\u529f'
|
||||
});
|
||||
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||
reject({
|
||||
cancelled: true,
|
||||
message: 'Payment cancelled'
|
||||
message: '\u5df2\u53d6\u6d88\u652f\u4ed8'
|
||||
});
|
||||
} else {
|
||||
reject({
|
||||
success: false,
|
||||
message: 'Payment failed',
|
||||
message: '\u652f\u4ed8\u5931\u8d25',
|
||||
error: res
|
||||
});
|
||||
}
|
||||
@@ -107,7 +190,7 @@ export function wechatH5Pay(payConfig) {
|
||||
|
||||
export function showPaymentToast(success, message) {
|
||||
uni.showToast({
|
||||
title: message || (success ? 'Payment successful' : 'Payment failed'),
|
||||
title: message || (success ? '\u652f\u4ed8\u6210\u529f' : '\u652f\u4ed8\u5931\u8d25'),
|
||||
icon: success ? 'success' : 'none',
|
||||
duration: 2000
|
||||
});
|
||||
@@ -115,9 +198,9 @@ export function showPaymentToast(success, message) {
|
||||
|
||||
export function handlePaymentError(error) {
|
||||
if (error.cancelled) {
|
||||
showPaymentToast(false, 'Payment cancelled');
|
||||
showPaymentToast(false, '\u5df2\u53d6\u6d88\u652f\u4ed8');
|
||||
return;
|
||||
}
|
||||
|
||||
showPaymentToast(false, error.message || 'Payment failed');
|
||||
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