feat(token): 新增修改 Token 接口及绑定记录视图切换功能

- 新增 apiTokenUpdatePost 接口支持修改 Token 信息
- 扩展 Token 数据结构,增加充值金额限制和次数限制相关字段
- 调整新增/编辑 Token 弹窗,支持编辑模式下的充值限制显示和输入
- 新增绑定记录卡片视图与表格视图切换功能
- 绑定记录视图显示总记录数和总金额统计信息
- 卡片视图改进绑定记录样式和交互,支持复制卡号和卡密
- 弹窗样式和分页控件优化,提升用户体验
- 修复并完善 Api 文档,补充修改 Token 接口说明和示例代码
This commit is contained in:
danial
2025-12-01 23:41:55 +08:00
parent 6f5b7680af
commit 246ff9f34e
17 changed files with 931 additions and 81 deletions

View File

@@ -63,6 +63,8 @@ docs/KamiApiCamelOilV1OrderListItem.md
docs/KamiApiCamelOilV1SubmitOrderReq.md docs/KamiApiCamelOilV1SubmitOrderReq.md
docs/KamiApiCamelOilV1SubmitOrderRes.md docs/KamiApiCamelOilV1SubmitOrderRes.md
docs/KamiApiCamelOilV1TokenInfo.md docs/KamiApiCamelOilV1TokenInfo.md
docs/KamiApiCamelOilV1UpdateTokenReq.md
docs/KamiApiCamelOilV1UpdateTokenRes.md
docs/KamiApiCardInfoAppleV1AppleCardListRecord.md docs/KamiApiCardInfoAppleV1AppleCardListRecord.md
docs/KamiApiCardInfoAppleV1AppleCardListRecordUploadUser.md docs/KamiApiCardInfoAppleV1AppleCardListRecordUploadUser.md
docs/KamiApiCardInfoAppleV1CallBackOrderManualReq.md docs/KamiApiCardInfoAppleV1CallBackOrderManualReq.md
@@ -494,6 +496,8 @@ models/kami-api-camel-oil-v1-order-list-item.ts
models/kami-api-camel-oil-v1-submit-order-req.ts models/kami-api-camel-oil-v1-submit-order-req.ts
models/kami-api-camel-oil-v1-submit-order-res.ts models/kami-api-camel-oil-v1-submit-order-res.ts
models/kami-api-camel-oil-v1-token-info.ts models/kami-api-camel-oil-v1-token-info.ts
models/kami-api-camel-oil-v1-update-token-req.ts
models/kami-api-camel-oil-v1-update-token-res.ts
models/kami-api-card-info-apple-v1-apple-card-list-record-upload-user.ts models/kami-api-card-info-apple-v1-apple-card-list-record-upload-user.ts
models/kami-api-card-info-apple-v1-apple-card-list-record.ts models/kami-api-card-info-apple-v1-apple-card-list-record.ts
models/kami-api-card-info-apple-v1-call-back-order-manual-req.ts models/kami-api-card-info-apple-v1-call-back-order-manual-req.ts

View File

