Merge remote-tracking branch 'yudao/dev' into dev

pull/117/head
jason 2025-05-27 18:43:18 +08:00
commit 8e111921dd
61 changed files with 2427 additions and 788 deletions

View File

@ -8,6 +8,7 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
/** 手机号正则表达式(中国) */
const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/;
async function initSetupVbenForm() {
@ -68,4 +69,3 @@ export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };
export type FormSchemaGetter = () => VbenFormSchema[];

View File

@ -268,8 +268,8 @@ setupVbenVxeTable({
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
// add by 星语:数量格式化,例如说:金额
vxeUI.formats.add('formatAmount', {
cellFormatMethod({ cellValue }, digits = 2) {
vxeUI.formats.add('formatNumber', {
tableCellFormatMethod({ cellValue }, digits = 2) {
if (cellValue === null || cellValue === undefined) {
return '';
}
@ -283,6 +283,22 @@ setupVbenVxeTable({
return cellValue.toFixed(digits);
},
});
vxeUI.formats.add('formatFraction', {
tableCellFormatMethod({ cellValue }) {
if (cellValue === null || cellValue === undefined) {
return '0.00';
}
if (isString(cellValue)) {
cellValue = Number.parseFloat(cellValue);
}
// 如果非 number则直接返回空串
if (Number.isNaN(cellValue)) {
return '0.00';
}
return `${(cellValue / 100).toFixed(2)}`;
},
});
},
useVbenForm,
});

View File

@ -23,10 +23,21 @@ export namespace PayAppApi {
id: number;
status: number;
}
export interface AppPageReqVO extends PageParam {
name?: string;
status?: number;
remark?: string;
payNotifyUrl?: string;
refundNotifyUrl?: string;
transferNotifyUrl?: string;
merchantName?: string;
createTime?: Date[];
}
}
/** 查询支付应用列表 */
export function getAppPage(params: PageParam) {
export function getAppPage(params: PayAppApi.AppPageReqVO) {
return requestClient.get<PageResult<PayAppApi.App>>('/pay/app/page', {
params,
});

View File

@ -1,38 +0,0 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayDemoApi {
/** 示例订单信息 */
export interface DemoOrder {
spuId: number;
createTime: Date;
}
}
/** 创建示例订单 */
export function createDemoOrder(data: PayDemoApi.DemoOrder) {
return requestClient.post('/pay/demo-order/create', data);
}
/** 获得示例订单 */
export function getDemoOrder(id: number) {
return requestClient.get<PayDemoApi.DemoOrder>(
`/pay/demo-order/get?id=${id}`,
);
}
/** 获得示例订单分页 */
export function getDemoOrderPage(params: PageParam) {
return requestClient.get<PageResult<PayDemoApi.DemoOrder>>(
'/pay/demo-order/page',
{
params,
},
);
}
/** 退款示例订单 */
export function refundDemoOrder(id: number) {
return requestClient.put(`/pay/demo-order/refund?id=${id}`);
}

View File

@ -0,0 +1,47 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace DemoOrderApi {
/** 示例订单信息 */
export interface Order {
id?: number;
userId?: number;
spuName?: string;
price?: number;
payStatus?: boolean;
payOrderId?: number;
payTime?: Date;
payChannelCode?: string;
payRefundId?: number;
refundPrice?: number;
refundTime?: Date;
spuId?: number;
createTime?: Date;
}
export interface OrderPageReqVO extends PageParam {
spuId?: number;
createTime?: Date[];
}
}
/** 创建示例订单 */
export function createDemoOrder(data: DemoOrderApi.Order) {
return requestClient.post('/pay/demo-order/create', data);
}
/** 获得示例订单分页 */
export function getDemoOrderPage(params: DemoOrderApi.OrderPageReqVO) {
return requestClient.get<PageResult<DemoOrderApi.Order>>(
'/pay/demo-order/page',
{
params,
},
);
}
/** 退款示例订单 */
export function refundDemoOrder(id: number) {
return requestClient.put(`/pay/demo-order/refund?id=${id}`);
}

View File

@ -1,29 +0,0 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace PayDemoTransferApi {
/** 示例转账单信息 */
export interface DemoTransfer {
price: number;
type: number;
userName: string;
alipayLogonId: string;
openid: string;
}
}
/** 创建示例转账单 */
export function createDemoTransfer(data: PayDemoTransferApi.DemoTransfer) {
return requestClient.post('/pay/demo-transfer/create', data);
}
/** 获得示例转账单分页 */
export function getDemoTransferPage(params: PageParam) {
return requestClient.get<PageResult<PayDemoTransferApi.DemoTransfer>>(
'/pay/demo-transfer/page',
{
params,
},
);
}

View File

@ -0,0 +1,40 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace DemoWithdrawApi {
/** 示例提现单信息 */
export interface Withdraw {
id?: number;
subject: string;
price: number;
userName: string;
userAccount: string;
type: number;
status?: number;
payTransferId?: number;
transferChannelCode?: string;
transferTime?: Date;
transferErrorMsg?: string;
}
}
/** 查询示例提现单列表 */
export function getDemoWithdrawPage(params: PageParam) {
return requestClient.get<PageResult<DemoWithdrawApi.Withdraw>>(
'/pay/demo-withdraw/page',
{
params,
},
);
}
/** 创建示例提现单 */
export function createDemoWithdraw(data: DemoWithdrawApi.Withdraw) {
return requestClient.post('/pay/demo-withdraw/create', data);
}
/** 发起提现单转账 */
export function transferDemoWithdraw(id: number) {
return requestClient.post(`/pay/demo-withdraw/transfer?id=${id}`);
}

View File

@ -52,7 +52,9 @@ export function getTransfer(id: number) {
);
}
/** 创建转账单 */
export function createTransfer(data: PayTransferApi.Transfer) {
return requestClient.post('/pay/transfer/create', data);
/** 导出转账单 */
export function exportTransfer(params: any) {
return requestClient.download('/pay/transfer/export-excel', {
params,
});
}

View File

@ -2,7 +2,7 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
@ -20,44 +20,19 @@ import { useUpload, useUploadType } from './use-upload';
defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
//
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
//
directory?: string;
disabled?: boolean;
helpText?: string;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [],
directory: undefined,
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: undefined,
resultField: '',
showDescription: false,
},
);
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
directory: undefined,
disabled: false,
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => [],
multiple: false,
api: undefined,
resultField: '',
showDescription: false,
});
const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
@ -112,7 +87,7 @@ watch(
},
);
const handleRemove = async (file: UploadFile) => {
async function handleRemove(file: UploadFile) {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
@ -122,9 +97,9 @@ const handleRemove = async (file: UploadFile) => {
emit('change', value);
emit('delete', file);
}
};
}
const beforeUpload = async (file: File) => {
async function beforeUpload(file: File) {
// 使Blob.text()FileReader
const fileContent = await file.text();
emit('returnText', fileContent);
@ -145,7 +120,7 @@ const beforeUpload = async (file: File) => {
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
}
async function customRequest(info: UploadRequestOption<any>) {
let { api } = props;

View File

@ -1,3 +1,8 @@
/**
*
*/
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export function checkFileType(file: File, accepts: string[]) {
if (!accepts || accepts.length === 0) {
return true;
@ -7,11 +12,6 @@ export function checkFileType(file: File, accepts: string[]) {
return reg.test(file.name);
}
/**
*
*/
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export function checkImgType(
file: File,
accepts: string[] = defaultImageAccepts,

View File

@ -2,9 +2,7 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { UploadListType } from './typing';
import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
@ -22,46 +20,20 @@ import { useUpload, useUploadType } from './use-upload';
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
//
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
//
directory?: string;
disabled?: boolean;
helpText?: string;
listType?: UploadListType;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [],
directory: undefined,
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
api: undefined,
resultField: '',
showDescription: true,
},
);
const props = withDefaults(defineProps<FileUploadProps>(), {
value: () => [],
directory: undefined,
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
api: undefined,
resultField: '',
showDescription: true,
});
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
@ -130,7 +102,7 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
});
}
const handlePreview = async (file: UploadFile) => {
async function handlePreview(file: UploadFile) {
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
}
@ -141,9 +113,9 @@ const handlePreview = async (file: UploadFile) => {
previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
);
};
}
const handleRemove = async (file: UploadFile) => {
async function handleRemove(file: UploadFile) {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
@ -153,14 +125,14 @@ const handleRemove = async (file: UploadFile) => {
emit('change', value);
emit('delete', file);
}
};
}
const handleCancel = () => {
function handleCancel() {
previewOpen.value = false;
previewTitle.value = '';
};
}
const beforeUpload = async (file: File) => {
async function beforeUpload(file: File) {
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
@ -177,7 +149,7 @@ const beforeUpload = async (file: File) => {
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
}
async function customRequest(info: UploadRequestOption<any>) {
let { api } = props;

View File

@ -1,2 +1,3 @@
export { default as FileUpload } from './file-upload.vue';
export { default as ImageUpload } from './image-upload.vue';
export { default as InputUpload } from './input-upload.vue';

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import type { InputProps, TextAreaProps } from 'ant-design-vue';
import type { FileUploadProps } from './typing';
import { computed, ref } from 'vue';
import { Col, Input, Row, Textarea } from 'ant-design-vue';
import FileUpload from './file-upload.vue';
const props = defineProps<{
fileUploadProps?: FileUploadProps;
inputProps?: InputProps;
inputType?: 'input' | 'textarea';
textareaProps?: TextAreaProps;
}>();
const emit = defineEmits(['change', 'update:value']);
const value = ref('');
function handleReturnText(text: string) {
value.value = text;
emit('change', value.value);
emit('update:value', value.value);
}
const inputProps = computed(() => {
return {
...props.inputProps,
value: value.value,
};
});
const textareaProps = computed(() => {
return {
...props.textareaProps,
value: value.value,
};
});
const fileUploadProps = computed(() => {
return {
...props.fileUploadProps,
};
});
</script>
<template>
<Row>
<Col :span="18">
<Input v-if="inputType === 'input'" v-bind="inputProps" />
<Textarea v-else :row="4" v-bind="textareaProps" />
</Col>
<Col :span="6">
<FileUpload
class="ml-4"
v-bind="fileUploadProps"
@return-text="handleReturnText"
/>
</Col>
</Row>
</template>

View File

@ -1,3 +1,7 @@
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api/infra/file';
export enum UploadResultStatus {
DONE = 'done',
ERROR = 'error',
@ -6,3 +10,28 @@ export enum UploadResultStatus {
}
export type UploadListType = 'picture' | 'picture-card' | 'text';
export interface FileUploadProps {
// 根据后缀,或者其他
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
// 上传的目录
directory?: string;
disabled?: boolean;
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
// 是否显示下面的描述
showDescription?: boolean;
value?: string | string[];
}

View File

@ -80,17 +80,17 @@ export function useUploadType({
}
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
export const useUpload = (directory?: string) => {
export function useUpload(directory?: string) {
// 后端上传地址
const uploadUrl = getUploadUrl();
// 是否使用前端直连上传
const isClientUpload =
UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
// 重写ElUpload上传方法
const httpRequest = async (
async function httpRequest(
file: File,
onUploadProgress?: AxiosProgressEvent,
) => {
) {
// 模式一:前端上传
if (isClientUpload) {
// 1.1 生成文件名称
@ -114,20 +114,20 @@ export const useUpload = (directory?: string) => {
// 模式二:后端上传
return uploadFile({ file, directory }, onUploadProgress);
}
};
}
return {
uploadUrl,
httpRequest,
};
};
}
/**
* URL
*/
export const getUploadUrl = (): string => {
export function getUploadUrl(): string {
return `${apiURL}/infra/file/upload`;
};
}
/**
*
@ -135,7 +135,10 @@ export const getUploadUrl = (): string => {
* @param vo
* @param file
*/
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) {
function createFile0(
vo: InfraFileApi.FilePresignedUrlRespVO,
file: File,
): InfraFileApi.File {
const fileVO = {
configId: vo.configId,
url: vo.url,

View File

@ -417,8 +417,8 @@ defineExpose({
<Spin :spinning="loading">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border border-gray-200">
<div class="border-b border-gray-200 p-2">
<div class="h-[500px] overflow-auto rounded border">
<div class="border-b p-2">
<Input
v-model:value="deptSearchKeys"
placeholder="搜索部门"

View File

@ -0,0 +1,16 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/pay/cashier',
component: () => import('#/views/pay/cashier/index.vue'),
name: 'PayCashier',
meta: {
title: '收银台',
icon: 'lucide:badge-japanese-yen',
hideInMenu: true,
},
},
];
export default routes;

View File

@ -0,0 +1,82 @@
/**
*
* @param num
*/
export function formatToFraction(num: number | string | undefined): string {
if (num === undefined) return '0.00';
const parsedNumber = typeof num === 'string' ? Number.parseFloat(num) : num;
return (parsedNumber / 100).toFixed(2);
}
/**
* 1.00
* 使
*
* @param num
*/
export function floatToFixed2(num: number | string | undefined): string {
let str = '0.00';
if (num === undefined) {
return str;
}
const f = formatToFraction(num);
const decimalPart = f.toString().split('.')[1];
const len = decimalPart ? decimalPart.length : 0;
switch (len) {
case 0: {
str = `${f.toString()}.00`;
break;
}
case 1: {
str = `${f.toString()}0`;
break;
}
case 2: {
str = f.toString();
break;
}
}
return str;
}
/**
*
* @param num
*/
export function convertToInteger(num: number | string | undefined): number {
if (num === undefined) return 0;
const parsedNumber = typeof num === 'string' ? Number.parseFloat(num) : num;
return Math.round(parsedNumber * 100);
}
/**
*
*/
export function yuanToFen(amount: number | string): number {
return convertToInteger(amount);
}
/**
*
*/
export function fenToYuan(price: number | string): string {
return formatToFraction(price);
}
/**
*
*
* @param value
* @param reference
*/
export function calculateRelativeRate(
value?: number,
reference?: number,
): number {
// 防止除0
if (!reference || reference === 0) return 0;
return Number.parseFloat(
((100 * ((value || 0) - reference)) / reference).toFixed(0),
);
}

View File

@ -1,6 +1,7 @@
export * from './constants';
export * from './dict';
export * from './download';
export * from './formatNumber';
export * from './formatTime';
export * from './formCreate';
export * from './rangePickerProps';

View File

@ -167,7 +167,7 @@ const handleCategorySortSubmit = async () => {
<CategoryFormModal @success="getList" />
<Card
:body-style="{ padding: '10px' }"
class="mb-4"
class="mb-4 h-[98%]"
v-spinning="modelListSpinning"
>
<div class="flex h-full items-center justify-between pl-5">

View File

@ -307,7 +307,7 @@ export function useContractColumns<T = CrmContractApi.Contract>(
field: 'price',
title: '合同金额(元)',
minWidth: 120,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'orderDate',
@ -349,13 +349,13 @@ export function useContractColumns<T = CrmContractApi.Contract>(
field: 'totalReceivablePrice',
title: '已回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'noReceivablePrice',
title: '未回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'contactLastTime',
@ -670,7 +670,7 @@ export function useReceivableAuditColumns<T = CrmReceivableApi.Receivable>(
field: 'price',
title: '回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'returnType',
@ -690,7 +690,7 @@ export function useReceivableAuditColumns<T = CrmReceivableApi.Receivable>(
field: 'contract.totalPrice',
title: '合同金额(元)',
minWidth: 140,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'ownerUserName',
@ -801,7 +801,7 @@ export function useReceivablePlanRemindColumns<T = CrmReceivableApi.Receivable>(
field: 'price',
title: '计划回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'returnTime',
@ -844,7 +844,7 @@ export function useReceivablePlanRemindColumns<T = CrmReceivableApi.Receivable>(
field: 'receivable.price',
title: '实际回款金额(元)',
minWidth: 160,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'receivable.returnTime',

View File

@ -118,7 +118,7 @@ export function useGridColumns<T = CrmBusinessApi.Business>(
field: 'totalPrice',
title: '商机金额(元)',
minWidth: 140,
formatter: 'formatAmount',
formatter: 'formatNumber',
},
{
field: 'dealTime',

View File

@ -134,7 +134,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
</script>
<template>
<Page :auto-content-height="true">
<Page auto-content-height>
<template #doc>
<DocAlert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
</template>

View File

@ -6,11 +6,10 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { message, Row, Space, Textarea } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createChannel, getChannel, updateChannel } from '#/api/pay/channel';
import { FileUpload } from '#/components/upload';
import { channelSchema } from './data';
@ -90,66 +89,6 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal :close-on-click-modal="false" :title="title" class="w-[40%]">
<Form :schema="channelSchema(formType)">
<template #appCertContent="slotProps">
<Space style="width: 100%" direction="vertical">
<Row>
<Textarea
v-bind="slotProps"
:rows="8"
placeholder="请上传商户公钥应用证书"
/>
</Row>
<Row>
<FileUpload
:accept="['crt']"
@return-text="
(text: string) => {
slotProps.setValue(text);
}
"
/>
</Row>
</Space>
</template>
<template #alipayPublicCertContent="slotProps">
<Space style="width: 100%" direction="vertical">
<Row>
<Textarea
v-bind="slotProps"
:rows="8"
placeholder="请上传支付宝公钥证书"
/>
</Row>
<Row>
<FileUpload
:accept="['.crt']"
@return-text="
(text: string) => {
slotProps.setValue(text);
}
"
/>
</Row>
</Space>
</template>
<template #rootCertContent="slotProps">
<Space style="width: 100%" direction="vertical">
<Row>
<Textarea v-bind="slotProps" :rows="8" placeholder="请上传根证书" />
</Row>
<Row>
<FileUpload
:accept="['.crt']"
@return-text="
(text: string) => {
slotProps.setValue(text);
}
"
/>
</Row>
</Space>
</template>
</Form>
<Form :schema="channelSchema(formType)" />
</Modal>
</template>

View File

@ -1,5 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import { h } from 'vue';
import { InputUpload } from '#/components/upload';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
export function channelSchema(formType: string): VbenFormSchema[] {
@ -147,13 +150,14 @@ export function channelSchema(formType: string): VbenFormSchema[] {
{
label: '商户公钥应用证书',
fieldName: 'config.appCertContent',
slotName: 'appCertContent',
component: 'Textarea',
component: h(InputUpload, {
inputType: 'textarea',
textareaProps: { rows: 8, placeholder: '请上传商户公钥应用证书' },
fileUploadProps: {
accept: ['crt'],
},
}),
rules: 'required',
componentProps: {
placeholder: '请上传商户公钥应用证书',
rows: 8,
},
dependencies: {
show(values) {
return values?.config?.mode === 1;
@ -164,13 +168,14 @@ export function channelSchema(formType: string): VbenFormSchema[] {
{
label: '支付宝公钥证书',
fieldName: 'config.alipayPublicCertContent',
slotName: 'alipayPublicCertContent',
component: 'Textarea',
component: h(InputUpload, {
inputType: 'textarea',
textareaProps: { rows: 8, placeholder: '请上传支付宝公钥证书' },
fileUploadProps: {
accept: ['crt'],
},
}),
rules: 'required',
componentProps: {
placeholder: '请上传支付宝公钥证书',
rows: 8,
},
dependencies: {
show(values) {
return values?.config?.mode === 1;
@ -181,13 +186,14 @@ export function channelSchema(formType: string): VbenFormSchema[] {
{
label: '根证书',
fieldName: 'config.rootCertContent',
slotName: 'rootCertContent',
component: 'Textarea',
component: h(InputUpload, {
inputType: 'textarea',
textareaProps: { rows: 8, placeholder: '请上传根证书' },
fileUploadProps: {
accept: ['crt'],
},
}),
rules: 'required',
componentProps: {
placeholder: '请上传根证书',
rows: 8,
},
dependencies: {
show(values) {
return values?.config?.mode === 1;
@ -453,12 +459,17 @@ export function channelSchema(formType: string): VbenFormSchema[] {
{
label: 'apiclient_cert.p12 证书',
fieldName: 'config.keyContent',
slotName: 'keyContent',
component: 'Input',
component: h(InputUpload, {
inputType: 'textarea',
textareaProps: {
rows: 8,
placeholder: '请上传 apiclient_cert.p12 证书',
},
fileUploadProps: {
accept: ['p12 '],
},
}),
rules: 'required',
componentProps: {
placeholder: '请上传 apiclient_cert.p12 证书',
},
dependencies: {
show(values) {
return values?.config?.apiVersion === 'v2';
@ -484,12 +495,17 @@ export function channelSchema(formType: string): VbenFormSchema[] {
{
label: 'apiclient_key.pem 证书',
fieldName: 'config.privateKeyContent',
slotName: 'privateKeyContent',
component: 'Input',
component: h(InputUpload, {
inputType: 'textarea',
textareaProps: {
rows: 8,
placeholder: '请上传 apiclient_key.pem 证书',
},
fileUploadProps: {
accept: ['pem'],
},
}),
rules: 'required',
componentProps: {
placeholder: '请上传 apiclient_key.pem 证书',
},
dependencies: {
show(values) {
return values?.config?.apiVersion === 'v3';

View File

@ -0,0 +1,7 @@
<script lang="ts" setup></script>
<template>
<div>
<h1>收银台</h1>
</div>
</template>

View File

@ -0,0 +1,104 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Select',
fieldName: 'spuId',
label: '商品',
rules: 'required',
componentProps: {
options: [
{ label: '华为手机 --- 1.00元', value: 1, price: 1 },
{ label: '小米电视 --- 10.00元', value: 2, price: 10 },
{ label: '苹果手表 --- 100.00元', value: 3, price: 100 },
{ label: '华硕笔记本 --- 1000.00元', value: 4, price: 1000 },
{ label: '蔚来汽车 --- 200000.00元', value: 5, price: 200_000 },
],
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '订单编号',
minWidth: 200,
},
{
field: 'userId',
title: '用户编号',
minWidth: 200,
},
{
field: 'spuName',
title: '商品名字',
minWidth: 200,
},
{
field: 'price',
title: '支付价格',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'refundPrice',
title: '退款金额',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'payOrderId',
title: '支付单号',
minWidth: 200,
},
{
field: 'payStatus',
title: '是否支付',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'payTime',
title: '支付时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'refundTime',
title: '退款时间',
minWidth: 180,
slots: { default: 'refundTime' },
},
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,13 +1,97 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DemoOrderApi } from '#/api/pay/demo/order';
import { Button } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDemoOrderPage, refundDemoOrder } from '#/api/pay/demo/order';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const router = useRouter();
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建订单 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 支付按钮操作 */
function handlePay(row: DemoOrderApi.Order) {
router.push({
name: 'PayCashier',
query: {
id: row.payOrderId,
returnUrl: encodeURIComponent(`/pay/demo/order?id=${row.id}`),
},
});
}
/** 退款按钮操作 */
async function handleRefund(row: DemoOrderApi.Order) {
const hideLoading = message.loading({
content: '退款中,请稍后...',
key: 'action_key_msg',
});
try {
await refundDemoOrder(row.id as number);
message.success({
content: '退款成功',
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDemoOrderPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<DemoOrderApi.Order>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<DocAlert
title="支付宝支付接入"
url="https://doc.iocoder.cn/pay/alipay-pay-demo/"
@ -24,23 +108,47 @@ import { DocAlert } from '#/components/doc-alert';
title="微信小程序支付接入"
url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/demo/order/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/demo/order/index
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['示例订单']),
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #refundTime="{ row }">
<span v-if="row.refundTime">{{ formatDateTime(row.refundTime) }}</span>
<span v-else-if="row.payRefundId">退款中等待退款结果</span>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '前往支付',
type: 'link',
ifShow: !row.payStatus,
onClick: handlePay.bind(null, row),
},
{
label: '发起退款',
type: 'link',
danger: true,
ifShow: row.payStatus && !row.payRefundId,
popConfirm: {
title: '确定发起退款吗?',
confirm: handleRefund.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import type { DemoOrderApi } from '#/api/pay/demo/order';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDemoOrder } from '#/api/pay/demo/order';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as DemoOrderApi.Order;
try {
await createDemoOrder(data);
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="$t('ui.actionTitle.create', ['退款订单'])">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -1,28 +0,0 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/demo/transfer/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/demo/transfer/index
代码pull request 贡献给我们
</Button>
</Page>
</template>

View File

@ -0,0 +1,135 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'subject',
label: '提现标题',
rules: 'required',
},
{
component: 'InputNumber',
fieldName: 'price',
label: '提现金额',
rules: 'required',
componentProps: {
min: 1,
precision: 2,
step: 0.01,
},
},
{
component: 'Select',
fieldName: 'type',
label: '提现类型',
rules: 'required',
componentProps: {
options: [
{ label: '支付宝', value: 1 },
{ label: '微信余额', value: 2 },
{ label: '钱包余额', value: 3 },
],
},
},
{
component: 'Input',
fieldName: 'userName',
label: '收款人姓名',
rules: 'required',
},
{
component: 'Input',
fieldName: 'userAccount',
label: '收款人账号',
rules: 'required',
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '提现单编号',
minWidth: 100,
},
{
field: 'subject',
title: '提现标题',
minWidth: 120,
},
{
field: 'type',
title: '提现类型',
minWidth: 90,
slots: { default: 'type' },
},
{
field: 'price',
title: '提现金额',
minWidth: 120,
formatter: 'formatNumber',
},
{
field: 'userName',
title: '收款人姓名',
minWidth: 150,
},
{
field: 'userAccount',
title: '收款人账号',
minWidth: 250,
},
{
field: 'status',
title: '提现状态',
minWidth: 100,
slots: { default: 'status' },
},
{
field: 'payTransferId',
title: '转账单号',
minWidth: 120,
},
{
field: 'transferChannelCode',
title: '转账渠道',
minWidth: 180,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
},
},
{
field: 'transferTime',
title: '转账时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'transferErrorMsg',
title: '转账失败原因',
minWidth: 200,
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -0,0 +1,145 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DemoWithdrawApi } from '#/api/pay/demo/withdraw';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getDemoWithdrawPage,
transferDemoWithdraw,
} from '#/api/pay/demo/withdraw';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建提现单 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 处理转账操作 */
async function handleTransfer(row: DemoWithdrawApi.Withdraw) {
const hideLoading = message.loading({
content: '转账中,请稍后...',
key: 'action_key_msg',
});
try {
const payTransferId = await transferDemoWithdraw(row.id as number);
message.success({
content: `转账提交成功,转账单号:${payTransferId}`,
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDemoWithdrawPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<DemoWithdrawApi.Withdraw>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="支付宝转账接入"
url="https://doc.iocoder.cn/pay/alipay-transfer-demo/"
/>
<DocAlert
title="微信转账接入"
url="https://doc.iocoder.cn/pay/wx-transfer-demo/"
/>
</template>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['示例提现单']),
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #type="{ row }">
<Tag v-if="row.type === 1"></Tag>
<Tag v-else-if="row.type === 2">微信余额</Tag>
<Tag v-else-if="row.type === 3">钱包余额</Tag>
</template>
<template #price="{ row }">
<span>{{ (row.price / 100.0).toFixed(2) }}</span>
</template>
<template #status="{ row }">
<Tag v-if="row.status === 0 && !row.payTransferId" type="warning">
等待转账
</Tag>
<Tag v-else-if="row.status === 0 && row.payTransferId" type="info">
转账中
</Tag>
<Tag v-else-if="row.status === 10" type="success"> 转账成功 </Tag>
<Tag v-else-if="row.status === 20" type="danger"> 转账失败 </Tag>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '发起转账',
type: 'link',
ifShow: row.status === 0 && !row.payTransferId,
onClick: handleTransfer.bind(null, row),
},
{
label: '重新转账',
type: 'link',
ifShow: row.status === 20,
onClick: handleTransfer.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import type { DemoWithdrawApi } from '#/api/pay/demo/withdraw';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDemoWithdraw } from '#/api/pay/demo/withdraw';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as DemoWithdrawApi.Withdraw;
try {
await createDemoWithdraw(data);
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="$t('ui.actionTitle.create', ['示例提现单'])">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -1,13 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getAppList } from '#/api/pay/app';
import { DICT_TYPE, getDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -69,9 +65,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = any>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -136,23 +130,10 @@ export function useGridColumns<T = any>(
},
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
show: hasAccessByCodes(['pay:notify:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,19 +1,16 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import * as PayNotifyApi from '#/api/pay/notify';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getNotifyTaskPage } from '#/api/pay/notify';
import { DocAlert } from '#/components/doc-alert';
import { useGridColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
const [NotifyDetailModal, notifyDetailModalApi] = useVbenModal({
const [DetailModal, detailModalApi] = useVbenModal({
connectedComponent: Detail,
destroyOnClose: true,
});
@ -24,18 +21,8 @@ function onRefresh() {
}
/** 查看详情 */
function onDetail(row: any) {
notifyDetailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<any>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
function handleDetail(row: any) {
detailModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
@ -43,13 +30,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await PayNotifyApi.getNotifyTaskPage({
return await getNotifyTaskPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
@ -72,7 +59,21 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template #doc>
<DocAlert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
</template>
<NotifyDetailModal @success="onRefresh" />
<Grid table-title="" />
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['pay:notify:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -33,13 +33,6 @@ const [Modal, modalApi] = useVbenModal({
}
},
});
/** 打开弹窗 */
const open = (id: number) => {
modalApi.setData({ id }).open();
};
defineExpose({ open });
</script>
<template>

View File

@ -1,144 +1,137 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { DICT_TYPE, getDictOptions } from '#/utils';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'appId',
label: '应用编号',
componentProps: {
placeholder: '请输入应用编号',
},
},
{
component: 'Select',
fieldName: 'channelCode',
label: '支付渠道',
componentProps: {
placeholder: '请选择开启状态',
options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'string'),
},
},
{
component: 'Input',
fieldName: 'merchantOrderId',
label: '商户单号',
componentProps: {
placeholder: '请输入商户单号',
},
},
{
component: 'Input',
fieldName: 'no',
label: '支付单号',
componentProps: {
placeholder: '请输入支付单号',
},
},
{
component: 'Input',
fieldName: 'channelOrderNo',
label: '渠道单号',
componentProps: {
placeholder: '请输入渠道单号',
},
},
{
component: 'Select',
fieldName: 'status',
label: '支付状态',
componentProps: {
placeholder: '请选择支付状态',
options: getDictOptions(DICT_TYPE.PAY_ORDER_STATUS, 'number'),
},
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '创建时间',
componentProps: {
placeholder: ['开始日期', '结束日期'],
},
},
];
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '编号',
field: 'id',
},
{
title: '支付金额',
field: 'price',
slots: {
default: ({ row }) => {
return `${(row.price || 0 / 100).toFixed(2)}`;
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'appId',
label: '应用编号',
componentProps: {
placeholder: '请输入应用编号',
},
},
},
{
title: '退款金额',
field: 'refundPrice',
slots: {
default: ({ row }) => {
return `${(row.refundPrice || 0 / 100).toFixed(2)}`;
{
component: 'Select',
fieldName: 'channelCode',
label: '支付渠道',
componentProps: {
placeholder: '请选择开启状态',
options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'string'),
},
},
},
{
title: '手续金额',
field: 'channelFeePrice',
slots: {
default: ({ row }) => {
return `${(row.channelFeePrice || 0 / 100).toFixed(2)}`;
{
component: 'Input',
fieldName: 'merchantOrderId',
label: '商户单号',
componentProps: {
placeholder: '请输入商户单号',
},
},
},
{
title: '订单号',
field: 'no',
slots: {
default: 'no',
{
component: 'Input',
fieldName: 'no',
label: '支付单号',
componentProps: {
placeholder: '请输入支付单号',
},
},
},
{
title: '支付状态',
field: 'status',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_ORDER_STATUS },
{
component: 'Input',
fieldName: 'channelOrderNo',
label: '渠道单号',
componentProps: {
placeholder: '请输入渠道单号',
},
},
},
{
title: '支付渠道',
field: 'channelCode',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
{
component: 'Select',
fieldName: 'status',
label: '支付状态',
componentProps: {
placeholder: '请选择支付状态',
options: getDictOptions(DICT_TYPE.PAY_ORDER_STATUS, 'number'),
},
},
},
{
title: '支付时间',
field: 'successTime',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '支付应用',
field: 'appName',
},
{
title: '商品标题',
field: 'subject',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
minWidth: 80,
},
];
{
component: 'RangePicker',
fieldName: 'createTime',
label: '创建时间',
componentProps: {
placeholder: ['开始日期', '结束日期'],
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 60 },
{
title: '编号',
field: 'id',
},
{
title: '支付金额',
field: 'price',
formatter: 'formatNumber',
},
{
title: '退款金额',
field: 'refundPrice',
formatter: 'formatNumber',
},
{
title: '手续金额',
field: 'channelFeePrice',
formatter: 'formatNumber',
},
{
title: '订单号',
field: 'no',
slots: {
default: 'no',
},
},
{
title: '支付状态',
field: 'status',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_ORDER_STATUS },
},
},
{
title: '支付渠道',
field: 'channelCode',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
},
},
{
title: '支付时间',
field: 'successTime',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '支付应用',
field: 'appName',
},
{
title: '商品标题',
field: 'subject',
},
{
title: '操作',
width: 80,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,110 +1,93 @@
<script lang="ts" setup>
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { PayOrderApi } from '#/api/pay/order';
import { Page, useVbenModal } from '@vben/common-ui';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import * as OrderApi from '#/api/pay/order';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getOrderPage } from '#/api/pay/order';
import { DocAlert } from '#/components/doc-alert';
import { columns, querySchema } from './data';
import detailFrom from './modules/order-detail.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 100,
componentProps: {
allowClear: true,
},
},
schema: querySchema(),
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
// RangePicker /
//
// fieldMappingTime: [
// [
// 'createTime',
// ['params[beginTime]', 'params[endTime]'],
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
// ],
// ],
};
const [DetailModal, detailModalApi] = useVbenModal({
connectedComponent: Detail,
destroyOnClose: true,
});
const gridOptions: VxeGridProps = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
// trigger: 'row',
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 查看详情 */
function handleDetail(row: PayOrderApi.Order) {
detailModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await OrderApi.getOrderPage({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getOrderPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
},
rowConfig: {
keyField: 'id',
},
//
id: 'pay-order-index',
};
const [BasicTable] = useVbenVxeGrid({
formOptions,
gridOptions,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<PayOrderApi.Order>,
});
const [DetailModal, modalDetailApi] = useVbenModal({
connectedComponent: detailFrom,
});
const openDetail = (id: number) => {
modalDetailApi.setData({
id,
});
modalDetailApi.open();
};
</script>
<template>
<Page :auto-content-height="true">
<DocAlert
title="支付宝支付接入"
url="https://doc.iocoder.cn/pay/alipay-pay-demo/"
/>
<DocAlert
title="微信公众号支付接入"
url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/"
/>
<DocAlert
title="微信小程序支付接入"
url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/"
/>
<BasicTable>
<template #action="{ row }">
<a-button
type="link"
v-access:code="['pay:order:query']"
@click="openDetail(row.id)"
>
{{ $t('ui.actionTitle.detail') }}
</a-button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="支付宝支付接入"
url="https://doc.iocoder.cn/pay/alipay-pay-demo/"
/>
<DocAlert
title="微信公众号支付接入"
url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/"
/>
<DocAlert
title="微信小程序支付接入"
url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/"
/>
</template>
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['pay:order:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
<template #no="{ row }">
<p class="order-font">
@ -118,7 +101,6 @@ const openDetail = (id: number) => {
{{ row.channelOrderNo }}
</p>
</template>
</BasicTable>
<DetailModal />
</Grid>
</Page>
</template>

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import type { PayOrderApi } from '#/api/pay/order';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@ -6,34 +8,39 @@ import { formatDateTime } from '@vben/utils';
import { Descriptions, Divider, Tag } from 'ant-design-vue';
import * as OrderApi from '#/api/pay/order';
import { getOrder } from '#/api/pay/order';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
const detailData = ref<OrderApi.PayOrderApi.Order>();
const detailData = ref<PayOrderApi.Order>();
const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false,
showCancelButton: false,
showConfirmButton: false,
const [Modal, modalApi] = useVbenModal({
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
detailData.value = undefined;
return;
}
//
const data = modalApi.getData<PayOrderApi.Order>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
detailData.value = await getOrder(data.id);
} finally {
modalApi.unlock();
}
modalApi.modalLoading(true);
const { id } = modalApi.getData() as {
id: number;
};
detailData.value = await OrderApi.getOrderDetail(id);
modalApi.modalLoading(false);
},
});
</script>
<template>
<BasicModal :close-on-click-modal="false" title="订单详情" class="w-[700px]">
<Modal
title="订单详情"
class="w-1/2"
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions :column="2">
<Descriptions.Item label="商户单号">
{{ detailData?.merchantOrderId }}
@ -121,5 +128,5 @@ const [BasicModal, modalApi] = useVbenModal({
{{ detailData?.channelNotifyData }}
</Descriptions.Item>
</Descriptions>
</BasicModal>
</Modal>
</template>

View File

@ -1,14 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { PayRefundApi } from '#/api/pay/refund';
import { useAccess } from '@vben/access';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getAppList } from '#/api/pay/app';
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -80,9 +75,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns<T = PayRefundApi.Refund>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
@ -155,23 +148,10 @@ export function useGridColumns<T = PayRefundApi.Refund>(
},
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
width: 80,
fixed: 'right',
cellRender: {
attrs: {
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
show: hasAccessByCodes(['pay:refund:query']),
},
],
},
slots: { default: 'actions' },
},
];
}

View File

@ -1,16 +1,10 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import * as RefundApi from '#/api/pay/refund';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
@ -29,32 +23,22 @@ function onRefresh() {
}
/** 导出表格 */
async function onExport() {
async function handleExport() {
const data = await RefundApi.exportRefund(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '支付退款.xls', source: data });
}
/** 查看详情 */
function onDetail(row: any) {
function handleDetail(row: any) {
refundDetailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<any>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -89,15 +73,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<RefundDetailModal @success="onRefresh" />
<Grid table-title="退">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['pay:refund:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['pay:refund:query'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['pay:refund:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>

View File

@ -33,13 +33,6 @@ const [Modal, modalApi] = useVbenModal({
}
},
});
/** 打开弹窗 */
const open = (id: number) => {
modalApi.setData({ id }).open();
};
defineExpose({ open });
</script>
<template>

View File

@ -0,0 +1,285 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { h } from 'vue';
import { formatDateTime } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'no',
label: '转账单号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入转账单号',
},
},
{
fieldName: 'channelCode',
label: '转账渠道',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE),
allowClear: true,
placeholder: '请选择支付渠道',
},
},
{
fieldName: 'merchantTransferId',
label: '商户单号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入商户单号',
},
},
{
fieldName: 'type',
label: '类型',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.PAY_TRANSFER_TYPE),
allowClear: true,
placeholder: '请选择类型',
},
},
{
fieldName: 'status',
label: '转账状态',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.PAY_TRANSFER_STATUS),
allowClear: true,
placeholder: '请选择转账状态',
},
},
{
fieldName: 'userName',
label: '收款人姓名',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入收款人姓名',
},
},
{
fieldName: 'accountNo',
label: '收款人账号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入收款人账号',
},
},
{
fieldName: 'channelTransferNo',
label: '渠道单号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入渠道单号',
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 100,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'appName',
title: '支付应用',
minWidth: 100,
},
{
field: 'price',
title: '转账金额',
minWidth: 120,
formatter: ({ cellValue }) => `${(cellValue / 100).toFixed(2)}`,
},
{
field: 'status',
title: '转账状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_TRANSFER_STATUS },
},
},
{
field: 'type',
title: '类型',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_TRANSFER_TYPE },
},
},
{
field: 'channelCode',
title: '支付渠道',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
},
},
{
field: 'merchantTransferId',
title: '商户单号',
minWidth: 180,
},
{
field: 'channelTransferNo',
title: '渠道单号',
minWidth: 180,
},
{
field: 'userName',
title: '收款人姓名',
minWidth: 120,
},
{
field: 'accountNo',
title: '收款人账号',
minWidth: 180,
},
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 详情的配置 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{
field: 'id',
label: '编号',
},
{
field: 'merchantTransferId',
label: '商户单号',
content: (data) => {
return h(Tag, {
color: 'blue',
content: data?.merchantTransferId,
});
},
},
{
field: 'no',
label: '转账单号',
content: (data) => {
return h(Tag, {
color: 'blue',
content: data?.no,
});
},
},
{
field: 'appId',
label: '应用编号',
},
{
field: 'status',
label: '转账状态',
content: (data) =>
h(DictTag, {
type: DICT_TYPE.PAY_TRANSFER_STATUS,
value: data?.status,
}),
},
{
field: 'price',
label: '转账金额',
content: (data) => {
return h(Tag, {
color: 'blue',
content: `${(data?.price / 100).toFixed(2)}`,
});
},
},
{
field: 'successTime',
label: '转账时间',
content: (data) => formatDateTime(data?.successTime) as string,
},
{
field: 'createTime',
label: '创建时间',
content: (data) => formatDateTime(data?.createTime) as string,
},
{
field: 'userName',
label: '收款人姓名',
},
{
field: 'userAccount',
label: '收款人账号',
},
{
field: 'channelCode',
label: '支付渠道',
content: (data) =>
h(DictTag, {
type: DICT_TYPE.PAY_CHANNEL_CODE,
value: data?.channelCode,
}),
},
{
field: 'channelCode',
label: '支付 IP',
},
{
field: 'channelTransferNo',
label: '渠道单号',
content: (data) => {
return h(Tag, {
color: 'blue',
content: data?.channelTransferNo,
});
},
},
{
field: 'notifyUrl',
label: '通知 URL',
},
{
field: 'channelNotifyData',
label: '转账渠道通知内容',
},
];
}

View File

@ -1,28 +1,103 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { PayTransferApi } from '#/api/pay/transfer';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { exportTransfer, getTransferPage } from '#/api/pay/transfer';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
const [DetailModal, detailModalApi] = useVbenModal({
connectedComponent: Detail,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function handleExport() {
const data = await exportTransfer(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '转账单.xls', source: data });
}
/** 查看转账详情 */
function handleDetail(row: PayTransferApi.Transfer) {
detailModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getTransferPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<PayTransferApi.Transfer>,
});
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/transfer/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/transfer/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert title="转账管理" url="https://doc.iocoder.cn/pay/transfer/" />
</template>
<DetailModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['pay:transfer:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['pay:transfer:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import type { PayTransferApi } from '#/api/pay/transfer';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { getTransfer } from '#/api/pay/transfer';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
const formData = ref<PayTransferApi.Transfer>();
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<PayTransferApi.Transfer>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getTransfer(data.id);
} finally {
modalApi.unlock();
}
},
});
const [Description] = useDescription({
componentProps: {
title: '基本信息',
bordered: false,
column: 2,
class: 'mx-4',
},
schema: useDetailSchema(),
});
</script>
<template>
<Modal
title="转账单详情"
class="w-1/2"
:show-cancel-button="false"
:show-confirm-button="false"
>
<Description :data="formData" />
</Modal>
</template>

View File

@ -0,0 +1,86 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'userId',
label: '用户编号',
component: 'Input',
},
{
fieldName: 'userType',
label: '用户类型',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
allowClear: true,
...getRangePickerDefaultProps(),
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
title: '编号',
field: 'id',
},
{
title: '用户编号',
field: 'userId',
},
{
title: '用户类型',
field: 'userType',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.USER_TYPE },
},
},
{
title: '余额',
field: 'balance',
formatter: 'formatFraction',
},
{
title: '累计支出',
field: 'totalExpense',
formatter: 'formatFraction',
},
{
title: '累计充值',
field: 'totalRecharge',
formatter: 'formatFraction',
},
{
title: '冻结金额',
field: 'freezePrice',
formatter: 'formatFraction',
},
{
title: '创建时间',
field: 'createTime',
formatter: 'formatDateTime',
},
{
title: '操作',
field: 'actions',
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,28 +1,82 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { PayWalletApi } from '#/api/pay/wallet/balance';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getWalletPage } from '#/api/pay/wallet/balance';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import WalletDetail from './modules/detail.vue';
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
const [WalletModal, walletModalApi] = useVbenModal({
connectedComponent: WalletDetail,
destroyOnClose: true,
});
function handleDetail(row: Required<PayWalletApi.WalletVO>) {
walletModalApi.setData(row).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getWalletPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<PayWalletApi.WalletVO>,
});
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/wallet/balance/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/wallet/balance/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert title="钱包余额" url="https://doc.iocoder.cn/pay/build/" />
</template>
<WalletModal @reload="onRefresh" />
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
onClick: handleDetail.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import type { PayWalletApi } from '#/api/pay/wallet/balance';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import WalletTransactionList from '../../transaction/index.vue';
const walletId = ref(0);
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
//
const data = modalApi.getData<PayWalletApi.WalletVO>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
walletId.value = data.id;
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal
title="消息详情"
class="w-[40%]"
:show-cancel-button="false"
:show-confirm-button="false"
>
<WalletTransactionList :wallet-id="walletId" />
</Modal>
</template>

View File

@ -0,0 +1,119 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '套餐名',
component: 'Input',
rules: 'required',
},
{
fieldName: 'payPrice',
label: '支付金额(元)',
component: 'InputNumber',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
fieldName: 'bonusPrice',
label: '赠送金额(元)',
component: 'InputNumber',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
fieldName: 'status',
label: '开启状态',
component: 'RadioGroup',
rules: 'required',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '套餐名称',
component: 'Input',
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
allowClear: true,
...getRangePickerDefaultProps(),
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
},
{
field: 'name',
title: '套餐名称',
},
{
field: 'payPrice',
title: '支付金额',
formatter: 'formatFraction',
},
{
field: 'bonusPrice',
title: '赠送金额',
formatter: 'formatFraction',
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,28 +1,129 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deletePackage,
getPackagePage,
} from '#/api/pay/wallet/rechargePackage';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建套餐 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑套餐 */
function handleEdit(row: any) {
formModalApi.setData(row).open();
}
/** 删除套餐 */
async function handleDelete(row: any) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deletePackage(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPackagePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<any>,
});
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/wallet/rechargePackage/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/wallet/rechargePackage/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['充值套餐']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['pay:wallet-recharge-package:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['pay:wallet-recharge-package:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['pay:wallet-recharge-package:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,98 @@
<script lang="ts" setup>
import type { WalletRechargePackageApi } from '#/api/pay/wallet/rechargePackage';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createPackage,
getPackage,
updatePackage,
} from '#/api/pay/wallet/rechargePackage';
import { $t } from '#/locales';
import { fenToYuan, yuanToFen } from '#/utils';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<WalletRechargePackageApi.Package>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['充值套餐'])
: $t('ui.actionTitle.create', ['充值套餐']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as WalletRechargePackageApi.Package;
try {
//
data.payPrice = yuanToFen(data.payPrice);
data.bonusPrice = yuanToFen(data.bonusPrice);
await (formData.value?.id ? updatePackage(data) : createPackage(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<WalletRechargePackageApi.Package>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getPackage(data.id as number);
//
formData.value.payPrice = Number.parseFloat(
fenToYuan(formData.value.payPrice),
);
formData.value.bonusPrice = Number.parseFloat(
fenToYuan(formData.value.bonusPrice),
);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-[600px]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@ -0,0 +1,40 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
width: 80,
},
{
field: 'walletId',
title: '钱包编号',
width: 100,
},
{
field: 'title',
title: '关联业务标题',
width: 200,
},
{
field: 'price',
title: '交易金额',
width: 120,
formatter: ({ cellValue }) => `${cellValue / 100}`,
},
{
field: 'balance',
title: '钱包余额',
width: 120,
formatter: ({ cellValue }) => `${cellValue / 100}`,
},
{
field: 'createTime',
title: '交易时间',
width: 180,
formatter: 'formatDateTime',
},
];
}

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getWallet } from '#/api/pay/wallet/balance';
import { getTransactionPage } from '#/api/pay/wallet/transaction';
import { useGridColumns } from './data';
const props = defineProps({
walletId: {
type: Number,
required: false,
default: undefined,
},
userId: {
type: Number,
required: false,
default: undefined,
},
});
const [Grid] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
let walletId = props.walletId;
if (props.userId) {
const wallet = await getWallet({ userId: props.userId });
walletId = wallet.id;
}
return await getTransactionPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
walletId,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
},
} as VxeTableGridOptions<any>,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="" />
</Page>
</template>

View File

@ -263,7 +263,7 @@ setupVbenVxeTable({
});
// 添加数量格式化,例如金额
vxeUI.formats.add('formatAmount', {
vxeUI.formats.add('formatNumber', {
cellFormatMethod({ cellValue }, digits = 2) {
if (cellValue === null || cellValue === undefined) {
return '';

View File

@ -277,7 +277,7 @@ setupVbenVxeTable({
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
// add by 星语:数量格式化,例如说:金额
vxeUI.formats.add('formatAmount', {
vxeUI.formats.add('formatNumber', {
cellFormatMethod({ cellValue }, digits = 2) {
if (cellValue === null || cellValue === undefined) {
return '';

View File

@ -557,16 +557,4 @@ import { z } from '#/adapter/form';
除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
如果需要使用自定义的插槽名而不是使用`fieldName`可以在schema中添加`slotName`属性。当提供了`slotName`属性时,将优先使用`slotName`作为插槽名。
```ts
// 使用自定义插槽名的例子
{
component: 'Textarea',
fieldName: 'config.appCertContent',
slotName: 'appCertSlot',
label: '商户公钥应用证书',
}
```
:::

View File

@ -155,11 +155,7 @@ const computedSchema = computed(
:rules="cSchema.rules"
>
<template #default="slotProps">
<slot
v-bind="slotProps"
:name="cSchema.slotName || cSchema.fieldName"
>
</slot>
<slot v-bind="slotProps" :name="cSchema.fieldName"> </slot>
</template>
</FormField>
</template>

View File

@ -263,8 +263,6 @@ export interface FormSchema<
renderComponentContent?: RenderComponentContentType;
/** 字段规则 */
rules?: FormSchemaRuleType;
/** 自定义插槽名如果不指定则使用fieldName */
slotName?: string;
/** 后缀 */
suffix?: CustomRenderType;
}

View File

@ -124,14 +124,6 @@ export class ModalApi {
return this.setState({ submitting: isLocked });
}
modalLoading(loading: boolean) {
this.store.setState((prev) => ({
...prev,
confirmLoading: loading,
loading,
}));
}
/**
*
*/

View File

@ -6,6 +6,7 @@ export { default as VbenVxeGrid } from './use-vxe-grid.vue';
export type {
VxeGridListeners,
VxeGridProps,
VxeGridPropTypes,
VxeTableInstance,
VxeToolbarInstance,
} from 'vxe-table';

View File

@ -6,9 +6,6 @@ settings:
catalogs:
default:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1
'@changesets/changelog-github':
specifier: ^0.5.1
version: 0.5.1
@ -686,9 +683,6 @@ importers:
apps/web-antd:
dependencies:
'@ant-design/icons-vue':
specifier: 'catalog:'
version: 7.0.1(vue@3.5.13(typescript@5.8.3))
'@form-create/ant-design-vue':
specifier: 'catalog:'
version: 3.2.22(vue@3.5.13(typescript@5.8.3))