@@ -52,6 +52,10 @@ import type { KamiApiCamelOilV1GetTokenRes } from '../models';
import type { KamiApiCamelOilV1ListCardBindingsByTokenRes } from '../models'; import type { KamiApiCamelOilV1ListCardBindingsByTokenRes } from '../models';
// @ts-ignore // @ts-ignore
import type { KamiApiCamelOilV1ListTokensRes } from '../models'; import type { KamiApiCamelOilV1ListTokensRes } from '../models';
// @ts-ignore
import type { KamiApiCamelOilV1UpdateTokenReq } from '../models';
// @ts-ignore
import type { KamiApiCamelOilV1UpdateTokenRes } from '../models';
/** /**
* JDV2TokenManagementApi - axios parameter creator * JDV2TokenManagementApi - axios parameter creator
*/ */
@@ -327,6 +331,54 @@ export const JDV2TokenManagementApiAxiosParamCreator = function (
...options.headers ...options.headers
}; };
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions
};
},
/**
*
* @summary 修改 Token
* @param {KamiApiCamelOilV1UpdateTokenReq} [kamiApiCamelOilV1UpdateTokenReq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiTokenUpdatePost: async (
kamiApiCamelOilV1UpdateTokenReq?: KamiApiCamelOilV1UpdateTokenReq,
options: RawAxiosRequestConfig = {}
): Promise<RequestArgs> => {
const localVarPath = `/api/token/update`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = {
method: 'POST',
...baseOptions,
...options
};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers
};
localVarRequestOptions.data = serializeDataIfNeeded(
kamiApiCamelOilV1UpdateTokenReq,
localVarRequestOptions,
configuration
);
return { return {
url: toPathString(localVarUrlObj), url: toPathString(localVarUrlObj),
options: localVarRequestOptions options: localVarRequestOptions
@@ -526,6 +578,40 @@ export const JDV2TokenManagementApiFp = function (
BASE_PATH, BASE_PATH,
configuration configuration
)(axios, localVarOperationServerBasePath || basePath); )(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary 修改 Token
* @param {KamiApiCamelOilV1UpdateTokenReq} [kamiApiCamelOilV1UpdateTokenReq]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiTokenUpdatePost(
kamiApiCamelOilV1UpdateTokenReq?: KamiApiCamelOilV1UpdateTokenReq,
options?: RawAxiosRequestConfig
): Promise<
(
axios?: AxiosInstance,
basePath?: string
) => AxiosPromise<KamiApiCamelOilV1UpdateTokenRes>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.apiTokenUpdatePost(
kamiApiCamelOilV1UpdateTokenReq,
options
);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath =
operationServerMap['JDV2TokenManagementApi.apiTokenUpdatePost']?.[
localVarOperationServerIndex
]?.url;
return (axios, basePath) =>
createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
)(axios, localVarOperationServerBasePath || basePath);
} }
}; };
}; };
@@ -631,6 +717,24 @@ export const JDV2TokenManagementApiFactory = function (
options options
) )
.then(request => request(axios, basePath)); .then(request => request(axios, basePath));
},
/**
*
* @summary 修改 Token
* @param {JDV2TokenManagementApiApiTokenUpdatePostRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiTokenUpdatePost(
requestParameters: JDV2TokenManagementApiApiTokenUpdatePostRequest = {},
options?: RawAxiosRequestConfig
): AxiosPromise<KamiApiCamelOilV1UpdateTokenRes> {
return localVarFp
.apiTokenUpdatePost(
requestParameters.kamiApiCamelOilV1UpdateTokenReq,
options
)
.then(request => request(axios, basePath));
} }
}; };
}; };
@@ -698,6 +802,18 @@ export interface JDV2TokenManagementApiInterface {
requestParameters: JDV2TokenManagementApiApiTokenListGetRequest, requestParameters: JDV2TokenManagementApiApiTokenListGetRequest,
options?: RawAxiosRequestConfig options?: RawAxiosRequestConfig
): AxiosPromise<KamiApiCamelOilV1ListTokensRes>; ): AxiosPromise<KamiApiCamelOilV1ListTokensRes>;
/**
*
* @summary 修改 Token
* @param {JDV2TokenManagementApiApiTokenUpdatePostRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiTokenUpdatePost(
requestParameters?: JDV2TokenManagementApiApiTokenUpdatePostRequest,
options?: RawAxiosRequestConfig
): AxiosPromise<KamiApiCamelOilV1UpdateTokenRes>;
} }
/** /**
@@ -769,6 +885,13 @@ export interface JDV2TokenManagementApiApiTokenListGetRequest {
readonly status?: number; readonly status?: number;
} }
/**
* Request parameters for apiTokenUpdatePost operation in JDV2TokenManagementApi.
*/
export interface JDV2TokenManagementApiApiTokenUpdatePostRequest {
readonly kamiApiCamelOilV1UpdateTokenReq?: KamiApiCamelOilV1UpdateTokenReq;
}
/** /**
* JDV2TokenManagementApi - object-oriented interface * JDV2TokenManagementApi - object-oriented interface
*/ */
@@ -872,6 +995,25 @@ export class JDV2TokenManagementApi
) )
.then(request => request(this.axios, this.basePath)); .then(request => request(this.axios, this.basePath));
} }
/**
*
* @summary 修改 Token
* @param {JDV2TokenManagementApiApiTokenUpdatePostRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public apiTokenUpdatePost(
requestParameters: JDV2TokenManagementApiApiTokenUpdatePostRequest = {},
options?: RawAxiosRequestConfig
) {
return JDV2TokenManagementApiFp(this.configuration)
.apiTokenUpdatePost(
requestParameters.kamiApiCamelOilV1UpdateTokenReq,
options
)
.then(request => request(this.axios, this.basePath));
}
} }
export enum ApiCardBindingListByTokenGetPageSizeEnum { export enum ApiCardBindingListByTokenGetPageSizeEnum {

View File

@@ -9,6 +9,7 @@ All URIs are relative to _http://localhost_
| [**apiTokenDeletePost**](#apitokendeletepost) | **POST** /api/token/delete | 删除 Token | | [**apiTokenDeletePost**](#apitokendeletepost) | **POST** /api/token/delete | 删除 Token |
| [**apiTokenGetGet**](#apitokengetget) | **GET** /api/token/get | 获取 Token 信息 | | [**apiTokenGetGet**](#apitokengetget) | **GET** /api/token/get | 获取 Token 信息 |
| [**apiTokenListGet**](#apitokenlistget) | **GET** /api/token/list | 列出 Token | | [**apiTokenListGet**](#apitokenlistget) | **GET** /api/token/list | 列出 Token |
| [**apiTokenUpdatePost**](#apitokenupdatepost) | **POST** /api/token/update | 修改 Token |
# **apiCardBindingListByTokenGet** # **apiCardBindingListByTokenGet**
@@ -260,3 +261,53 @@ No authorization required
| **200** | | - | | **200** | | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **apiTokenUpdatePost**
> KamiApiCamelOilV1UpdateTokenRes apiTokenUpdatePost()
### Example
```typescript
import {
JDV2TokenManagementApi,
Configuration,
KamiApiCamelOilV1UpdateTokenReq
} from './api';
const configuration = new Configuration();
const apiInstance = new JDV2TokenManagementApi(configuration);
let kamiApiCamelOilV1UpdateTokenReq: KamiApiCamelOilV1UpdateTokenReq; // (optional)
const { status, data } = await apiInstance.apiTokenUpdatePost(
kamiApiCamelOilV1UpdateTokenReq
);
```
### Parameters
| Name | Type | Description | Notes |
| ----------------------------------- | ----------------------------------- | ----------- | ----- |
| **kamiApiCamelOilV1UpdateTokenReq** | **KamiApiCamelOilV1UpdateTokenReq** | | |
### Return type
**KamiApiCamelOilV1UpdateTokenRes**
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
| ----------- | ----------- | ---------------- |
| **200** | | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -10,7 +10,7 @@
| **orderId** | **number** | 订单ID | [optional] [default to undefined] | | **orderId** | **number** | 订单ID | [optional] [default to undefined] |
| **cardNumber** | **string** | 卡号 | [optional] [default to undefined] | | **cardNumber** | **string** | 卡号 | [optional] [default to undefined] |
| **cardPassword** | **string** | 卡密 | [optional] [default to undefined] | | **cardPassword** | **string** | 卡密 | [optional] [default to undefined] |
| **amount** | **object** | | [optional] [default to undefined] | | **amount** | **number** | 绑定金额 | [optional] [default to undefined] |
| **createdAt** | **string** | 创建时间 | [optional] [default to undefined] | | **createdAt** | **string** | 创建时间 | [optional] [default to undefined] |
## Example ## Example

View File

@@ -2,12 +2,14 @@
## Properties ## Properties
| Name | Type | Description | Notes | | Name | Type | Description | Notes |
| -------------- | ---------- | ------------ | --------------------------------- | | ----------------------- | ---------- | ------------ | --------------------------------- |
| **tokenName** | **string** | Token名称 | [default to undefined] | | **tokenName** | **string** | Token名称 | [default to undefined] |
| **tokenValue** | **string** | Token值 | [default to undefined] | | **tokenValue** | **string** | Token值 | [default to undefined] |
| **phone** | **string** | 绑定的手机号 | [optional] [default to undefined] | | **phone** | **string** | 绑定的手机号 | [optional] [default to undefined] |
| **remark** | **string** | 备注 | [optional] [default to undefined] | | **remark** | **string** | 备注 | [optional] [default to undefined] |
| **rechargeLimitAmount** | **number** | 充值金额限制 | [default to undefined] |
| **rechargeLimitCount** | **number** | 充值次数限制 | [default to undefined] |
## Example ## Example
@@ -18,7 +20,9 @@ const instance: KamiApiCamelOilV1CreateTokenReq = {
tokenName, tokenName,
tokenValue, tokenValue,
phone, phone,
remark remark,
rechargeLimitAmount,
rechargeLimitCount
}; };
``` ```

View File

@@ -2,20 +2,23 @@
## Properties ## Properties
| Name | Type | Description | Notes | | Name | Type | Description | Notes |
| ------------------- | ---------- | -------------- | --------------------------------- | | ----------------------- | ---------- | -------------- | --------------------------------- |
| **id** | **number** | Token ID | [optional] [default to undefined] | | **id** | **number** | Token ID | [optional] [default to undefined] |
| **tokenName** | **string** | Token名称 | [optional] [default to undefined] | | **tokenName** | **string** | Token名称 | [optional] [default to undefined] |
| **tokenValue** | **string** | Token值 | [optional] [default to undefined] | | **tokenValue** | **string** | Token值 | [optional] [default to undefined] |
| **phone** | **string** | 绑定的手机号 | [optional] [default to undefined] | | **phone** | **string** | 绑定的手机号 | [optional] [default to undefined] |
| **status** | **number** | 状态 | [optional] [default to undefined] | | **status** | **number** | 状态 | [optional] [default to undefined] |
| **bindCount** | **number** | 已绑定卡密数量 | [optional] [default to undefined] | | **bindCount** | **number** | 已绑定卡密数量 | [optional] [default to undefined] |
| **totalBindAmount** | **object** | | [optional] [default to undefined] | | **totalBindAmount** | **number** | 累计绑定金额 | [optional] [default to undefined] |
| **lastBindAt** | **string** | 最后绑定时间 | [optional] [default to undefined] | | **totalRechargeAmount** | **number** | 总充值金额 | [optional] [default to undefined] |
| **lastUsedAt** | **string** | 最后使用时间 | [optional] [default to undefined] | | **rechargeLimitAmount** | **number** | 充值金额限制 | [optional] [default to undefined] |
| **remark** | **string** | 备注 | [optional] [default to undefined] | | **rechargeLimitCount** | **number** | 充值次数限制 | [optional] [default to undefined] |
| **createdAt** | **string** | 创建时间 | [optional] [default to undefined] | | **lastBindAt** | **string** | 最后绑定时间 | [optional] [default to undefined] |
| **updatedAt** | **string** | 更新时间 | [optional] [default to undefined] | | **lastUsedAt** | **string** | 最后使用时间 | [optional] [default to undefined] |
| **remark** | **string** | 备注 | [optional] [default to undefined] |
| **createdAt** | **string** | 创建时间 | [optional] [default to undefined] |
| **updatedAt** | **string** | 更新时间 | [optional] [default to undefined] |
## Example ## Example
@@ -30,6 +33,9 @@ const instance: KamiApiCamelOilV1TokenInfo = {
status, status,
bindCount, bindCount,
totalBindAmount, totalBindAmount,
totalRechargeAmount,
rechargeLimitAmount,
rechargeLimitCount,
lastBindAt, lastBindAt,
lastUsedAt, lastUsedAt,
remark, remark,

View File

@@ -0,0 +1,29 @@
# KamiApiCamelOilV1UpdateTokenReq
## Properties
| Name | Type | Description | Notes |
| ----------------------- | ---------- | ------------ | --------------------------------- |
| **tokenId** | **number** | Token ID | [default to undefined] |
| **tokenName** | **string** | Token名称 | [default to undefined] |
| **phone** | **string** | 绑定的手机号 | [optional] [default to undefined] |
| **remark** | **string** | 备注 | [optional] [default to undefined] |
| **rechargeLimitAmount** | **number** | 充值金额限制 | [default to undefined] |
| **rechargeLimitCount** | **number** | 充值次数限制 | [default to undefined] |
## Example
```typescript
import { KamiApiCamelOilV1UpdateTokenReq } from './api';
const instance: KamiApiCamelOilV1UpdateTokenReq = {
tokenId,
tokenName,
phone,
remark,
rechargeLimitAmount,
rechargeLimitCount
};
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,19 @@
# KamiApiCamelOilV1UpdateTokenRes
## Properties
| Name | Type | Description | Notes |
| ----------- | ---------- | ----------- | --------------------------------- |
| **message** | **string** | 信息 | [optional] [default to undefined] |
## Example
```typescript
import { KamiApiCamelOilV1UpdateTokenRes } from './api';
const instance: KamiApiCamelOilV1UpdateTokenRes = {
message
};
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -42,6 +42,8 @@ export * from './kami-api-camel-oil-v1-order-list-item';
export * from './kami-api-camel-oil-v1-submit-order-req'; export * from './kami-api-camel-oil-v1-submit-order-req';
export * from './kami-api-camel-oil-v1-submit-order-res'; export * from './kami-api-camel-oil-v1-submit-order-res';
export * from './kami-api-camel-oil-v1-token-info'; export * from './kami-api-camel-oil-v1-token-info';
export * from './kami-api-camel-oil-v1-update-token-req';
export * from './kami-api-camel-oil-v1-update-token-res';
export * from './kami-api-card-info-apple-v1-apple-card-list-record'; export * from './kami-api-card-info-apple-v1-apple-card-list-record';
export * from './kami-api-card-info-apple-v1-apple-card-list-record-upload-user'; export * from './kami-api-card-info-apple-v1-apple-card-list-record-upload-user';
export * from './kami-api-card-info-apple-v1-call-back-order-manual-req'; export * from './kami-api-card-info-apple-v1-call-back-order-manual-req';

View File

@@ -37,7 +37,10 @@ export interface KamiApiCamelOilV1CardBindingInfo {
* 卡密 * 卡密
*/ */
cardPassword?: string; cardPassword?: string;
amount?: object; /**
* 绑定金额
*/
amount?: number;
/** /**
* 创建时间 * 创建时间
*/ */

View File

@@ -29,4 +29,12 @@ export interface KamiApiCamelOilV1CreateTokenReq {
* 备注 * 备注
*/ */
remark?: string; remark?: string;
/**
* 充值金额限制
*/
rechargeLimitAmount: number;
/**
* 充值次数限制
*/
rechargeLimitCount: number;
} }

View File

@@ -37,7 +37,22 @@ export interface KamiApiCamelOilV1TokenInfo {
* 已绑定卡密数量 * 已绑定卡密数量
*/ */
bindCount?: number; bindCount?: number;
totalBindAmount?: object; /**
* 累计绑定金额
*/
totalBindAmount?: number;
/**
* 总充值金额
*/
totalRechargeAmount?: number;
/**
* 充值金额限制
*/
rechargeLimitAmount?: number;
/**
* 充值次数限制
*/
rechargeLimitCount?: number;
/** /**
* 最后绑定时间 * 最后绑定时间
*/ */

View File

@@ -0,0 +1,40 @@
/* tslint:disable */
/**
*
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document:
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface KamiApiCamelOilV1UpdateTokenReq {
/**
* Token ID
*/
tokenId: number;
/**
* Token名称
*/
tokenName: string;
/**
* 绑定的手机号
*/
phone?: string;
/**
* 备注
*/
remark?: string;
/**
* 充值金额限制
*/
rechargeLimitAmount: number;
/**
* 充值次数限制
*/
rechargeLimitCount: number;
}

View File

@@ -0,0 +1,20 @@
/* tslint:disable */
/**
*
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document:
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface KamiApiCamelOilV1UpdateTokenRes {
/**
* 信息
*/
message?: string;
}

View File

@@ -1,7 +1,7 @@
<template> <template>
<a-modal <a-modal
v-model:visible="modalVisible" v-model:visible="modalVisible"
title="新增Token" :title="isEditMode ? '编辑Token' : '新增Token'"
width="600px" width="600px"
@cancel="handleCancel" @cancel="handleCancel"
@before-ok="handleBeforeOk" @before-ok="handleBeforeOk"
@@ -22,7 +22,12 @@
show-word-limit show-word-limit
/> />
</a-form-item> </a-form-item>
<a-form-item field="tokenValue" label="Token值" required> <a-form-item
v-if="!isEditMode"
field="tokenValue"
label="Token值"
required
>
<a-textarea <a-textarea
v-model="formModel.tokenValue" v-model="formModel.tokenValue"
placeholder="请输入Token值" placeholder="请输入Token值"
@@ -47,6 +52,24 @@
show-word-limit show-word-limit
/> />
</a-form-item> </a-form-item>
<a-form-item field="rechargeLimitAmount" label="充值金额限制" required>
<a-input-number
v-model="formModel.rechargeLimitAmount"
placeholder="请输入充值金额限制"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
<a-form-item field="rechargeLimitCount" label="充值次数限制" required>
<a-input-number
v-model="formModel.rechargeLimitCount"
placeholder="请输入充值次数限制"
:min="0"
:precision="0"
style="width: 100%"
/>
</a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
</template> </template>
@@ -59,6 +82,7 @@ import { jdV2TokenClient } from '@/api/index.ts';
interface Props { interface Props {
visible: boolean; visible: boolean;
editData?: KamiApiCamelOilV1TokenInfo;
} }
interface Emits { interface Emits {
@@ -75,42 +99,68 @@ const modalVisible = computed({
set: value => emit('update:visible', value) set: value => emit('update:visible', value)
}); });
const isEditMode = computed(() => !!props.editData);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const generateFormModel = () => { const generateFormModel = () => {
return { return {
tokenId: 0,
tokenName: '', tokenName: '',
tokenValue: '', tokenValue: '',
phone: '', phone: '',
remark: '' remark: '',
rechargeLimitAmount: 0,
rechargeLimitCount: 0
}; };
}; };
const formModel = reactive(generateFormModel()); const formModel = reactive(generateFormModel());
const rules = { const rules = computed(() => ({
tokenName: [ tokenName: [
{ required: true, message: '请输入Token名称' }, { required: true, message: '请输入Token名称' },
{ min: 1, max: 100, message: 'Token名称长度为1-100个字符' } { min: 1, max: 100, message: 'Token名称长度为1-100个字符' }
], ],
tokenValue: [ tokenValue: [
{ required: true, message: '请输入Token值' }, { required: !isEditMode.value, message: '请输入Token值' },
{ min: 1, max: 1000, message: 'Token值长度为1-1000个字符' } { min: 1, max: 1000, message: 'Token值长度为1-1000个字符' }
], ],
phone: [ phone: [
{ required: true, message: '请输入绑定手机号' }, { required: true, message: '请输入绑定手机号' },
{ match: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' } { match: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
], ],
remark: [{ max: 500, message: '备注长度不能超过500个字符' }] remark: [{ max: 500, message: '备注长度不能超过500个字符' }],
}; rechargeLimitAmount: [
{ required: true, message: '请输入充值金额限制' },
{ type: 'number', min: 0, message: '充值金额限制不能小于0' }
],
rechargeLimitCount: [
{ required: true, message: '请输入充值次数限制' },
{ type: 'number', min: 0, message: '充值次数限制不能小于0' }
]
}));
// 监听弹窗显示状态,重置表单 // 监听弹窗显示状态和编辑数据,更新表单
watch( watch(
() => props.visible, [() => props.visible, () => props.editData],
visible => { ([visible, editData]) => {
if (visible) { if (visible) {
// 重置表单 if (editData && isEditMode.value) {
Object.assign(formModel, generateFormModel()); // 编辑模式,填充现有数据
Object.assign(formModel, {
tokenName: editData.tokenName || '',
tokenValue: editData.tokenValue || '',
phone: editData.phone || '',
remark: editData.remark || '',
rechargeLimitAmount: editData.rechargeLimitAmount || 0,
rechargeLimitCount: editData.rechargeLimitCount || 0,
tokenId: editData.id || 0
});
} else {
// 新增模式,重置表单
Object.assign(formModel, generateFormModel());
}
} }
}, },
{ immediate: true } { immediate: true }
@@ -127,27 +177,50 @@ const handleBeforeOk = async () => {
return false; return false;
} }
// 新增Token if (isEditMode.value) {
await jdV2TokenClient.apiTokenCreatePost({ // 编辑Token
kamiApiCamelOilV1CreateTokenReq: { await jdV2TokenClient.apiTokenUpdatePost({
tokenName: formModel.tokenName, kamiApiCamelOilV1UpdateTokenReq: {
tokenValue: formModel.tokenValue, tokenId: formModel.tokenId,
remark: formModel.remark tokenName: formModel.tokenName,
} phone: formModel.phone,
}); remark: formModel.remark,
rechargeLimitAmount: formModel.rechargeLimitAmount,
rechargeLimitCount: formModel.rechargeLimitCount
}
});
Notification.success({ Notification.success({
content: '新增Token成功', content: '编辑Token成功',
closable: true closable: true
}); });
} else {
// 新增Token
await jdV2TokenClient.apiTokenCreatePost({
kamiApiCamelOilV1CreateTokenReq: {
tokenName: formModel.tokenName,
tokenValue: formModel.tokenValue,
phone: formModel.phone,
remark: formModel.remark,
rechargeLimitAmount: formModel.rechargeLimitAmount,
rechargeLimitCount: formModel.rechargeLimitCount
}
});
Notification.success({
content: '新增Token成功',
closable: true
});
}
emit('success'); emit('success');
emit('update:visible', false); emit('update:visible', false);
return true; return true;
} catch (err) { } catch (err) {
console.error('新增Token失败:', err); const action = isEditMode.value ? '编辑' : '新增';
console.error(`${action}Token失败:`, err);
Notification.error({ Notification.error({
content: '新增Token失败', content: `${action}Token失败`,
closable: true closable: true
}); });
return false; return false;

View File

@@ -2,40 +2,188 @@
<a-modal <a-modal
v-model:visible="modalVisible" v-model:visible="modalVisible"
:title="`Token绑卡记录 - ${tokenName}`" :title="`Token绑卡记录 - ${tokenName}`"
width="80%" width="90%"
:footer="false" :footer="false"
@cancel="handleCancel" @cancel="handleCancel"
> >
<a-table <!-- 视图切换和统计信息 -->
:loading="loading" <div class="modal-header">
:pagination="{ <div class="view-controls">
current: pagination.current, <a-radio-group v-model="viewMode" type="button" size="small">
pageSize: pagination.pageSize, <a-radio value="table">
total: pagination.total, <template #radio="{ checked }">
pageSizeOptions: [10, 20, 50, 100], <a-button :type="checked ? 'primary' : 'secondary'" size="small">
showPageSize: true, <template #icon>
showTotal: true <icon-list />
}" </template>
:columns="columns" 表格视图
:data="renderData" </a-button>
:scroll="{ x: 1200 }" </template>
@page-change="onPageChange" </a-radio>
@page-size-change="onPageSizeChange" <a-radio value="card">
> <template #radio="{ checked }">
<template #createdAt="{ record }"> <a-button :type="checked ? 'primary' : 'secondary'" size="small">
{{ formatDateTime(record.createdAt) }} <template #icon>
</template> <icon-apps />
</template>
卡片视图
</a-button>
</template>
</a-radio>
</a-radio-group>
</div>
<template #amount="{ record }"> <div class="stats-info">
{{ formatAmount(record.amount) }} <a-space size="large">
</template> <a-statistic title="总记录数" :value="pagination.total" />
</a-table> <a-statistic
title="总金额"
:value="totalAmount"
:precision="2"
prefix="¥"
/>
</a-space>
</div>
</div>
<!-- 表格视图 -->
<div v-if="viewMode === 'table'" class="table-view">
<a-table
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
pageSizeOptions: [10, 20, 50, 100],
showPageSize: true,
showTotal: true
}"
:columns="columns"
:data="renderData"
:scroll="{ x: 1200 }"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #createdAt="{ record }">
{{ formatDateTime(record.createdAt) }}
</template>
<template #amount="{ record }">
<a-tag :color="getAmountColor(record.amount)">
¥{{ formatAmount(record.amount) }}
</a-tag>
</template>
<template #cardNumber="{ record }">
<a-typography-text
:copyable="{ text: record.cardNumber }"
:ellipsis="{ rows: 1, showTooltip: true }"
>
{{ record.cardNumber }}
</a-typography-text>
</template>
<template #cardPassword="{ record }">
<a-typography-text
:copyable="{ text: record.cardPassword }"
:ellipsis="{ rows: 1, showTooltip: true }"
>
{{ record.cardPassword }}
</a-typography-text>
</template>
</a-table>
</div>
<!-- 卡片视图 -->
<div v-else class="card-view">
<a-spin :loading="loading" style="width: 100%">
<div class="card-grid">
<a-card
v-for="record in renderData"
:key="record.id"
class="binding-card"
:hoverable="true"
>
<template #header>
<div class="card-header">
<div class="card-title">
<a-tag color="blue" size="small">ID: {{ record.id }}</a-tag>
<a-tag v-if="record.orderId" color="green" size="small">
订单: {{ record.orderId }}
</a-tag>
</div>
<div class="card-amount">
<a-statistic
:value="record.amount"
:precision="2"
prefix="¥"
:value-style="{ color: '#00b42a', fontSize: '16px' }"
/>
</div>
</div>
</template>
<div class="card-content">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="卡号">
<a-typography-text
:copyable="{ text: record.cardNumber }"
:ellipsis="{ rows: 1, showTooltip: true }"
class="copy-text"
>
{{ record.cardNumber }}
</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="卡密">
<a-typography-text
:copyable="{ text: record.cardPassword }"
:ellipsis="{ rows: 1, showTooltip: true }"
class="copy-text"
>
{{ record.cardPassword }}
</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="创建时间">
<a-typography-text type="secondary">
{{ formatDateTime(record.createdAt) }}
</a-typography-text>
</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
</div>
<!-- 分页控制 -->
<div v-if="pagination.total > 0" class="card-pagination">
<a-pagination
:current="pagination.current"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-size-options="[10, 20, 50, 100]"
show-page-size
show-total
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
<!-- 空状态 -->
<a-empty
v-if="!loading && renderData.length === 0"
description="暂无绑卡记录"
/>
</a-spin>
</div>
</a-modal> </a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'; import { ref, reactive, computed, watch } from 'vue';
import { TableColumnData } from '@arco-design/web-vue'; import { TableColumnData } from '@arco-design/web-vue';
import IconList from '@arco-design/web-vue/es/icon/icon-list';
import IconApps from '@arco-design/web-vue/es/icon/icon-apps';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global'; import { Pagination } from '@/types/global';
import { import {
@@ -138,6 +286,24 @@ const columns: TableColumnData[] = [
const { loading, setLoading } = useLoading(true); const { loading, setLoading } = useLoading(true);
const renderData = ref<KamiApiCamelOilV1CardBindingInfo[]>([]); const renderData = ref<KamiApiCamelOilV1CardBindingInfo[]>([]);
// 视图模式
const viewMode = ref<'table' | 'card'>('table');
// 计算总金额
const totalAmount = computed(() => {
return renderData.value.reduce((sum, record) => {
return sum + (Number(record.amount) || 0);
}, 0);
});
// 根据金额获取颜色
const getAmountColor = (amount: number | undefined): string => {
if (!amount) return 'gray';
if (amount < 100) return 'orange';
if (amount < 500) return 'blue';
return 'green';
};
const fetchData = async (params: any = { current: 1, pageSize: 20 }) => { const fetchData = async (params: any = { current: 1, pageSize: 20 }) => {
if (!props.tokenId) { if (!props.tokenId) {
return; return;
@@ -211,10 +377,134 @@ watch(
</script> </script>
<style scoped> <style scoped>
/* 使用 Arco Design 的原生样式,确保主题兼容性 */ /* 响应式设计 */
@media (width <= 768px) {
.card-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.modal-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.view-controls {
justify-content: center;
}
.stats-info {
justify-content: center;
}
.card-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
}
@media (width <= 480px) {
.card-grid {
grid-template-columns: 1fr;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--color-bg-2);
border-radius: 6px;
border: 1px solid var(--color-border-2);
}
.view-controls {
display: flex;
align-items: center;
}
.stats-info {
display: flex;
align-items: center;
}
/* 表格视图样式 */
.table-view {
margin-top: 16px;
}
/* 卡片视图样式 */
.card-view {
margin-top: 16px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 16px;
margin-bottom: 20px;
min-height: 200px;
}
.binding-card {
transition: all 0.2s ease;
border: 1px solid var(--color-border-2);
}
.binding-card:hover {
border-color: var(--color-primary-light-4);
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.card-title {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.card-amount {
display: flex;
align-items: center;
}
.card-content {
padding: 0;
}
.copy-text {
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
font-size: 13px;
background: var(--color-fill-2);
padding: 2px 6px;
border-radius: 4px;
word-break: break-all;
}
.card-pagination {
display: flex;
justify-content: center;
margin-top: 24px;
padding: 16px 0;
}
/* 模态框样式优化 */
:deep(.arco-modal-body) { :deep(.arco-modal-body) {
max-height: 70vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
padding: 20px 24px;
/* 确保滚动条样式跟随主题 */ /* 确保滚动条样式跟随主题 */
scrollbar-width: thin; scrollbar-width: thin;
@@ -222,15 +512,96 @@ watch(
} }
:deep(.arco-modal-body::-webkit-scrollbar) { :deep(.arco-modal-body::-webkit-scrollbar) {
width: 6px; width: 8px;
} }
:deep(.arco-modal-body::-webkit-scrollbar-thumb) { :deep(.arco-modal-body::-webkit-scrollbar-thumb) {
background-color: var(--color-border-3); background-color: var(--color-border-3);
border-radius: 3px; border-radius: 4px;
} }
:deep(.arco-modal-body::-webkit-scrollbar-track) { :deep(.arco-modal-body::-webkit-scrollbar-track) {
background-color: transparent; background-color: transparent;
} }
/* 表格优化 */
:deep(.arco-table) {
border-radius: 6px;
overflow: hidden;
}
:deep(.arco-table-th) {
background: var(--color-bg-2);
font-weight: 600;
}
:deep(.arco-table-container) {
border: 1px solid var(--color-border-2);
}
/* 单选按钮组样式 */
:deep(.arco-radio-group) {
background: transparent;
}
:deep(.arco-radio-button) {
background: var(--color-bg-2);
border-color: var(--color-border-2);
}
:deep(.arco-radio-button:hover) {
border-color: var(--color-primary-light-4);
}
/* 统计信息样式 */
:deep(.arco-statistic) {
text-align: center;
}
:deep(.arco-statistic-title) {
font-size: 14px;
color: var(--color-text-3);
margin-bottom: 4px;
}
:deep(.arco-statistic-value) {
font-size: 20px;
font-weight: 600;
}
/* 卡片描述列表样式 */
:deep(.arco-descriptions-item-label) {
font-weight: 500;
color: var(--color-text-2);
min-width: 60px;
}
:deep(.arco-descriptions-item-value) {
color: var(--color-text-1);
}
/* 加载状态优化 */
:deep(.arco-spin-container) {
min-height: 300px;
display: flex;
flex-direction: column;
}
/* 空状态样式 */
:deep(.arco-empty) {
margin: 40px 0;
}
/* 标签样式优化 */
:deep(.arco-tag) {
border-radius: 4px;
font-weight: 500;
}
/* 按钮图标样式 */
:deep(.arco-btn .arco-icon) {
margin-right: 4px;
}
/* 模态框头部样式 */
</style> </style>

View File

@@ -85,7 +85,7 @@
}" }"
:columns="columns" :columns="columns"
:data="renderData" :data="renderData"
:scroll="{ x: 1200 }" :scroll="{ x: 1800 }"
@page-change="onPageChange" @page-change="onPageChange"
@page-size-change="onPageSizeChange" @page-size-change="onPageSizeChange"
> >
@@ -196,12 +196,60 @@ const columns: TableColumnData[] = [
dataIndex: 'bindCount', dataIndex: 'bindCount',
width: 100 width: 100
}, },
{
title: '累计绑定金额',
dataIndex: 'totalBindAmount',
width: 120,
render: ({ record }) => {
if (!record.totalBindAmount) return '-';
return `¥${Number(record.totalBindAmount).toFixed(2)}`;
}
},
{
title: '总充值金额',
dataIndex: 'totalRechargeAmount',
width: 120,
render: ({ record }) => {
if (!record.totalRechargeAmount) return '-';
return `¥${Number(record.totalRechargeAmount).toFixed(2)}`;
}
},
{
title: '充值限制',
dataIndex: 'rechargeLimitAmount',
width: 110,
render: ({ record }) => {
if (!record.rechargeLimitAmount) return '-';
return `¥${Number(record.rechargeLimitAmount).toFixed(2)}`;
}
},
{
title: '次数限制',
dataIndex: 'rechargeLimitCount',
width: 100,
render: ({ record }) => {
return record.rechargeLimitCount || '-';
}
},
{
title: '最后绑定时间',
dataIndex: 'lastBindAt',
width: 180,
ellipsis: true,
tooltip: true,
render: ({ record }) => {
return formatDateTime(record.lastBindAt);
}
},
{ {
title: '最后使用时间', title: '最后使用时间',
dataIndex: 'lastUsedAt', dataIndex: 'lastUsedAt',
width: 180, width: 180,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
render: ({ record }) => {
return formatDateTime(record.lastUsedAt);
}
}, },
{ {
title: '状态', title: '状态',
@@ -219,7 +267,10 @@ const columns: TableColumnData[] = [
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createdAt', dataIndex: 'createdAt',
width: 180 width: 180,
render: ({ record }) => {
return formatDateTime(record.createdAt);
}
}, },
{ {
title: '操作', title: '操作',
@@ -341,6 +392,18 @@ const statusMapper = (status: number): { text: string; color: string } => {
} }
}; };
const formatDateTime = (dateTime: string | undefined): string => {
if (!dateTime) return '-';
return new Date(dateTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
const download = () => { const download = () => {
// TODO: 实现导出功能 // TODO: 实现导出功能
Notification.info({ Notification.info({