f1
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
2026-01-12 18:18:57 +08:00
commit aed91a09f1
1452 changed files with 128040 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const RichText = defineAsyncComponent(() =>
import('@vben-core/shadcn-ui').then((m) => m.RichText),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return defineComponent({
inheritAttrs: false,
name: component.name,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'RichText'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
}),
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
RichText,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };

View File

@@ -0,0 +1,48 @@
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
TinyMCE: 'modelValue',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@@ -0,0 +1,276 @@
import type { Recordable } from '@vben/types';
import { h } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $te } from '@vben/locales';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { get, isFunction, isString } from '@vben/utils';
import { objectOmit } from '@vueuse/core';
import { Button, Image, Popconfirm, Switch, Tag } from 'ant-design-vue';
import { $t } from '#/locales';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
formConfig: {
// 全局禁用vxe-table的表单配置使用formOptions
enabled: false,
},
minHeight: 180,
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: '',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
},
});
/**
* 解决vxeTable在热更新时可能会出错的问题
*/
vxeUI.renderer.forEach((_item, key) => {
if (key.startsWith('Cell')) {
vxeUI.renderer.delete(key);
}
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 单元格渲染: Tag
vxeUI.renderer.add('CellTag', {
renderTableDefault({ options, props }, { column, row }) {
const value = get(row, column.field);
const tagOptions = options ?? [
{ color: 'success', label: $t('common.enabled'), value: 1 },
{ color: 'error', label: $t('common.disabled'), value: 0 },
];
const tagItem = tagOptions.find((item) => item.value === value);
return h(
Tag,
{
...props,
...objectOmit(tagItem ?? {}, ['label']),
},
{ default: () => tagItem?.label ?? value },
);
},
});
vxeUI.renderer.add('CellSwitch', {
renderTableDefault({ attrs, props }, { column, row }) {
const loadingKey = `__loading_${column.field}`;
const finallyProps = {
checkedChildren: $t('common.enabled'),
checkedValue: 1,
unCheckedChildren: $t('common.disabled'),
unCheckedValue: 0,
...props,
checked: row[column.field],
loading: row[loadingKey] ?? false,
'onUpdate:checked': onChange,
};
async function onChange(newVal: any) {
row[loadingKey] = true;
try {
const result = await attrs?.beforeChange?.(newVal, row);
if (result !== false) {
row[column.field] = newVal;
}
} finally {
row[loadingKey] = false;
}
}
return h(Switch, finallyProps);
},
});
/**
* 注册表格的操作按钮渲染器
*/
vxeUI.renderer.add('CellOperation', {
renderTableDefault({ attrs, options, props }, { column, row }) {
const defaultProps = { size: 'small', type: 'link', ...props };
let align = 'end';
switch (column.align) {
case 'center': {
align = 'center';
break;
}
case 'left': {
align = 'start';
break;
}
default: {
align = 'end';
break;
}
}
const presets: Recordable<Recordable<any>> = {
delete: {
danger: true,
text: $t('common.delete'),
},
edit: {
text: $t('common.edit'),
},
};
const operations: Array<Recordable<any>> = (
options || ['edit', 'delete']
)
.map((opt) => {
if (isString(opt)) {
return presets[opt]
? { code: opt, ...presets[opt], ...defaultProps }
: {
code: opt,
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
...defaultProps,
};
} else {
return { ...defaultProps, ...presets[opt.code], ...opt };
}
})
.map((opt) => {
const optBtn: Recordable<any> = {};
Object.keys(opt).forEach((key) => {
optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
});
return optBtn;
})
.filter((opt) => opt.show !== false);
function renderBtn(opt: Recordable<any>, listen = true) {
return h(
Button,
{
...props,
...opt,
icon: undefined,
onClick: listen
? () =>
attrs?.onClick?.({
code: opt.code,
row,
})
: undefined,
},
{
default: () => {
const content = [];
if (opt.icon) {
content.push(
h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
);
}
content.push(opt.text);
return content;
},
},
);
}
function renderConfirm(opt: Recordable<any>) {
return h(
Popconfirm,
{
getPopupContainer(el) {
return (
el
.closest('.vxe-table--viewport-wrapper')
?.querySelector('.vxe-table--main-wrapper')
?.querySelector('tbody') || document.body
);
},
placement: 'topLeft',
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
...props,
...opt,
icon: undefined,
onConfirm: () => {
attrs?.onClick?.({
code: opt.code,
row,
});
},
},
{
default: () => renderBtn({ ...opt }, false),
description: () =>
h(
'div',
{ class: 'truncate' },
$t('ui.actionMessage.deleteConfirm', [
row[attrs?.nameField || 'name'],
]),
),
},
);
}
const btns = operations.map((opt) =>
opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
);
return h(
'div',
{
class: 'flex table-operations',
style: { justifyContent: align },
},
btns,
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type OnActionClickParams<T = Recordable<any>> = {
code: string;
row: T;
};
export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>,
) => void;
export type * from '@vben/plugins/vxe-table';

View File

@@ -0,0 +1,715 @@
import { requestClient } from '#/api/request';
export namespace AgentApi {
export interface AgentListItem {
id: number;
user_id: number;
level_name: string;
region: string;
mobile: string;
membership_expiry_time: string;
balance: number;
total_earnings: number;
frozen_balance: number;
withdrawn_amount: number;
create_time: string;
is_real_name_verified: boolean;
real_name: string;
id_card: string;
real_name_status: 'approved' | 'pending' | 'rejected';
}
export interface AgentList {
total: number;
items: AgentListItem[];
}
export interface GetAgentListParams {
page: number;
pageSize: number;
mobile?: string;
region?: string;
parent_agent_id?: number;
id?: number;
create_time_start?: string;
create_time_end?: string;
order_by?: string;
order_type?: 'asc' | 'desc';
}
export interface AgentLinkListItem {
agent_id: number;
product_name: string;
price: number;
link_identifier: string;
create_time: string;
}
export interface AgentLinkList {
total: number;
items: AgentLinkListItem[];
}
export interface GetAgentLinkListParams {
page: number;
pageSize: number;
agent_id?: number;
product_name?: string;
link_identifier?: string;
}
// 代理佣金相关接口
export interface AgentCommissionListItem {
id: number;
agent_id: number;
order_id: number;
amount: number;
product_name: string;
status: number;
create_time: string;
}
export interface AgentCommissionList {
total: number;
items: AgentCommissionListItem[];
}
export interface GetAgentCommissionListParams {
page: number;
pageSize: number;
agent_id?: number;
order_id?: number;
product_name?: string;
status?: number;
}
// 代理奖励相关接口
export interface AgentRewardListItem {
id: number;
agent_id: number;
relation_agent_id: number;
amount: number;
type: string;
create_time: string;
}
export interface AgentRewardList {
total: number;
items: AgentRewardListItem[];
}
export interface GetAgentRewardListParams {
page: number;
pageSize: number;
agent_id?: number;
relation_agent_id?: number;
type?: string;
}
// 代理提现相关接口
export interface AgentWithdrawalListItem {
id: number;
agent_id: number;
withdraw_no: string;
amount: number;
actual_amount: number; // 实际到账金额(扣税后)
tax_amount: number; // 扣税金额
status: number;
payee_account: string;
remark: string;
create_time: string;
withdraw_type: number; // 提现类型:1-支付宝,2-银行卡
bank_card_no?: string; // 银行卡号
bank_name?: string; // 开户支行
payee_name?: string; // 收款人姓名
}
export interface AgentWithdrawalList {
total: number;
items: AgentWithdrawalListItem[];
}
export interface GetAgentWithdrawalListParams {
page: number;
pageSize: number;
agent_id?: number;
status?: number;
withdraw_no?: string;
}
// 提现统计数据
export interface WithdrawalStatistics {
total_withdrawal_amount: number;
today_withdrawal_amount: number;
total_actual_amount: number;
total_tax_amount: number;
}
// 代理订单统计数据
export interface AgentOrderStatistics {
total_agent_order_count: number; // 总代理订单数
today_agent_order_count: number; // 今日代理订单数
}
// 代理统计数据
export interface AgentStatistics {
total_agent_count: number; // 总代理数
today_agent_count: number; // 今日新增代理数
}
// 代理链接产品统计项
export interface AgentLinkProductStatisticsItem {
product_name: string;
link_count: number;
}
// 代理链接产品统计响应
export interface AgentLinkProductStatisticsResp {
items: AgentLinkProductStatisticsItem[];
}
// 代理链接产品统计请求参数
export interface GetAgentLinkProductStatisticsParams {}
// 代理上级抽佣相关接口
export interface AgentCommissionDeductionListItem {
id: number;
agent_id: number;
deducted_agent_id: number;
amount: number;
product_name: string;
type: 'cost' | 'pricing';
status: number;
create_time: string;
}
export interface AgentCommissionDeductionList {
total: number;
items: AgentCommissionDeductionListItem[];
}
export interface GetAgentCommissionDeductionListParams {
page: number;
pageSize: number;
agent_id?: number;
product_name?: string;
type?: 'cost' | 'pricing';
status?: number;
}
// 平台抽佣列表项
export interface AgentPlatformDeductionListItem {
id: number;
agent_id: number;
amount: number;
type: 'cost' | 'pricing';
status: number;
create_time: string;
}
// 平台抽佣列表响应
export interface AgentPlatformDeductionList {
total: number;
items: AgentPlatformDeductionListItem[];
}
// 获取平台抽佣列表参数
export interface GetAgentPlatformDeductionListParams {
page: number;
pageSize: number;
agent_id?: number;
type?: 'cost' | 'pricing';
status?: number;
}
// 代理产品配置列表项
export interface AgentProductionConfigItem {
id: number;
product_name: string;
cost_price: number;
price_range_min: number;
price_range_max: number;
pricing_standard: number;
overpricing_ratio: number;
create_time: string;
}
// 代理产品配置列表响应
export interface AgentProductionConfigList {
total: number;
items: AgentProductionConfigItem[];
}
// 获取代理产品配置列表参数
export interface GetAgentProductionConfigListParams {
page: number;
pageSize: number;
product_name?: string;
id?: number;
}
// 更新代理产品配置参数
export interface UpdateAgentProductionConfigParams {
id: number;
cost_price: number;
price_range_min: number;
price_range_max: number;
pricing_standard: number;
overpricing_ratio: number;
}
// 更新代理产品配置响应
export interface UpdateAgentProductionConfigResp {
success: boolean;
}
export interface MembershipRechargeOrderListItem {
id: number;
user_id: number;
agent_id: number;
level_name: string;
amount: number;
payment_method: 'alipay' | 'appleiap' | 'other' | 'wechat';
order_no: string;
platform_order_id: string;
status: 'cancelled' | 'failed' | 'pending' | 'success';
create_time: string;
}
export interface GetMembershipRechargeOrderListParams {
page: number;
pageSize: number;
user_id?: number;
agent_id?: number;
level_name?: string;
status?: string;
}
export interface MembershipRechargeOrderList {
total: number;
items: MembershipRechargeOrderListItem[];
}
// 代理会员配置相关接口
export interface AgentMembershipConfigListItem {
id: number;
level_name: string;
price: number;
report_commission: number;
lower_activity_reward: null | number;
new_activity_reward: null | number;
lower_standard_count: null | number;
new_lower_standard_count: null | number;
lower_withdraw_reward_ratio: null | number;
lower_convert_vip_reward: null | number;
lower_convert_svip_reward: null | number;
exemption_amount: number;
price_increase_max: null | number;
price_ratio: null | number;
price_increase_amount: null | number;
create_time: string;
}
export interface GetAgentMembershipConfigListParams {
page: number;
pageSize: number;
level_name?: string;
}
// 代理会员配置编辑请求参数
export interface UpdateAgentMembershipConfigParams {
id: number; // 主键
level_name: string; // 会员级别名称
price: number; // 会员年费
report_commission: number; // 直推报告收益
lower_activity_reward?: null | number; // 下级活跃奖励金额
new_activity_reward?: null | number; // 新增活跃奖励金额
lower_standard_count?: null | number; // 活跃下级达标个数
new_lower_standard_count?: null | number; // 新增活跃下级达标个数
lower_withdraw_reward_ratio?: null | number; // 下级提现奖励比例
lower_convert_vip_reward?: null | number; // 下级转化VIP奖励
lower_convert_svip_reward?: null | number; // 下级转化SVIP奖励
exemption_amount?: null | number; // 免责金额
price_increase_max?: null | number; // 提价最高金额
price_ratio?: null | number; // 提价区间收取比例
price_increase_amount?: null | number; // 在原本成本上加价的金额
}
// 代理钱包信息
export interface AgentWalletInfo {
balance: number; // 可用余额
frozen_balance: number; // 冻结余额
total_earnings: number; // 总收益
}
// 修改代理钱包余额请求
export interface UpdateAgentWalletBalanceReq {
agent_id: number; // 代理ID
amount: number; // 修改金额(正数增加,负数减少)
}
// 修改代理钱包余额响应
export interface UpdateAgentWalletBalanceResp {
success: boolean; // 是否成功
balance: number; // 修改后的余额
}
// 代理钱包流水相关接口
export interface WalletTransactionListItem {
id: number;
agent_id: number;
transaction_type: string;
amount: number;
balance_before: number;
balance_after: number;
frozen_balance_before: number;
frozen_balance_after: number;
transaction_id?: string;
related_user_id?: number;
remark?: string;
create_time: string;
}
export interface WalletTransactionList {
total: number;
items: WalletTransactionListItem[];
}
export interface GetWalletTransactionListParams {
page: number;
pageSize: number;
agent_id: number;
transaction_type?: string;
create_time_start?: string;
create_time_end?: string;
}
// 系统配置相关接口
export interface SystemConfig {
commission_safe_mode: boolean; // 佣金安全防御模式
}
export interface UpdateSystemConfigReq {
commission_safe_mode: boolean; // 佣金安全防御模式true-冻结模式false-直接结算模式
}
export interface UpdateSystemConfigResp {
success: boolean; // 是否成功
}
}
/**
* 获取代理列表数据
* @param params 查询参数
*/
async function getAgentList(params: AgentApi.GetAgentListParams) {
return requestClient.get<AgentApi.AgentList>('/agent/list', {
params,
});
}
/**
* 获取代理推广链接列表
*/
async function getAgentLinkList(params: AgentApi.GetAgentLinkListParams) {
return requestClient.get<AgentApi.AgentLinkList>('/agent/agent-link/list', {
params,
});
}
/**
* 获取代理佣金列表
*/
async function getAgentCommissionList(
params: AgentApi.GetAgentCommissionListParams,
) {
return requestClient.get<AgentApi.AgentCommissionList>(
'/agent/agent-commission/list',
{
params,
},
);
}
/**
* 更新代理佣金状态
*/
async function updateAgentCommissionStatus(
id: number,
status: number,
) {
return requestClient.post<{ success: boolean }>(
'/agent/agent-commission/update-status',
{
id,
status,
},
);
}
/**
* 批量解冻代理佣金
*/
async function batchUnfreezeAgentCommission(
agentId?: number,
) {
return requestClient.post<{
success: boolean;
count: number;
amount: number;
}>('/agent/agent-commission/batch-unfreeze', {
agent_id: agentId,
});
}
/**
* 获取代理奖励列表
*/
async function getAgentRewardList(params: AgentApi.GetAgentRewardListParams) {
return requestClient.get<AgentApi.AgentRewardList>(
'/agent/agent-reward/list',
{
params,
},
);
}
/**
* 获取代理提现列表
*/
async function getAgentWithdrawalList(
params: AgentApi.GetAgentWithdrawalListParams,
) {
return requestClient.get<AgentApi.AgentWithdrawalList>(
'/agent/agent-withdrawal/list',
{
params,
},
);
}
/**
* 获取代理上级抽佣列表
*/
async function getAgentCommissionDeductionList(
params: AgentApi.GetAgentCommissionDeductionListParams,
) {
return requestClient.get<AgentApi.AgentCommissionDeductionList>(
'/agent/agent-commission-deduction/list',
{
params,
},
);
}
/**
* 获取平台抽佣列表
*/
async function getAgentPlatformDeductionList(
params: AgentApi.GetAgentPlatformDeductionListParams,
) {
return requestClient.get<AgentApi.AgentPlatformDeductionList>(
'/agent/agent-platform-deduction/list',
{
params,
},
);
}
/**
* 获取代理产品配置列表
*/
async function getAgentProductionConfigList(
params: AgentApi.GetAgentProductionConfigListParams,
) {
return requestClient.get<AgentApi.AgentProductionConfigList>(
'/agent/agent-production-config/list',
{
params,
},
);
}
/**
* 更新代理产品配置
*/
async function updateAgentProductionConfig(
params: AgentApi.UpdateAgentProductionConfigParams,
) {
return requestClient.post<AgentApi.UpdateAgentProductionConfigResp>(
'/agent/agent-production-config/update',
params,
);
}
/**
* 获取会员充值订单列表
*/
async function getMembershipRechargeOrderList(
params: AgentApi.GetMembershipRechargeOrderListParams,
) {
return requestClient.get<AgentApi.MembershipRechargeOrderList>(
'/agent/agent-membership-recharge-order/list',
{
params,
},
);
}
/**
* 获取代理会员配置列表
*/
async function getAgentMembershipConfigList(
params: AgentApi.GetAgentMembershipConfigListParams,
) {
return requestClient.get<{
items: AgentApi.AgentMembershipConfigListItem[];
total: number;
}>('/agent/agent-membership-config/list', { params });
}
/**
* 更新代理会员配置
*/
async function updateAgentMembershipConfig(
params: AgentApi.UpdateAgentMembershipConfigParams,
) {
return requestClient.post<{ success: boolean }>(
'/agent/agent-membership-config/update',
params,
);
}
/**
* 银行卡提现审核
*/
export interface ReviewBankCardWithdrawalParams {
withdrawal_id: number;
action: 1 | 2; // 1-确认, 2-拒绝
remark?: string;
}
async function reviewBankCardWithdrawal(
params: ReviewBankCardWithdrawalParams,
) {
return requestClient.post<{ success: boolean }>(
'/agent/agent-withdrawal/bank-card/review',
params,
);
}
/**
* 获取提现统计数据
*/
async function getWithdrawalStatistics() {
return requestClient.get<AgentApi.WithdrawalStatistics>(
'/agent/agent-withdrawal/statistics',
);
}
/**
* 获取代理订单统计数据
*/
async function getAgentOrderStatistics() {
return requestClient.get<AgentApi.AgentOrderStatistics>(
'/agent/agent-order/statistics',
);
}
/**
* 获取代理统计数据
*/
async function getAgentStatistics() {
return requestClient.get<AgentApi.AgentStatistics>(
'/agent/statistics',
);
}
/**
* 获取代理链接产品统计数据
*/
async function getAgentLinkProductStatistics() {
return requestClient.get<AgentApi.AgentLinkProductStatisticsResp>(
'/agent/agent-link/product-statistics',
);
}
/**
* 获取代理钱包信息
*/
async function getAgentWallet(agentId: number) {
return requestClient.get<AgentApi.AgentWalletInfo>(
`/agent/wallet/${agentId}`,
);
}
/**
* 修改代理钱包余额
*/
async function updateAgentWalletBalance(params: AgentApi.UpdateAgentWalletBalanceReq) {
return requestClient.post<AgentApi.UpdateAgentWalletBalanceResp>(
'/agent/wallet/update-balance',
params,
);
}
/**
* 获取系统配置
*/
async function getSystemConfig() {
return requestClient.get<AgentApi.SystemConfig>(
'/agent/system-config',
);
}
/**
* 更新系统配置
*/
async function updateSystemConfig(params: AgentApi.UpdateSystemConfigReq) {
return requestClient.post<AgentApi.UpdateSystemConfigResp>(
'/agent/system-config',
params,
);
}
/**
* 获取代理钱包流水列表
*/
async function getWalletTransactionList(
params: AgentApi.GetWalletTransactionListParams,
) {
return requestClient.get<AgentApi.WalletTransactionList>(
'/agent/wallet-transaction/list',
{ params },
);
}
export {
batchUnfreezeAgentCommission,
getAgentCommissionDeductionList,
getAgentCommissionList,
getAgentLinkList,
getAgentLinkProductStatistics,
getAgentList,
getAgentMembershipConfigList,
getAgentOrderStatistics,
getAgentPlatformDeductionList,
getAgentProductionConfigList,
getAgentRewardList,
getAgentStatistics,
getAgentWallet,
getAgentWithdrawalList,
getMembershipRechargeOrderList,
getWithdrawalStatistics,
reviewBankCardWithdrawal,
updateAgentCommissionStatus,
updateAgentMembershipConfig,
updateAgentProductionConfig,
updateAgentWalletBalance,
getSystemConfig,
updateSystemConfig,
getWalletTransactionList,
};

View File

@@ -0,0 +1 @@
export * from './agent';

View File

@@ -0,0 +1,52 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
access_token: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
// return requestClient.get<string[]>('/auth/codes');
return [];
}

View File

@@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@@ -0,0 +1,10 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@@ -0,0 +1,10 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}

View File

@@ -0,0 +1,37 @@
export * from './agent';
export * from './core';
export * from './notification';
export * from './order';
export * from './platform-user';
export * from './product-manage';
export * from './promotion';
export * from './system';
export interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
export interface PageResult<T> {
items: T[];
total: number;
}
declare global {
interface Window {
$http: {
delete<T = any>(url: string, config?: any): Promise<ApiResponse<T>>;
get<T = any>(url: string, config?: any): Promise<ApiResponse<T>>;
post<T = any>(
url: string,
data?: any,
config?: any,
): Promise<ApiResponse<T>>;
put<T = any>(
url: string,
data?: any,
config?: any,
): Promise<ApiResponse<T>>;
};
}
}

View File

@@ -0,0 +1,105 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace NotificationApi {
export interface NotificationItem {
id: number;
title: string;
content?: string;
notification_page: string;
start_date: string;
start_time: string;
end_date: string;
end_time: string;
status: number;
create_time: string;
update_time: string;
}
export interface NotificationList {
total: number;
items: NotificationItem[];
}
export interface CreateNotificationRequest {
title: string;
content: string;
notification_page: string;
start_date: string;
start_time: string;
end_date: string;
end_time: string;
status: 0 | 1;
}
export interface CreateNotificationResponse {
id: number;
}
export interface UpdateNotificationRequest
extends Partial<CreateNotificationRequest> {
id: number;
}
export interface UpdateNotificationResponse {
success: boolean;
}
export interface DeleteNotificationResponse {
success: boolean;
}
}
/**
* 获取通知列表
*/
async function getNotificationList(params: Recordable<any>) {
return requestClient.get<NotificationApi.NotificationList>(
'/notification/list',
{
params,
},
);
}
/**
* 创建通知
*/
async function createNotification(
data: NotificationApi.CreateNotificationRequest,
) {
return requestClient.post<NotificationApi.CreateNotificationResponse>(
'/notification/create',
data,
);
}
/**
* 更新通知
*/
async function updateNotification(
id: number,
data: NotificationApi.UpdateNotificationRequest,
) {
return requestClient.put<NotificationApi.UpdateNotificationResponse>(
`/notification/update/${id}`,
data,
);
}
/**
* 删除通知
*/
async function deleteNotification(id: number) {
return requestClient.delete<NotificationApi.DeleteNotificationResponse>(
`/notification/delete/${id}`,
);
}
export {
createNotification,
deleteNotification,
getNotificationList,
updateNotification,
};

View File

@@ -0,0 +1 @@
export * from './order';

View File

@@ -0,0 +1,28 @@
import { requestClient } from '#/api/request';
export namespace OrderStatisticsApi {
// 订单统计数据项
export interface OrderStatisticsItem {
date: string; // 日期
count: number; // 订单数量
amount: number; // 订单金额
}
// 订单统计响应
export interface OrderStatisticsResponse {
items: OrderStatisticsItem[];
}
// 时间维度类型
export type TimeDimension = 'day' | 'month' | 'year' | 'all';
}
/**
* 获取订单统计数据
* @param dimension 时间维度day-日(当月1号到今天)month-月(今年1月到当月)year-年(过去5年)all-全部(按日统计)
*/
export function getOrderStatistics(dimension: OrderStatisticsApi.TimeDimension) {
return requestClient.get<OrderStatisticsApi.OrderStatisticsResponse>('/order/statistics', {
params: { dimension }
});
}

View File

@@ -0,0 +1,102 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace OrderApi {
export interface Order {
id: number;
order_no: string;
platform_order_id: string;
product_name: string;
payment_platform: 'alipay' | 'appleiap' | 'wechat';
payment_scene: 'app' | 'h5' | 'mini_program' | 'public_account';
amount: number;
sales_cost: number;
status: 'closed' | 'failed' | 'paid' | 'pending' | 'refunded';
query_state: 'cleaned' | 'failed' | 'pending' | 'processing' | 'success';
create_time: string;
pay_time: null | string;
refund_time: null | string;
is_promotion: 0 | 1;
}
export interface OrderList {
total: number;
items: Order[];
}
export interface RefundOrderRequest {
refund_amount: number;
refund_reason: string;
}
export interface RefundOrderResponse {
status: string;
refund_no: string;
amount: number;
}
// 退款统计数据
export interface RefundStatistics {
total_refund_amount: number;
today_refund_amount: number;
}
// 收入统计数据
export interface IncomeStatistics {
total_revenue_amount: number;
today_revenue_amount: number;
total_profit_amount: number;
today_profit_amount: number;
}
// 订单来源统计数据
export interface OrderSourceStatistics {
product_name: string;
order_count: number;
}
}
/**
* 获取订单列表数据
*/
async function getOrderList(params: Recordable<any>) {
return requestClient.get<OrderApi.OrderList>('/order/list', {
params,
});
}
/**
* 订单退款
* @param id 订单 ID
* @param data 退款请求数据
*/
async function refundOrder(id: number, data: OrderApi.RefundOrderRequest) {
return requestClient.post<OrderApi.RefundOrderResponse>(
`/order/refund/${id}`,
data,
);
}
/**
* 获取退款统计数据
*/
async function getRefundStatistics() {
return requestClient.get<OrderApi.RefundStatistics>('/order/refund-statistics');
}
/**
* 获取收入统计数据
*/
async function getIncomeStatistics() {
return requestClient.get<OrderApi.IncomeStatistics>('/order/revenue-statistics');
}
/**
* 获取订单来源统计数据
*/
async function getOrderSourceStatistics() {
return requestClient.get<{ items: OrderApi.OrderSourceStatistics[] }>('/order/source-statistics');
}
export { getOrderList, refundOrder, getRefundStatistics, getIncomeStatistics, getOrderSourceStatistics };

View File

@@ -0,0 +1,184 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace OrderQueryApi {
export interface QueryItem {
feature: Recordable<any>;
data: Recordable<any>;
}
export interface QueryDetail {
id: number;
order_id: number;
user_id: number;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
create_time: string;
update_time: string;
query_state: string;
}
export interface GetQueryDetailRequest {
order_id: number;
}
export interface GetQueryDetailResponse {
id: number;
order_id: number;
user_id: number;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
create_time: string;
update_time: string;
query_state: string;
}
// 清理日志相关接口定义
export interface GetQueryCleanupLogListRequest {
page?: number;
page_size?: number;
status?: number;
start_time?: string;
end_time?: string;
}
export interface QueryCleanupLogItem {
id: number;
cleanup_time: string;
cleanup_before: string;
status: number;
affected_rows: number;
error_msg: string;
remark: string;
create_time: string;
}
export interface GetQueryCleanupLogListResponse {
total: number;
items: QueryCleanupLogItem[];
}
// 清理详情相关接口定义
export interface GetQueryCleanupDetailListRequest {
log_id: number;
page?: number;
page_size?: number;
}
export interface QueryCleanupDetailItem {
id: number;
cleanup_log_id: number;
query_id: number;
order_id: number;
user_id: number;
product_id: number;
query_state: string;
create_time_old: string;
create_time: string;
}
export interface GetQueryCleanupDetailListResponse {
total: number;
items: QueryCleanupDetailItem[];
}
// 清理配置相关接口定义
export interface GetQueryCleanupConfigListRequest {
status?: number;
}
export interface QueryCleanupConfigItem {
id: number;
config_key: string;
config_value: string;
config_desc: string;
status: number;
create_time: string;
update_time: string;
}
export interface GetQueryCleanupConfigListResponse {
items: QueryCleanupConfigItem[];
}
export interface UpdateQueryCleanupConfigRequest {
id: number;
config_value: string;
status: number;
}
export interface UpdateQueryCleanupConfigResponse {
success: boolean;
}
}
/**
* 获取订单查询详情
* @param orderId 订单ID
*/
async function getOrderQueryDetail(orderId: number) {
return requestClient.get<OrderQueryApi.GetQueryDetailResponse>(
`/query/detail/${orderId}`,
);
}
/**
* 获取清理日志列表
*/
async function getQueryCleanupLogList(
params: OrderQueryApi.GetQueryCleanupLogListRequest,
) {
return requestClient.get<OrderQueryApi.GetQueryCleanupLogListResponse>(
'/query/cleanup/logs',
{ params },
);
}
/**
* 获取清理详情列表
* @param logId 清理日志ID
*/
async function getQueryCleanupDetailList(
logId: number,
params: Omit<OrderQueryApi.GetQueryCleanupDetailListRequest, 'log_id'>,
) {
return requestClient.get<OrderQueryApi.GetQueryCleanupDetailListResponse>(
`/query/cleanup/details/${logId}`,
{ params },
);
}
/**
* 获取清理配置列表
*/
async function getQueryCleanupConfigList(
params?: OrderQueryApi.GetQueryCleanupConfigListRequest,
) {
return requestClient.get<OrderQueryApi.GetQueryCleanupConfigListResponse>(
'/query/cleanup/configs',
{ params },
);
}
/**
* 更新清理配置
*/
async function updateQueryCleanupConfig(
data: OrderQueryApi.UpdateQueryCleanupConfigRequest,
) {
return requestClient.put<OrderQueryApi.UpdateQueryCleanupConfigResponse>(
'/query/cleanup/config',
data,
);
}
export {
getOrderQueryDetail,
getQueryCleanupConfigList,
getQueryCleanupDetailList,
getQueryCleanupLogList,
updateQueryCleanupConfig,
};

View File

@@ -0,0 +1,53 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace PlatformUserApi {
export interface PlatformUserItem {
id: number;
mobile: string;
nickname: string;
info: string;
inside: number;
create_time: string;
update_time: string;
}
export interface PlatformUserList {
total: number;
items: PlatformUserItem[];
}
export interface UpdatePlatformUserRequest {
mobile: string;
nickname: string;
info: string;
inside: number;
}
}
/**
* 获取平台用户列表数据
*/
async function getPlatformUserList(params: Recordable<any>) {
return requestClient.get<PlatformUserApi.PlatformUserList>(
'/platform_user/list',
{
params,
},
);
}
/**
* 更新平台用户
* @param id 用户 ID
* @param data 用户数据
*/
async function updatePlatformUser(
id: number,
data: PlatformUserApi.UpdatePlatformUserRequest,
) {
return requestClient.put(`/platform_user/update/${id}`, data);
}
export { getPlatformUserList, updatePlatformUser };

View File

@@ -0,0 +1,140 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace FeatureApi {
export interface FeatureItem {
id: number;
api_id: string;
name: string;
cost_price: number;
create_time: string;
update_time: string;
}
export interface FeatureList {
total: number;
items: FeatureItem[];
}
export interface CreateFeatureRequest {
api_id: string;
name: string;
cost_price: number;
}
export interface UpdateFeatureRequest {
api_id?: string;
name?: string;
cost_price?: number;
}
export interface FeatureExampleItem {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
export interface ConfigFeatureExampleRequest {
feature_id: number;
data: string;
}
export interface ConfigFeatureExampleResponse {
success: boolean;
}
export interface GetFeatureExampleRequest {
feature_id: number;
}
export interface GetFeatureExampleResponse {
id: number;
feature_id: number;
api_id: string;
data: string;
create_time: string;
update_time: string;
}
}
/**
* 获取模块列表数据
*/
async function getFeatureList(params: Recordable<any>) {
return requestClient.get<FeatureApi.FeatureList>('/feature/list', {
params,
});
}
/**
* 获取模块详情
* @param id 模块ID
*/
async function getFeatureDetail(id: number) {
return requestClient.get<FeatureApi.FeatureItem>(`/feature/detail/${id}`);
}
/**
* 创建模块
* @param data 模块数据
*/
async function createFeature(data: FeatureApi.CreateFeatureRequest) {
return requestClient.post<{ id: number }>('/feature/create', data);
}
/**
* 更新模块
* @param id 模块ID
* @param data 模块数据
*/
async function updateFeature(
id: number,
data: FeatureApi.UpdateFeatureRequest,
) {
return requestClient.put<{ success: boolean }>(`/feature/update/${id}`, data);
}
/**
* 删除模块
* @param id 模块ID
*/
async function deleteFeature(id: number) {
return requestClient.delete<{ success: boolean }>(`/feature/delete/${id}`);
}
/**
* 配置功能示例数据
* @param data 示例数据配置
*/
async function configFeatureExample(
data: FeatureApi.ConfigFeatureExampleRequest,
) {
return requestClient.post<FeatureApi.ConfigFeatureExampleResponse>(
'/feature/config-example',
data,
);
}
/**
* 获取功能示例数据
* @param featureId 功能ID
*/
async function getFeatureExample(featureId: number) {
return requestClient.get<FeatureApi.GetFeatureExampleResponse>(
`/feature/example/${featureId}`,
);
}
export {
configFeatureExample,
createFeature,
deleteFeature,
getFeatureDetail,
getFeatureExample,
getFeatureList,
updateFeature,
};

View File

@@ -0,0 +1,2 @@
export * from './feature';
export * from './product';

View File

@@ -0,0 +1,148 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace ProductApi {
export interface ProductItem {
id: number;
product_name: string;
product_en: string;
description: string;
notes: string;
cost_price: number;
sell_price: number;
create_time: string;
update_time: string;
}
export interface ProductList {
total: number;
items: ProductItem[];
}
export interface CreateProductRequest {
product_name: string;
product_en: string;
description: string;
notes?: string;
cost_price: number;
sell_price: number;
}
export interface UpdateProductRequest {
product_name?: string;
product_en?: string;
description?: string;
notes?: string;
cost_price?: number;
sell_price?: number;
}
export interface ProductFeatureListItem {
id: number;
product_id: number;
feature_id: number;
api_id: string;
name: string;
sort: number;
enable: number;
is_important: number;
create_time: string;
update_time: string;
}
export interface ProductFeatureItem {
feature_id: number;
sort: number;
enable: number;
is_important: number;
}
export interface UpdateProductFeaturesRequest {
features: ProductFeatureItem[];
}
export interface UpdateProductFeaturesResponse {
success: boolean;
}
}
/**
* 获取产品列表数据
*/
async function getProductList(params: Recordable<any>) {
return requestClient.get<ProductApi.ProductList>('/product/list', {
params,
});
}
/**
* 获取产品详情
* @param id 产品ID
*/
async function getProductDetail(id: number) {
return requestClient.get<ProductApi.ProductItem>(`/product/detail/${id}`);
}
/**
* 创建产品
* @param data 产品数据
*/
async function createProduct(data: ProductApi.CreateProductRequest) {
return requestClient.post<{ id: number }>('/product/create', data);
}
/**
* 更新产品
* @param id 产品ID
* @param data 产品数据
*/
async function updateProduct(
id: number,
data: ProductApi.UpdateProductRequest,
) {
return requestClient.put<{ success: boolean }>(`/product/update/${id}`, data);
}
/**
* 删除产品
* @param id 产品ID
*/
async function deleteProduct(id: number) {
return requestClient.delete<{ success: boolean }>(`/product/delete/${id}`);
}
/**
* 获取产品功能列表
* @param productId 产品ID
*/
async function getProductFeatureList(productId: number) {
return requestClient.get<ProductApi.ProductFeatureListItem[]>(
`/product/feature/list/${productId}`,
);
}
/**
* 更新产品功能关联
* @param productId 产品ID
* @param data 功能列表数据
*/
async function updateProductFeatures(
productId: number,
data: ProductApi.UpdateProductFeaturesRequest,
) {
return requestClient.put<{ success: boolean }>(
`/product/feature/update/${productId}`,
data,
);
}
export {
createProduct,
deleteProduct,
getProductDetail,
getProductFeatureList,
getProductList,
updateProduct,
updateProductFeatures,
};

View File

@@ -0,0 +1,45 @@
import { requestClient } from '#/api/request';
export namespace PromotionAnalyticsApi {
export interface OverviewData {
today_click_count: number;
today_pay_count: number;
today_pay_amount: number;
total_click_count: number;
total_pay_count: number;
total_pay_amount: number;
}
export interface TrendData {
id: number;
link_id: number;
pay_amount: number;
click_count: number;
pay_count: number;
stats_date: string;
}
}
/**
* 获取推广数据概览
*/
async function statsTotal() {
return requestClient.get<PromotionAnalyticsApi.OverviewData>(
'/promotion/stats/total',
);
}
/**
* 获取推广数据趋势
* @param params 日期范围参数
*/
async function statsHistory(params: { end_date: string; start_date: string }) {
return requestClient.get<PromotionAnalyticsApi.TrendData[]>(
'/promotion/stats/history',
{
params,
},
);
}
export { statsHistory, statsTotal };

View File

@@ -0,0 +1,2 @@
export * from './analytics';
export * from './link';

View File

@@ -0,0 +1,67 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace PromotionLinkApi {
export interface PromotionLinkItem {
id: number;
name: string;
url: string;
create_time: string;
}
export interface PromotionLink {
total: number;
items: PromotionLinkItem[];
}
}
/**
* 获取推广链接列表数据
*/
async function getPromotionLinkList(params: Recordable<any>) {
return requestClient.get<PromotionLinkApi.PromotionLink>(
'/promotion/link/list',
{
params,
},
);
}
/**
* 创建推广链接
* @param data 推广链接数据
*/
async function createPromotionLink(
data: Omit<PromotionLinkApi.PromotionLinkItem, 'id'>,
) {
return requestClient.post('/promotion/link/create', data);
}
/**
* 更新推广链接
*
* @param id 推广链接 ID
* @param data 推广链接数据
*/
async function updatePromotionLink(
id: string,
data: Omit<PromotionLinkApi.PromotionLinkItem, 'id'>,
) {
return requestClient.put(`/promotion/link/update/${id}`, data);
}
/**
* 删除推广链接
* @param id 推广链接 ID
*/
async function deletePromotionLink(id: string) {
return requestClient.delete(`/promotion/link/delete/${id}`);
}
export {
createPromotionLink,
deletePromotionLink,
getPromotionLinkList,
updatePromotionLink,
};

View File

@@ -0,0 +1,113 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 200,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.msg ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

View File

@@ -0,0 +1,165 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemApiApi {
export interface SystemApiItem {
id: number;
role_id?: number;
api_id?: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
create_time?: string;
update_time?: string;
}
export interface SystemApi {
list: SystemApiItem[];
total: number;
}
export interface SystemApiAllResponse {
items: SystemApiItem[];
}
export interface SystemRoleApiResponse {
items: null | SystemApiItem[];
}
export interface RoleApiItem {
id: number;
role_id: number;
api_id: number;
api_name: string;
api_code: string;
method: string;
url: string;
status: 0 | 1;
description?: string;
}
export interface RoleApi {
list: RoleApiItem[];
}
}
/**
* 获取API列表数据
*/
async function getApiList(params: Recordable<any>) {
return requestClient.get<SystemApiApi.SystemApi>('/api/list', {
params,
});
}
/**
* 获取API详情
* @param id API ID
*/
async function getApiDetail(id: number) {
return requestClient.get<SystemApiApi.SystemApiItem>(`/api/detail/${id}`);
}
/**
* 创建API
* @param data API数据
*/
async function createApi(
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.post('/api/create', data);
}
/**
* 更新API
* @param id API ID
* @param data API数据
*/
async function updateApi(
id: number,
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.put(`/api/update/${id}`, data);
}
/**
* 删除API
* @param id API ID
*/
async function deleteApi(id: number) {
return requestClient.delete(`/api/delete/${id}`);
}
/**
* 批量更新API状态
* @param data.ids API ID数组
* @param data.status 状态值
*/
async function batchUpdateApiStatus(data: { ids: number[]; status: 0 | 1 }) {
return requestClient.put('/api/batch-update-status', data);
}
/**
* 获取角色API权限列表
* @param roleId 角色ID
*/
async function getRoleApiList(roleId: number) {
return requestClient.get<SystemApiApi.SystemRoleApiResponse>(
`/role/${roleId}/api/list`,
);
}
/**
* 分配角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function assignRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/assign', data);
}
/**
* 移除角色API权限
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function removeRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.post('/role/api/remove', data);
}
/**
* 更新角色API权限全量更新
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function updateRoleApi(data: { api_ids: number[]; role_id: number }) {
return requestClient.put('/role/api/update', data);
}
/**
* 获取所有API列表用于权限分配
* @param params.status 状态过滤
*/
async function getAllApiList(params?: { status?: number }) {
return requestClient.get<SystemApiApi.SystemApiAllResponse>('/api/all', {
params,
});
}
export {
assignRoleApi,
batchUpdateApiStatus,
createApi,
deleteApi,
getAllApiList,
getApiDetail,
getApiList,
getRoleApiList,
removeRoleApi,
updateApi,
updateRoleApi,
};

View File

@@ -0,0 +1,54 @@
import { requestClient } from '#/api/request';
export namespace SystemDeptApi {
export interface SystemDept {
[key: string]: any;
children?: SystemDept[];
id: string;
name: string;
remark?: string;
status: 0 | 1;
}
}
/**
* 获取部门列表数据
*/
async function getDeptList() {
return requestClient.get<Array<SystemDeptApi.SystemDept>>(
'/system/dept/list',
);
}
/**
* 创建部门
* @param data 部门数据
*/
async function createDept(
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
) {
return requestClient.post('/system/dept', data);
}
/**
* 更新部门
*
* @param id 部门 ID
* @param data 部门数据
*/
async function updateDept(
id: string,
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
) {
return requestClient.put(`/system/dept/${id}`, data);
}
/**
* 删除部门
* @param id 部门 ID
*/
async function deleteDept(id: string) {
return requestClient.delete(`/system/dept/${id}`);
}
export { createDept, deleteDept, getDeptList, updateDept };

View File

@@ -0,0 +1,5 @@
export * from './api';
export * from './dept';
export * from './menu';
export * from './role';
export * from './user';

View File

@@ -0,0 +1,156 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemMenuApi {
/** 徽标颜色集合 */
export const BadgeVariants = [
'default',
'destructive',
'primary',
'success',
'warning',
] as const;
/** 徽标类型集合 */
export const BadgeTypes = ['dot', 'normal'] as const;
/** 菜单类型集合 */
export const MenuTypes = [
'catalog',
'menu',
'embedded',
'link',
'button',
] as const;
/** 系统菜单 */
export interface SystemMenu {
[key: string]: any;
/** 后端权限标识 */
authCode: string;
/** 子级 */
children?: SystemMenu[];
/** 组件 */
component?: string;
/** 菜单ID */
id: string;
/** 菜单元数据 */
meta?: {
/** 激活时显示的图标 */
activeIcon?: string;
/** 作为路由时需要激活的菜单的Path */
activePath?: string;
/** 固定在标签栏 */
affixTab?: boolean;
/** 在标签栏固定的顺序 */
affixTabOrder?: number;
/** 徽标内容(当徽标类型为normal时有效) */
badge?: string;
/** 徽标类型 */
badgeType?: (typeof BadgeTypes)[number];
/** 徽标颜色 */
badgeVariants?: (typeof BadgeVariants)[number];
/** 在菜单中隐藏下级 */
hideChildrenInMenu?: boolean;
/** 在面包屑中隐藏 */
hideInBreadcrumb?: boolean;
/** 在菜单中隐藏 */
hideInMenu?: boolean;
/** 在标签栏中隐藏 */
hideInTab?: boolean;
/** 菜单图标 */
icon?: string;
/** 内嵌Iframe的URL */
iframeSrc?: string;
/** 是否缓存页面 */
keepAlive?: boolean;
/** 外链页面的URL */
link?: string;
/** 同一个路由最大打开的标签数 */
maxNumOfOpenTab?: number;
/** 无需基础布局 */
noBasicLayout?: boolean;
/** 是否在新窗口打开 */
openInNewWindow?: boolean;
/** 菜单排序 */
order?: number;
/** 额外的路由参数 */
query?: Recordable<any>;
/** 菜单标题 */
title?: string;
};
/** 菜单名称 */
name: string;
/** 路由路径 */
path: string;
/** 父级ID */
pid: string;
/** 重定向 */
redirect?: string;
/** 菜单类型 */
type: (typeof MenuTypes)[number];
}
}
/**
* 获取菜单数据列表
*/
async function getMenuList() {
return requestClient.get<Array<SystemMenuApi.SystemMenu>>('/menu/list');
}
async function isMenuNameExists(
name: string,
id?: SystemMenuApi.SystemMenu['id'],
) {
return requestClient.get<boolean>('/menu/name-exists', {
params: { id, name },
});
}
async function isMenuPathExists(
path: string,
id?: SystemMenuApi.SystemMenu['id'],
) {
return requestClient.get<boolean>('/menu/path-exists', {
params: { id, path },
});
}
/**
* 创建菜单
* @param data 菜单数据
*/
async function createMenu(
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
) {
return requestClient.post('/menu/create', data);
}
/**
* 更新菜单
*
* @param id 菜单 ID
* @param data 菜单数据
*/
async function updateMenu(
id: string,
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
) {
return requestClient.put(`/menu/update/${id}`, data);
}
/**
* 删除菜单
* @param id 菜单 ID
*/
async function deleteMenu(id: string) {
return requestClient.delete(`/menu/delete/${id}`);
}
export {
createMenu,
deleteMenu,
getMenuList,
isMenuNameExists,
isMenuPathExists,
updateMenu,
};

View File

@@ -0,0 +1,61 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemRoleApi {
export interface SystemRoleItem {
id: number;
role_name: string;
role_code: string;
description?: string;
status: 0 | 1;
sort: number;
create_time: string;
menu_ids: number[];
}
export interface SystemRole {
total: number;
items: SystemRoleItem[];
}
}
/**
* 获取角色列表数据
*/
async function getRoleList(params: Recordable<any>) {
return requestClient.get<SystemRoleApi.SystemRole>('/role/list', {
params,
});
}
/**
* 创建角色
* @param data 角色数据
*/
async function createRole(data: Omit<SystemRoleApi.SystemRoleItem, 'id'>) {
return requestClient.post('/role/create', data);
}
/**
* 更新角色
*
* @param id 角色 ID
* @param data 角色数据
*/
async function updateRole(
id: number,
data: Omit<SystemRoleApi.SystemRoleItem, 'id'>,
) {
return requestClient.put(`/role/update/${id}`, data);
}
/**
* 删除角色
* @param id 角色 ID
*/
async function deleteRole(id: string) {
return requestClient.delete(`/role/delete/${id}`);
}
export { createRole, deleteRole, getRoleList, updateRole };

View File

@@ -0,0 +1,64 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace SystemUserApi {
export interface SystemUser {
[key: string]: any;
id: string;
name: string;
permissions: string[];
remark?: string;
status: 0 | 1;
}
}
/**
* 获取角色列表数据
*/
async function getUserList(params: Recordable<any>) {
return requestClient.get<Array<SystemUserApi.SystemUser>>('/user/list', {
params,
});
}
/**
* 创建角色
* @param data 角色数据
*/
async function createUser(data: Omit<SystemUserApi.SystemUser, 'id'>) {
return requestClient.post('/user/create', data);
}
/**
* 更新角色
*
* @param id 角色 ID
* @param data 角色数据
*/
async function updateUser(
id: string,
data: Omit<SystemUserApi.SystemUser, 'id'>,
) {
return requestClient.put(`/user/update/${id}`, data);
}
/**
* 删除角色
* @param id 角色 ID
*/
async function deleteUser(id: string) {
return requestClient.delete(`/user/delete/${id}`);
}
/**
* 重置用户密码
* @param id 用户 ID
* @param data 新密码数据
* @param data.password 新密码
*/
async function resetPassword(id: string, data: { password: string }) {
return requestClient.post(`/reset-password/${id}`, data);
}
export { createUser, deleteUser, getUserList, resetPassword, updateUser };

39
apps/web-antd/src/app.vue Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd 紧凑模式算法
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@@ -0,0 +1,72 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="appName"
page-title="大型中后台管理系统"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { useWatermark } from '@vben/hooks';
import { BasicLayout, LockScreen, UserDropdown } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([]);
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const { destroyWatermark, updateWatermark } = useWatermark();
const _showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => []);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
async function handleLogout() {
await authStore.logout(false);
}
function _handleNoticeClear() {
notifications.value = [];
}
function _handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
watch(
() => preferences.app.watermark,
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:description="userStore.userInfo?.username"
tag-text="智能查"
@logout="handleLogout"
/>
</template>
<!-- <template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@make-all="handleMakeAll"
/>
</template> -->
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
</template>

View File

@@ -0,0 +1,6 @@
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };

View File

@@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View File

@@ -0,0 +1,102 @@
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref<Locale>(antdDefaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };

View File

@@ -0,0 +1,31 @@
export default {
platformUser: {
name: '平台用户',
list: '平台用户列表',
field: {
mobile: '手机号',
nickname: '昵称',
platform: '用户平台',
inside: '是否内部用户',
createTime: '创建时间',
updateTime: '更新时间',
info: '备注信息',
},
search: {
mobile: '请输入手机号',
nickname: '请输入昵称',
platform: '请选择用户平台',
inside: '请选择是否内部用户',
createTime: '请选择创建时间',
},
operation: '操作',
platformMap: {
h5: 'H5',
wechat: '微信',
},
insideMap: {
1: '是',
0: '否',
},
},
};

View File

@@ -0,0 +1,12 @@
{
"title": "Demos",
"antd": "Ant Design Vue",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}

View File

@@ -0,0 +1,14 @@
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}

View File

@@ -0,0 +1,99 @@
{
"title": "System Management",
"dept": {
"name": "Department",
"title": "Department Management",
"deptName": "Department Name",
"status": "Status",
"createTime": "Create Time",
"remark": "Remark",
"operation": "Operation",
"parentDept": "Parent Department"
},
"menu": {
"title": "Menu Management",
"parent": "Parent Menu",
"menuTitle": "Title",
"menuName": "Menu Name",
"name": "Menu",
"type": "Type",
"typeCatalog": "Catalog",
"typeMenu": "Menu",
"typeButton": "Button",
"typeLink": "Link",
"typeEmbedded": "Embedded",
"icon": "Icon",
"activeIcon": "Active Icon",
"activePath": "Active Path",
"path": "Route Path",
"component": "Component",
"status": "Status",
"authCode": "Auth Code",
"badge": "Badge",
"operation": "Operation",
"linkSrc": "Link Address",
"affixTab": "Affix In Tabs",
"keepAlive": "Keep Alive",
"hideInMenu": "Hide In Menu",
"hideInTab": "Hide In Tabbar",
"hideChildrenInMenu": "Hide Children In Menu",
"hideInBreadcrumb": "Hide In Breadcrumb",
"advancedSettings": "Other Settings",
"activePathMustExist": "The path could not find a valid menu",
"activePathHelp": "When jumping to the current route, \nthe menu path that needs to be activated must be specified when it does not display in the navigation menu.",
"badgeType": {
"title": "Badge Type",
"dot": "Dot",
"normal": "Text",
"none": "None"
},
"badgeVariants": "Badge Style"
},
"role": {
"title": "Role Management",
"list": "Role List",
"name": "Role",
"roleName": "Role Name",
"id": "Role ID",
"status": "Status",
"remark": "Remark",
"createTime": "Creation Time",
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Permissions",
"setApiPermissions": "API Permissions",
"roleCode": "Role Code",
"description": "Description"
},
"api": {
"title": "API Management",
"list": "API List",
"name": "API",
"apiName": "API Name",
"apiCode": "API Code",
"method": "Request Method",
"url": "API URL",
"status": "Status",
"description": "Description",
"createTime": "Create Time",
"operation": "Operation",
"permissions": "Permissions",
"setPermissions": "Set Permissions"
},
"user": {
"title": "User Management",
"name": "User",
"list": "User List",
"userName": "Username",
"realName": "Real Name",
"status": "Status",
"setPermissions": "Set Permissions",
"createTime": "Create Time",
"operation": "Operation",
"resetPassword": "Reset Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"confirmPasswordRequired": "Please confirm password",
"passwordMismatch": "The two passwords do not match"
}
}

View File

@@ -0,0 +1,12 @@
{
"title": "演示",
"antd": "Ant Design Vue",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}

View File

@@ -0,0 +1,14 @@
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}

View File

@@ -0,0 +1,101 @@
{
"dept": {
"list": "部门列表",
"createTime": "创建时间",
"deptName": "部门名称",
"name": "部门",
"operation": "操作",
"parentDept": "上级部门",
"remark": "备注",
"status": "状态",
"title": "部门管理"
},
"menu": {
"list": "菜单列表",
"activeIcon": "激活图标",
"activePath": "激活路径",
"activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时需要指定激活路径",
"activePathMustExist": "该路径未能找到有效的菜单",
"advancedSettings": "其它设置",
"affixTab": "固定在标签",
"authCode": "权限标识",
"badge": "徽章内容",
"badgeVariants": "徽标样式",
"badgeType": {
"dot": "点",
"none": "无",
"normal": "文字",
"title": "徽标类型"
},
"component": "页面组件",
"hideChildrenInMenu": "隐藏子菜单",
"hideInBreadcrumb": "在面包屑中隐藏",
"hideInMenu": "隐藏菜单",
"hideInTab": "在标签栏中隐藏",
"icon": "图标",
"keepAlive": "缓存标签页",
"linkSrc": "链接地址",
"menuName": "菜单名称",
"menuTitle": "标题",
"name": "菜单",
"operation": "操作",
"parent": "上级菜单",
"path": "路由地址",
"status": "状态",
"title": "菜单管理",
"type": "类型",
"typeButton": "按钮",
"typeCatalog": "目录",
"typeEmbedded": "内嵌",
"typeLink": "外链",
"typeMenu": "菜单"
},
"role": {
"title": "角色管理",
"list": "角色列表",
"name": "角色",
"roleName": "角色名称",
"id": "角色ID",
"status": "状态",
"remark": "备注",
"createTime": "创建时间",
"operation": "操作",
"permissions": "权限",
"setPermissions": "授权",
"setApiPermissions": "API权限",
"roleCode": "角色编号",
"description": "描述"
},
"title": "系统管理",
"api": {
"title": "API管理",
"list": "API列表",
"name": "API",
"apiName": "API名称",
"apiCode": "API编码",
"method": "请求方法",
"url": "API地址",
"status": "状态",
"description": "描述",
"createTime": "创建时间",
"operation": "操作",
"permissions": "权限",
"setPermissions": "设置权限"
},
"user": {
"title": "用户管理",
"name": "用户",
"list": "用户列表",
"userName": "用户名",
"realName": "真实姓名",
"status": "状态",
"setPermissions": "设置权限",
"createTime": "创建时间",
"operation": "操作",
"resetPassword": "重置密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"confirmPasswordRequired": "请确认密码",
"passwordMismatch": "两次输入的密码不一致"
}
}

31
apps/web-antd/src/main.ts Normal file
View File

@@ -0,0 +1,31 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@@ -0,0 +1,27 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
accessMode: 'backend',
},
logo: {
source: 'https://ctrlph.tianyuandb.com/logo.png',
},
copyright: {
companyName: '海南海宇大数据有限公司',
companySiteLink: 'https://www.tianyuandb.com',
date: '2025',
icp: '琼ICP备2024048057号-1',
icpLink: 'https://beian.miit.gov.cn/',
},
footer: {
enable: false,
},
});

View File

@@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@@ -0,0 +1,133 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@@ -0,0 +1,37 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@@ -0,0 +1,96 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局作为所有页面的父级容器子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@@ -0,0 +1,46 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
const componentKeys: string[] = Object.keys(
import.meta.glob('../../views/**/*.vue'),
)
.filter((item) => !item.includes('/modules/'))
.map((v) => {
const path = v.replace('../../views/', '/');
return path.endsWith('.vue') ? path.slice(0, -4) : path;
});
export { accessRoutes, componentKeys, coreRouteNames, routes };

View File

@@ -0,0 +1,38 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,16 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:shopping-cart',
order: 1000,
title: '订单管理',
},
name: 'Order',
path: '/order',
component: () => import('#/views/order/order/index.vue'),
},
];
export default routes;

View File

@@ -0,0 +1,64 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ion:settings-outline',
order: 9997,
title: $t('system.title'),
},
name: 'System',
path: '/system',
children: [
{
path: '/system/user',
name: 'SystemUser',
meta: {
icon: 'mdi:account',
title: $t('system.user.title'),
},
component: () => import('#/views/system/user/list.vue'),
},
{
path: '/system/role',
name: 'SystemRole',
meta: {
icon: 'mdi:account-group',
title: $t('system.role.title'),
},
component: () => import('#/views/system/role/list.vue'),
},
{
path: '/system/api',
name: 'SystemApi',
meta: {
icon: 'mdi:api',
title: $t('system.api.title'),
},
component: () => import('#/views/system/api/list.vue'),
},
{
path: '/system/menu',
name: 'SystemMenu',
meta: {
icon: 'mdi:menu',
title: $t('system.menu.title'),
},
component: () => import('#/views/system/menu/list.vue'),
},
{
path: '/system/dept',
name: 'SystemDept',
meta: {
icon: 'charm:organisation',
title: $t('system.dept.title'),
},
component: () => import('#/views/system/dept/list.vue'),
},
],
},
];
export default routes;

View File

@@ -0,0 +1,115 @@
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: Recordable<any>,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { access_token } = await loginApi(params);
console.info('accessToken', access_token);
// 如果成功获取到 accessToken
if (access_token) {
accessStore.setAccessToken(access_token);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});

View File

@@ -0,0 +1 @@
export * from './auth';

View File

@@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { About } from '@vben/common-ui';
defineOptions({ name: 'About' });
</script>
<template>
<About />
</template>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
</template>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import { computed, markRaw } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
},
];
});
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
:show-third-party-login="false"
:show-register="false"
:show-forget-password="false"
:show-qrcode-login="false"
:show-code-login="false"
@submit="authStore.authLogin"
/>
</template>

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'QrCodeLogin' });
</script>
<template>
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
</template>

View File

@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, h, ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'Register' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string({ required_error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class: 'vben-link ml-1 ',
href: '',
},
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

View File

@@ -0,0 +1,101 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useCommissionDeductionColumns(): VxeTableGridOptions['columns'] {
return [
{
title: 'ID',
field: 'id',
width: 80,
},
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '被扣代理ID',
field: 'deducted_agent_id',
width: 100,
},
{
title: '抽佣金额',
field: 'amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '产品名称',
field: 'product_name',
width: 150,
},
{
title: '抽佣类型',
field: 'type',
width: 120,
formatter: ({ cellValue }: { cellValue: 'cost' | 'pricing' }) => {
const typeMap = {
cost: '成本抽佣',
pricing: '定价抽佣',
};
return typeMap[cellValue] || cellValue;
},
},
{
title: '状态',
field: 'status',
width: 100,
cellRender: {
name: 'CellTag',
options: [
{ value: 0, color: 'warning', label: '待结算' },
{ value: 1, color: 'success', label: '已结算' },
{ value: 2, color: 'error', label: '已取消' },
],
},
},
{
title: '创建时间',
field: 'create_time',
width: 180,
},
];
}
export function useCommissionDeductionFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'product_name',
label: '产品名称',
component: 'Input',
componentProps: {
allowClear: true,
},
},
{
fieldName: 'type',
label: '抽佣类型',
component: 'Select',
componentProps: {
options: [
{ label: '成本抽佣', value: 'cost' },
{ label: '定价抽佣', value: 'pricing' },
],
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '待结算', value: 0 },
{ label: '已结算', value: 1 },
{ label: '已取消', value: 2 },
],
allowClear: true,
},
},
];
}

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentCommissionDeductionList } from '#/api/agent';
import {
useCommissionDeductionColumns,
useCommissionDeductionFormSchema,
} from './data';
interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCommissionDeductionFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useCommissionDeductionColumns(),
proxyConfig: {
ajax: {
query: async ({
form,
page,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentCommissionDeductionList({
...queryParams.value,
...form,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '上级抽佣列表' : '所有上级抽佣记录'" />
</Page>
</template>

View File

@@ -0,0 +1,135 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AgentApi } from '#/api';
// 佣金记录列表列配置
export function useCommissionColumns(
onActionClick?: (params: { code: string; row: AgentApi.AgentCommissionListItem }) => void,
): VxeTableGridOptions['columns'] {
return [
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'order_id',
title: '订单ID',
width: 100,
},
{
field: 'amount',
title: '佣金金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'status',
title: '状态',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
0: '已结算',
1: '冻结中',
2: '已退款',
};
return statusMap[cellValue] || '未知';
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
sortType: 'string' as const,
},
{
align: 'center',
cellRender: {
name: 'CellOperation',
attrs: {
nameField: 'id',
nameTitle: '操作',
onClick: onActionClick,
},
options: [
{
code: 'freeze',
text: '冻结',
type: 'warning',
disabled: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 0,
class: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 0 ? '!text-gray-400 !cursor-not-allowed' : '',
tooltip: (row: AgentApi.AgentCommissionListItem) => {
if (row?.status === 1) return '该佣金已处于冻结中';
if (row?.status === 2) return '已取消的佣金无法操作';
return '';
},
},
{
code: 'unfreeze',
text: '解冻',
type: 'primary',
disabled: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 1,
class: (row: AgentApi.AgentCommissionListItem) =>
row?.status !== 1 ? '!text-gray-400 !cursor-not-allowed' : '',
tooltip: (row: AgentApi.AgentCommissionListItem) => {
if (row?.status === 0) return '已结算的佣金无需解冻';
if (row?.status === 2) return '已退款的佣金无法操作';
return '';
},
},
],
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 300,
},
];
}
// 佣金记录搜索表单配置
export function useCommissionFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'order_id',
label: '订单ID',
componentProps: {
placeholder: '请输入订单ID',
style: { width: '100%' },
},
},
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
componentProps: {
placeholder: '请输入产品名称(支持模糊搜索)',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
placeholder: '请选择状态',
options: [
{ label: '已结算', value: 0 },
{ label: '冻结中', value: 1 },
{ label: '已退款', value: 2 },
],
},
},
];
}

View File

@@ -0,0 +1,456 @@
<script lang="ts" setup>
import { computed, h, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, message, Modal, Select, Switch, Tooltip } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
batchUnfreezeAgentCommission,
getAgentCommissionList,
getAgentList,
getAgentWallet,
updateAgentCommissionStatus,
getSystemConfig,
updateSystemConfig,
} from '#/api/agent';
import type { AgentApi } from '#/api';
import { useCommissionColumns, useCommissionFormSchema } from './data';
interface Props {
agentId?: number;
}
const props = defineProps<Props>();
// 用于一键解冻筛选的代理商ID
const unfreezeAgentId = ref<number | undefined>();
// 佣金安全防御模式配置
const commissionSafeMode = ref<boolean>(false);
const safeModeLoading = ref<boolean>(false);
// 代理商列表(完整列表)
const allAgentList = ref<AgentApi.AgentListItem[]>([]);
// 显示在下拉框中的代理商列表(可能是过滤后的)
const agentList = ref<AgentApi.AgentListItem[]>([]);
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
...(unfreezeAgentId.value ? { agent_id: unfreezeAgentId.value } : {}),
}));
// 加载代理商列表
async function loadAgentList() {
try {
const result = await getAgentList({ page: 1, pageSize: 10000 });
allAgentList.value = result.items || [];
agentList.value = result.items || [];
} catch (error) {
console.error('加载代理商列表失败:', error);
message.error('加载代理商列表失败,请刷新页面重试');
}
}
// 搜索时动态过滤代理商支持ID和手机号
function onAgentSearch(value: string) {
if (!value || value.trim() === '') {
// 如果输入为空,显示所有代理商
agentList.value = allAgentList.value;
return;
}
const searchValue = value.trim();
// 从完整列表中过滤匹配的代理商
const filtered = allAgentList.value.filter(agent => {
// 匹配代理ID
if (agent.id.toString().includes(searchValue)) {
return true;
}
// 匹配手机号
if (agent.mobile && agent.mobile.includes(searchValue)) {
return true;
}
// 匹配姓名(如果存在)
if (agent.real_name && agent.real_name.includes(searchValue)) {
return true;
}
return false;
});
agentList.value = filtered;
}
// 获取未找到时的提示文案
function getNotFoundContent() {
if (unfreezeAgentId.value) {
return '点击确认使用此ID';
}
return '暂无代理商';
}
// 页面加载时获取代理商列表和系统配置
onMounted(async () => {
loadAgentList();
await loadSystemConfig();
});
// 加载系统配置
async function loadSystemConfig() {
try {
const config = await getSystemConfig();
commissionSafeMode.value = config.commission_safe_mode;
} catch (error: any) {
console.error('加载系统配置失败:', error);
message.error('加载系统配置失败');
}
}
// 切换安全防御模式
async function onSafeModeChange(checked: boolean | string | number) {
const isChecked = Boolean(checked);
safeModeLoading.value = true;
try {
await updateSystemConfig({ commission_safe_mode: isChecked });
commissionSafeMode.value = isChecked;
message.success(`佣金安全防御模式已${isChecked ? '开启' : '关闭'}`);
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
// 恢复原状态
commissionSafeMode.value = !isChecked;
} finally {
safeModeLoading.value = false;
}
}
// 操作处理函数
function onActionClick({ code, row }: { code: string; row: any }) {
switch (code) {
case 'freeze':
onFreeze(row);
break;
case 'unfreeze':
onUnfreeze(row);
break;
}
}
// 冻结佣金
async function onFreeze(row: any) {
try {
// 先获取代理商钱包信息,检查余额是否充足
const hideChecking = message.loading({
content: '正在检查余额...',
duration: 0,
key: 'check_balance',
});
const wallet = await getAgentWallet(row.agent_id);
hideChecking();
// 检查余额是否充足
if (wallet.balance < row.amount) {
Modal.warning({
title: '余额不足',
content: `该代理商当前可用余额为 ¥${wallet.balance.toFixed(2)},不足以冻结佣金 ¥${row.amount.toFixed(2)}\n缺少金额¥${(row.amount - wallet.balance).toFixed(2)}`,
okText: '我知道了',
});
return;
}
// 余额充足,继续冻结操作
Modal.confirm({
title: '确认冻结',
content: `确定要冻结佣金金额 ¥${row.amount.toFixed(2)} 吗?\n当前可用余额¥${wallet.balance.toFixed(2)},冻结后可用余额:¥${(wallet.balance - row.amount).toFixed(2)}`,
okText: '确认冻结',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 1);
message.success('佣金已冻结');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
},
});
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '获取钱包信息失败,请重试';
message.error(errorMsg);
}
}
// 解冻佣金到用户余额
async function onUnfreeze(row: any) {
try {
// 获取代理商钱包信息用于显示
const hideChecking = message.loading({
content: '正在获取钱包信息...',
duration: 0,
key: 'get_wallet',
});
const wallet = await getAgentWallet(row.agent_id);
hideChecking();
Modal.confirm({
title: '确认解冻',
content: `确定要解冻佣金金额 ¥${row.amount.toFixed(2)} 吗?\n解冻后将转入用户钱包余额。\n当前可用余额¥${wallet.balance.toFixed(2)},解冻后可用余额:¥${(wallet.balance + row.amount).toFixed(2)}`,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
try {
await updateAgentCommissionStatus(row.id, 0);
message.success('佣金已解冻并转入用户钱包余额');
onRefresh();
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '操作失败,请重试';
message.error(errorMsg);
}
},
});
} catch (error: any) {
const errorMsg = error?.response?.data?.msg || error?.message || '获取钱包信息失败,请重试';
message.error(errorMsg);
}
}
// 刷新列表
function onRefresh() {
gridApi.query();
}
// 选中代理商后刷新表格
function onAgentSelect(value: any) {
console.log('选中代理商:', value);
// 如果是清空选择value 为 undefined
// 如果有值,转换为数字
unfreezeAgentId.value = value ? parseInt(String(value), 10) : undefined;
// 延迟一点让 computed 更新后再刷新
setTimeout(() => {
onRefresh();
}, 100);
}
// 批量解冻佣金
async function onBatchUnfreeze() {
const targetAgentId = unfreezeAgentId.value || props.agentId;
const hideChecking = message.loading({
content: '正在检查数据,请稍候...',
duration: 0,
key: 'check_frozen_data',
});
try {
// 1. 查询所有冻结中的佣金记录
const frozenCommissions = await getAgentCommissionList({
page: 1,
pageSize: 10000,
status: 1,
...(targetAgentId ? { agent_id: targetAgentId } : {}),
});
hideChecking();
// 如果没有冻结的佣金,直接返回
if (!frozenCommissions.items || frozenCommissions.items.length === 0) {
message.info({
content: '没有需要解冻的冻结佣金',
key: 'check_frozen_data',
});
return;
}
// 2. 统计每个代理商的佣金冻结金额
const commissionFrozenMap = new Map<number, number>();
frozenCommissions.items.forEach((item: any) => {
const currentAmount = commissionFrozenMap.get(item.agent_id) || 0;
commissionFrozenMap.set(item.agent_id, currentAmount + item.amount);
});
// 3. 检查每个代理商的钱包冻结余额是否足够
let insufficientAgents: string[] = [];
let totalFrozenAmount = 0;
for (const [agentId, commissionAmount] of commissionFrozenMap) {
totalFrozenAmount += commissionAmount;
// 获取该代理商的钱包信息
const wallet = await getAgentWallet(agentId);
if (wallet.frozen_balance < commissionAmount) {
// 找到该代理商的信息
const agent = allAgentList.value.find(a => a.id === agentId);
const agentInfo = agent ? `${agent.real_name || agent.mobile} (ID: ${agentId})` : `ID: ${agentId}`;
insufficientAgents.push(
`${agentInfo}:钱包冻结余额 ¥${wallet.frozen_balance.toFixed(2)},需要解冻 ¥${commissionAmount.toFixed(2)},缺少 ¥${(commissionAmount - wallet.frozen_balance).toFixed(2)}`
);
}
}
// 如果有余额不足的代理商,显示错误信息
if (insufficientAgents.length > 0) {
Modal.error({
title: '冻结余额不足,无法解冻',
width: 600,
content: h('div', [
h('p', '以下代理商的钱包冻结余额不足以解冻其冻结的佣金:'),
h('ul', { style: { 'max-height': '300px', 'overflow-y': 'auto', 'padding-left': '20px' } },
insufficientAgents.map(msg => h('li', { style: { marginBottom: '8px' } }, msg))
),
h('p', { style: { marginTop: '16px', color: '#999' } }, '请先核实钱包冻结余额数据,或联系技术支持。'),
]),
okText: '我知道了',
});
return;
}
// 4. 余额检查通过,确认解冻
const content = targetAgentId
? `确定要一键解冻代理商 ID: ${targetAgentId} 所有冻结中的佣金吗?\n\n共 ${frozenCommissions.items.length} 条记录,总金额 ¥${totalFrozenAmount.toFixed(2)}\n解冻后将全部转入用户钱包余额。`
: `确定要一键解冻所有冻结中的佣金吗?\n\n共 ${frozenCommissions.items.length} 条记录,总金额 ¥${totalFrozenAmount.toFixed(2)}\n解冻后将全部转入用户钱包余额。`;
Modal.confirm({
title: '批量解冻确认',
content,
okText: '确认解冻',
cancelText: '取消',
onOk: async () => {
const hideLoading = message.loading({
content: '正在批量解冻佣金,请稍候...',
duration: 0,
key: 'batch_unfreeze',
});
try {
const result = await batchUnfreezeAgentCommission(targetAgentId);
message.success({
content: `批量解冻成功!共解冻 ${result.count} 条记录,总金额 ¥${result.amount.toFixed(2)}`,
key: 'batch_unfreeze',
});
onRefresh();
} catch (error: any) {
hideLoading();
const errorMsg = error?.response?.data?.msg || error?.message || '批量解冻失败,请重试';
// 如果是版本冲突错误,给出更友好的提示
if (errorMsg.includes('update db no rows change') || errorMsg.includes('版本冲突') || errorMsg.includes('状态已被其他操作修改')) {
message.error({
content: '批量解冻失败:部分佣金或钱包数据已被其他操作修改,请稍后重试。如果问题持续,请联系管理员。',
key: 'batch_unfreeze',
});
} else {
message.error({
content: `批量解冻失败:${errorMsg}`,
key: 'batch_unfreeze',
});
}
}
},
});
} catch (error: any) {
hideChecking();
const errorMsg = error?.response?.data?.msg || error?.message || '数据检查失败,请重试';
message.error({
content: `检查失败:${errorMsg}`,
key: 'check_frozen_data',
});
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useCommissionFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useCommissionColumns(onActionClick),
height: 'auto',
keepSource: true,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
proxyConfig: {
ajax: {
query: async ({ page, form, sort }: any, formValues: Record<string, any>) => {
return await getAgentCommissionList({
...queryParams.value,
...formValues,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
autoLoad: true,
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '佣金记录列表' : '所有佣金记录'">
<template #toolbar-tools>
<div class="flex items-center">
<Tooltip placement="top" title="开启后,佣金结算时将先冻结到钱包冻结余额,需要手动解冻才能使用;关闭后,佣金将直接结算到可用余额">
<div class="flex items-center gap-2 mr-4">
<span class="text-sm text-gray-600">安全防御机制:</span>
<Switch
v-model:checked="commissionSafeMode"
:loading="safeModeLoading"
checked-children="开启"
un-checked-children="关闭"
@change="onSafeModeChange"
/>
</div>
</Tooltip>
<div class="w-px h-6 bg-gray-300 mx-2"></div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">选择代理商:</span>
<Select
v-model:value="unfreezeAgentId"
placeholder="全部代理商 / 输入代理ID或手机号"
:allow-clear="true"
:loading="agentList.length === 0"
style="width: 260px"
show-search
:filter-option="false"
:show-arrow="true"
:not-found-content="getNotFoundContent()"
@search="onAgentSearch"
@change="onAgentSelect"
>
<Select.Option
v-for="agent in agentList"
:key="agent.id"
:value="agent.id"
:label="`${agent.real_name || agent.mobile} (ID: ${agent.id})`"
>
{{ agent.real_name || agent.mobile }} (ID: {{ agent.id }})
</Select.Option>
</Select>
<Button type="primary" @click="onBatchUnfreeze">
<span class="mr-1"></span>
一键解冻
</Button>
</div>
</div>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,51 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
// 推广链接列表列配置
export function useLinkColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'price',
title: '价格',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
field: 'link_identifier',
title: '推广码',
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
sortType: 'string' as const,
},
];
}
// 推广链接搜索表单配置
export function useLinkFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
},
{
component: 'Input',
fieldName: 'link_identifier',
label: '推广码',
},
];
}

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentLinkList } from '#/api/agent';
import { useLinkColumns, useLinkFormSchema } from './data';
interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useLinkFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useLinkColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentLinkList({
...queryParams.value,
...form,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '推广链接列表' : '所有推广链接'" />
</Page>
</template>

View File

@@ -0,0 +1,436 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
// 表单配置
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'mobile',
label: '手机号',
rules: 'required',
},
{
component: 'Input',
fieldName: 'level_name',
label: '等级名称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'region',
label: '区域',
rules: 'required',
},
{
component: 'DatePicker',
fieldName: 'membership_expiry_time',
label: '会员到期时间',
rules: 'required',
componentProps: {
showTime: true,
},
},
];
}
// 搜索表单配置
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'mobile',
label: '手机号',
},
{
component: 'Input',
fieldName: 'region',
label: '区域',
},
{
component: 'RangePicker',
fieldName: 'create_time',
label: '创建时间',
componentProps: {
showTime: true,
},
},
];
}
// 表格列配置
export function useColumns(): VxeTableGridOptions['columns'] {
const columns = [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'user_id',
title: '用户ID',
width: 100,
},
{
field: 'level_name',
title: '等级名称',
width: 120,
formatter: ({ cellValue }: { cellValue: string }) => {
if (cellValue === '' || cellValue === 'normal') {
return '普通代理';
}
return cellValue;
},
},
{
field: 'region',
title: '区域',
width: 120,
},
{
field: 'mobile',
title: '手机号',
width: 120,
},
{
cellRender: {
name: 'CellTag',
options: [
{ value: 'approved', color: 'success', label: '已认证' },
{ value: 'pending', color: 'warning', label: '审核中' },
{ value: 'rejected', color: 'error', label: '已拒绝' },
{ value: '', color: 'default', label: '未认证' },
],
},
field: 'real_name_status',
title: '实名认证状态',
width: 120,
},
{
field: 'real_name',
title: '实名姓名',
width: 120,
formatter: ({ cellValue }: { cellValue: string }) => {
if (!cellValue) return '-';
return cellValue;
},
},
{
field: 'id_card',
title: '身份证号',
width: 180,
formatter: ({ cellValue }: { cellValue: string }) => {
if (!cellValue) return '-';
// 只显示前6位和后4位中间用*代替
return `${cellValue.slice(0, 6)}${'*'.repeat(8)}${cellValue.slice(-4)}`;
},
},
{
field: 'membership_expiry_time',
title: '会员到期时间',
width: 160,
sortable: true,
sortType: 'string' as const,
},
{
field: 'balance',
title: '钱包余额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'total_earnings',
title: '累计收益',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'frozen_balance',
title: '冻结余额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'withdrawn_amount',
title: '提现总额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'create_time',
title: '成为代理时间',
width: 160,
sortable: true,
sortType: 'string' as const,
},
{
align: 'center' as const,
slots: { default: 'operation' },
field: 'operation',
fixed: 'right' as const,
title: '操作',
width: 280,
},
];
return columns;
}
// 推广链接列表列配置
export function useLinkColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'price',
title: '价格',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
field: 'link_identifier',
title: '推广码',
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
sortType: 'string' as const,
},
];
}
// 推广链接搜索表单配置
export function useLinkFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
},
{
component: 'Input',
fieldName: 'link_identifier',
label: '推广码',
},
];
}
// 佣金记录列表列配置
export function useCommissionColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'order_id',
title: '订单ID',
width: 100,
},
{
field: 'amount',
title: '佣金金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'status',
title: '状态',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
0: '待结算',
1: '已结算',
2: '已取消',
};
return statusMap[cellValue] || '未知';
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
// 佣金记录搜索表单配置
export function useCommissionFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '待结算', value: 0 },
{ label: '已结算', value: 1 },
{ label: '已取消', value: 2 },
],
},
},
];
}
// 奖励记录列表列配置
export function useRewardColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'relation_agent_id',
title: '关联代理ID',
width: 100,
},
{
field: 'amount',
title: '奖励金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'type',
title: '奖励类型',
width: 120,
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
// 奖励记录搜索表单配置
export function useRewardFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'type',
label: '奖励类型',
},
{
component: 'Input',
fieldName: 'relation_agent_id',
label: '关联代理ID',
},
];
}
// 提现记录列表列配置
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'withdraw_no',
title: '提现单号',
width: 180,
},
{
field: 'amount',
title: '提现金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'status',
title: '状态',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '已拒绝',
3: '已打款',
};
return statusMap[cellValue] || '未知';
},
},
{
field: 'payee_account',
title: '收款账户',
width: 180,
},
{
field: 'remark',
title: '备注',
width: 200,
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
// 提现记录搜索表单配置
export function useWithdrawalFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'withdraw_no',
label: '提现单号',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '待审核', value: 0 },
{ label: '已通过', value: 1 },
{ label: '已拒绝', value: 2 },
{ label: '已打款', value: 3 },
],
},
},
];
}

View File

@@ -0,0 +1,405 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeGridListeners,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { Button, Card, Dropdown, Menu } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentList } from '#/api/agent';
import { useColumns, useGridFormSchema } from './data';
import CommissionDeductionModal from './modules/commission-deduction-modal.vue';
import CommissionModal from './modules/commission-modal.vue';
import CommissionHistoryModal from './modules/commission-history-modal.vue';
import Form from './modules/form.vue';
import LinkModal from './modules/link-modal.vue';
import PlatformDeductionModal from './modules/platform-deduction-modal.vue';
import RewardModal from './modules/reward-modal.vue';
import WithdrawalModal from './modules/withdrawal-modal.vue';
import BalanceModal from './modules/balance-modal.vue';
import WalletTransactionModal from './modules/wallet-transaction-modal.vue';
const route = useRoute();
const router = useRouter();
// 表单抽屉
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
// 推广链接弹窗
const [LinkModalComponent, linkModalApi] = useVbenModal({
connectedComponent: LinkModal,
destroyOnClose: true,
});
// 佣金记录弹窗
const [CommissionModalComponent, commissionModalApi] = useVbenModal({
connectedComponent: CommissionModal,
destroyOnClose: true,
});
// 奖励记录弹窗
const [RewardModalComponent, rewardModalApi] = useVbenModal({
connectedComponent: RewardModal,
destroyOnClose: true,
});
// 提现记录弹窗
const [WithdrawalModalComponent, withdrawalModalApi] = useVbenModal({
connectedComponent: WithdrawalModal,
destroyOnClose: true,
});
// 上级抽佣弹窗
const [CommissionDeductionModalComponent, commissionDeductionModalApi] =
useVbenModal({
connectedComponent: CommissionDeductionModal,
destroyOnClose: true,
});
// 平台抽佣弹窗
const [PlatformDeductionModalComponent, platformDeductionModalApi] =
useVbenModal({
connectedComponent: PlatformDeductionModal,
destroyOnClose: true,
});
// 修改余额弹窗
const [BalanceModalComponent, balanceModalApi] = useVbenModal({
connectedComponent: BalanceModal,
destroyOnClose: true,
});
// 钱包流水记录弹窗
const [WalletTransactionModalComponent, walletTransactionModalApi] = useVbenModal({
connectedComponent: WalletTransactionModal,
destroyOnClose: true,
});
// 历史佣金记录弹窗
const [CommissionHistoryModalComponent, commissionHistoryModalApi] = useVbenModal({
connectedComponent: CommissionHistoryModal,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [
['create_time', ['create_time_start', 'create_time_end']],
],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridEvents: {
sortChange: () => {
gridApi.query();
},
} as VxeGridListeners<AgentApi.AgentListItem>,
gridOptions: {
columns: useColumns(),
height: 'auto',
keepSource: true,
sortConfig: {
remote: true,
multiple: false,
trigger: 'default',
orders: ['asc', 'desc', null],
resetPage: true,
},
proxyConfig: {
ajax: {
query: async ({ page, sort }, formValues) => {
const sortParams = sort
? {
order_by: sort.field,
order_type: sort.order,
}
: {};
const res = await getAgentList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
...sortParams,
parent_agent_id: route.query.parent_agent_id
? Number(route.query.parent_agent_id)
: undefined,
});
return {
...res,
sort: sort || null,
};
},
},
props: {
result: 'items',
total: 'total',
},
autoLoad: true,
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<AgentApi.AgentListItem>,
});
// 更多操作菜单项
const moreMenuItems = [
{
key: 'update-balance',
label: '修改余额',
},
{
key: 'commission-history',
label: '历史佣金记录',
},
{
key: 'wallet-transaction',
label: '钱包流水记录',
},
{
key: 'links',
label: '推广链接',
},
// {
// key: 'commission',
// label: '佣金记录',
// },
// {
// key: 'commission-deduction',
// label: '上级抽佣',
// },
// {
// key: 'platform-deduction',
// label: '平台抽佣',
// },
{
key: 'reward',
label: '奖励记录',
},
{
key: 'withdrawal',
label: '提现记录',
},
];
// 上级代理信息
const parentAgentId = computed(() => route.query.parent_agent_id);
// 返回上级列表
function onBackToParent() {
router.replace({
query: {
...route.query,
parent_agent_id: undefined,
},
});
}
// 操作处理函数
function onActionClick(
e:
| OnActionClickParams<AgentApi.AgentListItem>
| { code: string; row: AgentApi.AgentListItem },
) {
switch (e.code) {
case 'commission': {
onViewCommission(e.row);
break;
}
case 'commission-history': {
onViewCommissionHistory(e.row);
break;
}
case 'commission-deduction': {
onViewCommissionDeduction(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
case 'links': {
onViewLinks(e.row);
break;
}
case 'platform-deduction': {
onViewPlatformDeduction(e.row);
break;
}
case 'reward': {
onViewReward(e.row);
break;
}
case 'update-balance': {
onUpdateBalance(e.row);
break;
}
case 'view-sub-agent': {
router.replace({
query: {
...route.query,
parent_agent_id: e.row.id,
},
});
break;
}
case 'withdrawal': {
onViewWithdrawal(e.row);
break;
}
case 'wallet-transaction': {
onViewWalletTransaction(e.row);
break;
}
}
}
// 编辑处理
function onEdit(row: AgentApi.AgentListItem) {
formDrawerApi.setData(row).open();
}
// 查看推广链接
function onViewLinks(row: AgentApi.AgentListItem) {
linkModalApi.setData({ agentId: row.id }).open();
}
// 修改余额
function onUpdateBalance(row: AgentApi.AgentListItem) {
balanceModalApi.setData({ agentId: row.id }).open();
}
// 查看钱包流水记录
function onViewWalletTransaction(row: AgentApi.AgentListItem) {
walletTransactionModalApi.setData({ agentId: row.id }).open();
}
// 查看佣金记录
function onViewCommission(row: AgentApi.AgentListItem) {
commissionModalApi.setData({ agentId: row.id }).open();
}
// 查看奖励记录
function onViewReward(row: AgentApi.AgentListItem) {
rewardModalApi.setData({ agentId: row.id }).open();
}
// 查看提现记录
function onViewWithdrawal(row: AgentApi.AgentListItem) {
withdrawalModalApi.setData({ agentId: row.id }).open();
}
// 查看上级抽佣记录
function onViewCommissionDeduction(row: AgentApi.AgentListItem) {
commissionDeductionModalApi.setData({ agentId: row.id }).open();
}
// 查看平台抽佣记录
function onViewPlatformDeduction(row: AgentApi.AgentListItem) {
platformDeductionModalApi.setData({ agentId: row.id }).open();
}
// 查看历史佣金记录
function onViewCommissionHistory(row: AgentApi.AgentListItem) {
commissionHistoryModalApi.setData({ agentId: row.id }).open();
}
// 刷新处理
function onRefresh() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<LinkModalComponent />
<CommissionModalComponent />
<CommissionHistoryModalComponent />
<CommissionDeductionModalComponent />
<PlatformDeductionModalComponent />
<RewardModalComponent />
<WithdrawalModalComponent />
<BalanceModalComponent @success="onRefresh" />
<WalletTransactionModalComponent />
<!-- 上级代理信息卡片 -->
<Card v-if="parentAgentId" class="mb-4">
<div class="flex items-center gap-4">
<Button @click="onBackToParent">返回上级列表</Button>
<div>上级代理ID{{ parentAgentId }}</div>
</div>
</Card>
<Grid table-title="代理列表">
<template #operation="{ row }">
<div class="operation-buttons">
<Button type="link" @click="onActionClick({ code: 'edit', row })">
编辑
</Button>
<Button
type="link"
@click="onActionClick({ code: 'view-sub-agent', row })"
>
查看下级
</Button>
<!-- <Button
type="link"
@click="onActionClick({ code: 'order-record', row })"
>
订单记录
</Button> -->
<Dropdown>
<Button type="link">更多操作</Button>
<template #overlay>
<Menu
:items="moreMenuItems"
@click="(e) => onActionClick({ code: String(e.key), row })"
/>
</template>
</Dropdown>
</div>
</template>
</Grid>
</Page>
</template>
<style lang="less" scoped>
.parent-agent-card {
margin-bottom: 16px;
background-color: #fafafa;
}
.operation-buttons {
display: flex;
align-items: center;
justify-content: space-around;
gap: 4px;
:deep(.ant-btn-link) {
padding: 0 4px;
}
}
</style>

View File

@@ -0,0 +1,161 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { getAgentWallet, updateAgentWalletBalance } from '#/api/agent';
import type { AgentApi } from '#/api/agent';
interface ModalData {
agentId: number;
}
// 钱包信息
const walletInfo = ref<AgentApi.AgentWalletInfo>({
balance: 0,
frozen_balance: 0,
total_earnings: 0,
});
const loading = ref(false);
const emit = defineEmits<{
(e: 'success'): void;
}>();
const [ModalComponent, modalApi] = useVbenModal({
title: '修改余额',
destroyOnClose: true,
onOpenChange(isOpen) {
if (isOpen) {
fetchWalletInfo();
// 重置表单
formApi.setValues({
operation_type: 'add',
amount: undefined,
});
}
},
});
const modalData = computed(() => modalApi.getData<ModalData>());
// 获取钱包信息
async function fetchWalletInfo() {
const agentId = modalData.value?.agentId;
if (!agentId) return;
try {
loading.value = true;
const data = await getAgentWallet(agentId);
walletInfo.value = data;
} catch (error: any) {
message.error(error.message || '获取钱包信息失败');
} finally {
loading.value = false;
}
}
// 表单引用
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'RadioGroup',
fieldName: 'operation_type',
label: '操作类型',
componentProps: {
options: [
{ label: '增加余额', value: 'add' },
{ label: '减少余额', value: 'subtract' },
],
},
rules: 'required',
defaultValue: 'add',
},
{
component: 'InputNumber',
fieldName: 'amount',
label: '金额',
componentProps: {
precision: 2,
min: 0.01,
placeholder: '请输入金额',
style: { width: '100%' },
},
rules: 'required',
},
],
showDefaultActions: false,
});
// 提交前确认
async function handleSubmit() {
try {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
if (!values) return;
// 根据操作类型确定金额正负
const finalAmount = values.operation_type === 'subtract' ? -Math.abs(values.amount) : Math.abs(values.amount);
// 二次确认
Modal.confirm({
title: '确认修改余额',
content: `您确定要${values.operation_type === 'add' ? '增加' : '减少'} ¥${Math.abs(values.amount).toFixed(2)} 的余额吗?`,
onOk: async () => {
try {
await updateAgentWalletBalance({
agent_id: modalData.value?.agentId || 0,
amount: finalAmount,
});
message.success('修改余额成功');
modalApi.close();
emit('success');
} catch (error: any) {
message.error(error.message || '修改余额失败');
}
},
});
} catch (error) {
// 表单验证失败,不处理
}
}
// 重置表单
function handleReset() {
formApi.setValues({
operation_type: 'add',
amount: undefined,
});
}
</script>
<template>
<ModalComponent width="500px" :footer="false">
<div class="balance-modal">
<Form />
<div class="form-actions">
<a-button @click="handleReset">重置</a-button>
<a-button type="primary" @click="handleSubmit">确认修改</a-button>
</div>
</div>
</ModalComponent>
</template>
<style lang="less" scoped>
.balance-modal {
padding: 16px 0;
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import CommissionDeductionList from '../../agent-commission-deduction/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '上级抽佣记录',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-commission-deduction-modal">
<CommissionDeductionList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.agent-commission-deduction-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,323 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Card, Statistic, Row, Col, DatePicker, Button, Space } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentCommissionList } from '#/api/agent';
import type { AgentApi } from '#/api';
import dayjs, { Dayjs } from 'dayjs';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '历史佣金记录',
destroyOnClose: true,
footer: false,
});
const modalData = computed(() => modalApi.getData<ModalData>());
// 时间范围选择
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
// 统计数据
const statistics = ref({
totalAmount: 0,
settledAmount: 0,
frozenAmount: 0,
refundedAmount: 0,
totalCount: 0,
settledCount: 0,
frozenCount: 0,
refundedCount: 0,
});
// 快捷时间范围选择
function selectTimeRange(range: string) {
const now = dayjs();
let startDate: Dayjs;
switch (range) {
case '7d':
startDate = now.subtract(7, 'day');
break;
case '1m':
startDate = now.subtract(1, 'month');
break;
case '3m':
startDate = now.subtract(3, 'month');
break;
case '1y':
startDate = now.subtract(1, 'year');
break;
default:
dateRange.value = undefined;
return;
}
dateRange.value = [startDate, now];
}
// 重置时间范围
function resetTimeRange() {
dateRange.value = undefined;
}
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['create_time_start', 'create_time_end']]],
schema: [
{
component: 'InputNumber',
fieldName: 'order_id',
label: '订单ID',
componentProps: {
placeholder: '请输入订单ID',
style: { width: '100%' },
},
},
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
componentProps: {
placeholder: '请输入产品名称',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
placeholder: '请选择状态',
options: [
{ label: '已结算', value: 0 },
{ label: '冻结中', value: 1 },
{ label: '已退款', value: 2 },
],
},
},
],
submitOnChange: true,
},
gridOptions: {
columns: [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'order_id',
title: '订单ID',
width: 100,
},
{
field: 'amount',
title: '佣金金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'product_name',
title: '产品名称',
width: 180,
},
{
field: 'status',
title: '状态',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
0: '已结算',
1: '冻结中',
2: '已退款',
};
return statusMap[cellValue] || '未知';
},
className: ({ cellValue }: { cellValue: number }) => {
if (cellValue === 0) return 'text-green-600';
if (cellValue === 1) return 'text-orange-600';
if (cellValue === 2) return 'text-red-600';
return '';
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
],
height: 600,
maxHeight: 800,
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
layouts: ['Total', 'Sizes', 'PrevJump', 'Number', 'NextJump', 'FullJump'],
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
proxyConfig: {
ajax: {
query: async ({ page }: { page: { currentPage: number; pageSize: number } }, formValues: Record<string, any>) => {
const params: any = {
agent_id: modalData.value?.agentId,
...formValues,
page: page.currentPage,
pageSize: page.pageSize,
};
// 添加时间范围参数
if (dateRange.value && dateRange.value[0] && dateRange.value[1]) {
params.create_time_start = dateRange.value[0].format('YYYY-MM-DD HH:mm:ss');
params.create_time_end = dateRange.value[1].format('YYYY-MM-DD HH:mm:ss');
}
const result = await getAgentCommissionList(params);
// 更新统计数据
updateStatistics(result.items || []);
return result;
},
},
props: {
result: 'items',
total: 'total',
},
autoLoad: true,
},
},
});
// 更新统计数据
function updateStatistics(items: AgentApi.AgentCommissionListItem[]) {
statistics.value = {
totalCount: items.length,
totalAmount: items.reduce((sum, item) => sum + item.amount, 0),
settledCount: items.filter(item => item.status === 0).length,
settledAmount: items.filter(item => item.status === 0).reduce((sum, item) => sum + item.amount, 0),
frozenCount: items.filter(item => item.status === 1).length,
frozenAmount: items.filter(item => item.status === 1).reduce((sum, item) => sum + item.amount, 0),
refundedCount: items.filter(item => item.status === 2).length,
refundedAmount: items.filter(item => item.status === 2).reduce((sum, item) => sum + item.amount, 0),
};
}
// 监听时间范围变化,自动刷新表格
watch(dateRange, () => {
if (gridApi) {
gridApi.query();
}
});
</script>
<template>
<Modal class="w-[calc(100vw-200px)]">
<div class="commission-history-modal">
<!-- 时间范围选择 -->
<Card class="mb-4" title="时间范围选择">
<Space :size="12">
<span class="text-sm text-gray-600">快速选择</span>
<Button size="small" @click="selectTimeRange('7d')">近7日</Button>
<Button size="small" @click="selectTimeRange('1m')">近1月</Button>
<Button size="small" @click="selectTimeRange('3m')">近3月</Button>
<Button size="small" @click="selectTimeRange('1y')">近1年</Button>
<Button size="small" type="default" @click="resetTimeRange">全部</Button>
<span class="text-sm text-gray-400 ml-4">|</span>
<span class="text-sm text-gray-600">自定义</span>
<DatePicker.RangePicker
v-model:value="dateRange"
show-time
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
style="width: 380px"
/>
</Space>
</Card>
<!-- 统计卡片 -->
<Card class="mb-4" title="统计信息">
<Row :gutter="[16, 16]">
<Col :span="6">
<Statistic title="总记录数" :value="statistics.totalCount" />
</Col>
<Col :span="6">
<Statistic
title="总佣金金额"
:value="statistics.totalAmount"
:precision="2"
prefix="¥"
:value-style="{ color: '#1890ff' }"
/>
</Col>
<Col :span="6">
<Statistic
title="已结算金额"
:value="statistics.settledAmount"
:precision="2"
prefix="¥"
:value-style="{ color: '#52c41a' }"
/>
<div class="text-sm text-gray-500 mt-1">
{{ statistics.settledCount }} 条记录
</div>
</Col>
<Col :span="6">
<Statistic
title="冻结中金额"
:value="statistics.frozenAmount"
:precision="2"
prefix="¥"
:value-style="{ color: '#fa8c16' }"
/>
<div class="text-sm text-gray-500 mt-1">
{{ statistics.frozenCount }} 条记录
</div>
</Col>
</Row>
<Row :gutter="[16, 16]" class="mt-4">
<Col :span="12">
<Statistic
title="已退款金额"
:value="statistics.refundedAmount"
:precision="2"
prefix="¥"
:value-style="{ color: '#f5222d' }"
/>
<div class="text-sm text-gray-500 mt-1">
{{ statistics.refundedCount }} 条记录
</div>
</Col>
</Row>
</Card>
<!-- 佣金记录列表 -->
<Grid :table-title="`代理商 ID: ${modalData?.agentId} 的历史佣金记录`" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.commission-history-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import CommissionList from '../../agent-commission/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '佣金记录列表',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-commission-modal">
<CommissionList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.agent-commission-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,62 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AgentApi.AgentListItem>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
// TODO: 实现更新代理信息的接口
// updateAgent(id.value, values as AgentApi.UpdateAgentRequest)
// .then(() => {
// emit('success');
// drawerApi.close();
// })
// .catch(() => {
// drawerApi.unlock();
// });
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<AgentApi.AgentListItem>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
formApi.setValues(data);
} else {
id.value = undefined;
}
}
},
});
const getDrawerTitle = computed(() => {
return formData.value?.id ? '编辑代理' : '创建代理';
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form />
</Drawer>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import LinkList from '../../agent-links/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '推广链接列表',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-link-modal">
<LinkList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.agent-link-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import PlatformDeductionList from '../../agent-platform-deduction/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '平台抽佣记录',
width: 1000,
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal>
<PlatformDeductionList v-if="modalData" :agent-id="modalData.agentId" />
</Modal>
</template>
<style scoped>
:deep(.ant-modal-body) {
padding: 24px;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import RewardList from '../../agent-reward/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '奖励记录',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-reward-modal">
<RewardList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.agent-reward-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,202 @@
<script lang="ts" setup>
import { computed, h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getWalletTransactionList } from '#/api/agent';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '钱包流水记录',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
// 交易类型映射
const transactionTypeMap: Record<string, { label: string; color: string }> = {
commission: { label: '佣金收入', color: 'green' },
withdraw: { label: '提现', color: 'red' },
freeze: { label: '冻结', color: 'orange' },
unfreeze: { label: '解冻', color: 'blue' },
reward: { label: '奖励', color: 'green' },
refund: { label: '退款', color: 'purple' },
adjust: { label: '调整', color: 'cyan' },
};
// 获取交易类型标签
function getTransactionTypeTag(type: string) {
const config = transactionTypeMap[type] || { label: type, color: 'default' };
return h(Tag, { color: config.color }, config.label);
}
// 获取金额显示
function getAmountCellRender(params: any) {
const amount = params.row.amount;
const isPositive = amount >= 0;
const color = isPositive ? '#52c41a' : '#ff4d4f';
const sign = isPositive ? '+' : '';
return h('span', { style: { color, fontWeight: 'bold' } }, `${sign}${amount.toFixed(2)}`);
}
// 表格列配置
const columns = [
{
field: 'id',
title: '流水ID',
width: 80,
},
{
field: 'transaction_type',
title: '交易类型',
width: 120,
cellRender: {
name: 'CustomRender',
render: getTransactionTypeTag,
},
},
{
field: 'amount',
title: '变动金额',
width: 120,
cellRender: {
name: 'CustomRender',
render: getAmountCellRender,
},
},
{
field: 'balance_before',
title: '变动前余额',
width: 120,
formatter: ({ cellValue }: any) => `¥${Number(cellValue || 0).toFixed(2)}`,
},
{
field: 'balance_after',
title: '变动后余额',
width: 120,
formatter: ({ cellValue }: any) => `¥${Number(cellValue || 0).toFixed(2)}`,
},
{
field: 'frozen_balance_before',
title: '变动前冻结',
width: 120,
formatter: ({ cellValue }: any) => `¥${Number(cellValue || 0).toFixed(2)}`,
},
{
field: 'frozen_balance_after',
title: '变动后冻结',
width: 120,
formatter: ({ cellValue }: any) => `¥${Number(cellValue || 0).toFixed(2)}`,
},
{
field: 'transaction_id',
title: '关联交易ID',
width: 150,
formatter: ({ cellValue }: any) => cellValue || '-',
},
{
field: 'remark',
title: '备注',
width: 200,
formatter: ({ cellValue }: any) => cellValue || '-',
},
{
field: 'create_time',
title: '创建时间',
width: 180,
},
];
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: [
{
component: 'Input',
fieldName: 'transaction_type',
label: '交易类型',
help: '如: commission, withdraw, freeze, unfreeze, reward, refund, adjust',
},
{
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm:ss',
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
fieldName: 'create_time_start',
label: '开始时间',
},
{
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm:ss',
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
fieldName: 'create_time_end',
label: '结束时间',
},
],
submitOnChange: true,
},
gridOptions: {
columns,
height: 600,
maxHeight: 800,
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 20,
pageSizes: [10, 20, 50, 100],
layouts: ['Total', 'Sizes', 'PrevJump', 'Number', 'NextJump', 'FullJump'],
},
proxyConfig: {
ajax: {
query: async ({ page }: { page: { currentPage: number; pageSize: number } }, formValues: Record<string, any>) => {
return await getWalletTransactionList({
agent_id: modalData.value?.agentId || 0,
...formValues,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
autoLoad: true,
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
},
});
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="wallet-transaction-modal">
<Grid table-title="钱包流水记录" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.wallet-transaction-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import WithdrawalList from '../../agent-withdrawal/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '提现记录',
destroyOnClose: true,
});
const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-withdrawal-modal">
<WithdrawalList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style lang="less" scoped>
.agent-withdrawal-modal {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,254 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
// 会员等级选项
export const levelNameOptions = [
{ label: '普通会员', value: 'normal' },
{ label: 'VIP会员', value: 'VIP' },
{ label: 'SVIP会员', value: 'SVIP' },
];
// 代理会员配置列表列配置
export function useColumns(): VxeTableGridOptions['columns'] {
return [
{ field: 'id', title: 'ID', width: 80 },
{
field: 'level_name',
title: '会员等级',
formatter: ({ cellValue }) => {
const option = levelNameOptions.find(
(item) => item.value === cellValue,
);
return option?.label || cellValue;
},
},
{
field: 'price',
title: '会员年费',
formatter: ({ cellValue }) =>
cellValue !== null && cellValue !== undefined
? `¥${cellValue.toFixed(2)}`
: '-',
},
{
field: 'report_commission',
title: '直推报告收益',
formatter: ({ cellValue }) =>
cellValue !== null && cellValue !== undefined
? `¥${cellValue.toFixed(2)}`
: '-',
},
{
field: 'lower_activity_reward',
title: '下级活跃奖励',
formatter: ({ cellValue }) =>
cellValue !== null && cellValue !== undefined
? `¥${cellValue.toFixed(2)}`
: '-',
},
{
field: 'new_activity_reward',
title: '新增活跃奖励',
formatter: ({ cellValue }) =>
cellValue !== null && cellValue !== undefined
? `¥${cellValue.toFixed(2)}`
: '-',
},
{
field: 'lower_standard_count',
title: '活跃下级达标数',
formatter: ({ cellValue }) => cellValue ?? '-',
},
{
field: 'new_lower_standard_count',
title: '新增活跃下级达标数',
formatter: ({ cellValue }) => cellValue ?? '-',
},
{
field: 'lower_withdraw_reward_ratio',
title: '下级提现奖励比例',
formatter: ({ cellValue }) =>
cellValue !== null && cellValue !== undefined
? `${(cellValue * 100).toFixed(2)}%`
: '-',
},
{
field: 'create_time',
title: '创建时间',
width: 180,
},
{
align: 'center',
slots: { default: 'operation' },
field: 'operation',
fixed: 'right',
title: '操作',
width: 120,
},
] as const;
}
// 代理会员配置搜索表单配置
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Select',
fieldName: 'level_name',
label: '会员等级',
componentProps: {
placeholder: '请选择会员等级',
options: levelNameOptions,
},
},
];
}
// 代理会员配置编辑表单配置
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Select',
fieldName: 'level_name',
label: '会员等级',
rules: 'required',
componentProps: {
options: levelNameOptions,
},
},
{
component: 'InputNumber',
fieldName: 'price',
label: '会员年费',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'report_commission',
label: '直推报告收益',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'lower_activity_reward',
label: '下级活跃奖励',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'new_activity_reward',
label: '新增活跃奖励',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'lower_standard_count',
label: '活跃下级达标数',
componentProps: {
min: 0,
precision: 0,
},
},
{
component: 'InputNumber',
fieldName: 'new_lower_standard_count',
label: '新增活跃下级达标数',
componentProps: {
min: 0,
precision: 0,
},
},
{
component: 'InputNumber',
fieldName: 'lower_withdraw_reward_ratio',
label: '下级提现奖励比例',
componentProps: {
min: 0,
max: 100,
precision: 2,
step: 0.01,
addonAfter: '%',
},
},
{
component: 'InputNumber',
fieldName: 'lower_convert_vip_reward',
label: '下级转化VIP奖励',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'lower_convert_svip_reward',
label: '下级转化SVIP奖励',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'exemption_amount',
label: '免责金额',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'price_increase_max',
label: '提价最高金额',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'price_ratio',
label: '提价区间收取比例',
componentProps: {
min: 0,
max: 100,
precision: 2,
step: 0.01,
addonAfter: '%',
},
},
{
component: 'InputNumber',
fieldName: 'price_increase_amount',
label: '加价金额',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
];
}

View File

@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeGridListeners,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Button, Space } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentMembershipConfigList } from '#/api/agent';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
// 表单抽屉
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
submitOnChange: true,
},
gridEvents: {
sortChange: () => {
gridApi.query();
},
} as VxeGridListeners<AgentApi.AgentMembershipConfigListItem>,
gridOptions: {
columns: useColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await getAgentMembershipConfigList({
page: page.currentPage,
pageSize: page.pageSize,
level_name: formValues.level_name,
});
return res;
},
},
props: {
result: 'items',
total: 'total',
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<AgentApi.AgentMembershipConfigListItem>,
});
// 操作处理函数
function onActionClick(
e: OnActionClickParams<AgentApi.AgentMembershipConfigListItem>,
) {
switch (e.code) {
case 'edit': {
onEdit(e.row);
break;
}
}
}
// 编辑处理
function onEdit(row: AgentApi.AgentMembershipConfigListItem) {
formDrawerApi.setData(row).open();
}
// 刷新处理
function onRefresh() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid table-title="代理会员配置列表">
<template #operation="{ row }">
<Space>
<Button type="link" @click="onActionClick({ code: 'edit', row })">
配置
</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { ref } from 'vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { updateAgentMembershipConfig } from '#/api/agent';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AgentApi.AgentMembershipConfigListItem>();
const id = ref<number>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const drawerTitle = ref('会员配置');
const [Drawer, drawerApi] = useVbenDrawer({
title: drawerTitle.value,
destroyOnClose: true,
async onConfirm() {
const valid = await formApi.validate();
if (!valid || !id.value) return;
const values = await formApi.getValues();
const params: AgentApi.UpdateAgentMembershipConfigParams = {
id: id.value,
level_name: values.level_name,
price: values.price,
report_commission: values.report_commission,
lower_activity_reward: values.lower_activity_reward ?? null,
new_activity_reward: values.new_activity_reward ?? null,
lower_standard_count: values.lower_standard_count ?? null,
new_lower_standard_count: values.new_lower_standard_count ?? null,
lower_withdraw_reward_ratio:
values.lower_withdraw_reward_ratio !== null &&
values.lower_withdraw_reward_ratio !== undefined
? values.lower_withdraw_reward_ratio / 100
: null,
lower_convert_vip_reward: values.lower_convert_vip_reward ?? null,
lower_convert_svip_reward: values.lower_convert_svip_reward ?? null,
exemption_amount: values.exemption_amount ?? null,
price_increase_max: values.price_increase_max ?? null,
price_ratio:
values.price_ratio !== null && values.price_ratio !== undefined
? values.price_ratio / 100
: null,
price_increase_amount: values.price_increase_amount ?? null,
};
await updateAgentMembershipConfig(params);
emit('success');
drawerApi.close();
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<AgentApi.AgentMembershipConfigListItem>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
formApi.setValues({
...data,
lower_withdraw_reward_ratio: data.lower_withdraw_reward_ratio
? data.lower_withdraw_reward_ratio * 100
: null,
price_ratio: data.price_ratio ? data.price_ratio * 100 : null,
});
} else {
id.value = undefined;
}
}
},
});
</script>
<template>
<Drawer :title="drawerTitle">
<Form />
</Drawer>
</template>

View File

@@ -0,0 +1,134 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
// 支付方式选项
export const paymentMethodOptions = [
{ label: '支付宝', value: 'alipay' },
{ label: '微信', value: 'wechat' },
{ label: '苹果支付', value: 'appleiap' },
{ label: '其他', value: 'other' },
];
// 会员等级选项
export const levelNameOptions = [
{ label: '普通会员', value: '' },
{ label: 'VIP会员', value: 'VIP' },
{ label: 'SVIP会员', value: 'SVIP' },
];
// 状态选项
export const statusOptions = [
{ label: '待支付', value: 'pending' },
{ label: '支付成功', value: 'success' },
{ label: '支付失败', value: 'failed' },
{ label: '已取消', value: 'cancelled' },
];
// 列表列配置
export function useMembershipRechargeOrderColumns(): VxeTableGridOptions['columns'] {
return [
{ field: 'id', title: 'ID', width: 80 },
{ field: 'user_id', title: '用户ID', width: 100 },
{ field: 'agent_id', title: '代理ID', width: 100 },
{
field: 'level_name',
title: '会员等级',
width: 100,
formatter: ({ cellValue }) => {
const option = levelNameOptions.find(
(item) => item.value === cellValue,
);
return option?.label || '普通会员';
},
},
{
field: 'amount',
title: '金额',
width: 100,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
field: 'payment_method',
title: '支付方式',
width: 100,
formatter: ({ cellValue }) => {
const option = paymentMethodOptions.find(
(item) => item.value === cellValue,
);
return option?.label || cellValue;
},
},
{ field: 'order_no', title: '订单号', width: 180 },
{ field: 'platform_order_id', title: '平台订单号', width: 180 },
{
field: 'status',
title: '状态',
width: 100,
formatter: ({ cellValue }) => {
const option = statusOptions.find((item) => item.value === cellValue);
return option?.label || cellValue;
},
},
{
field: 'create_time',
title: '创建时间',
width: 180,
},
];
}
// 搜索表单配置
export function useMembershipRechargeOrderFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'user_id',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
},
},
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '代理ID',
componentProps: {
placeholder: '请输入代理ID',
},
},
{
component: 'Input',
fieldName: 'order_no',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
},
},
{
component: 'Input',
fieldName: 'platform_order_id',
label: '平台订单号',
componentProps: {
placeholder: '请输入平台订单号',
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
placeholder: '请选择状态',
options: statusOptions,
},
},
{
component: 'Select',
fieldName: 'payment_method',
label: '支付方式',
componentProps: {
placeholder: '请选择支付方式',
options: paymentMethodOptions,
},
},
];
}

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent/agent';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getMembershipRechargeOrderList } from '#/api/agent';
import {
useMembershipRechargeOrderColumns,
useMembershipRechargeOrderFormSchema,
} from './data';
const [Grid, _gridApi] = useVbenVxeGrid({
formOptions: {
schema: useMembershipRechargeOrderFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useMembershipRechargeOrderColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async (
{ page }: { page: { currentPage: number; pageSize: number } },
formValues: Record<string, any>,
) => {
const res = await getMembershipRechargeOrderList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
return { items: res.items, total: res.total };
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeGridProps<AgentApi.MembershipRechargeOrderListItem>,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="会员充值订单列表" />
</Page>
</template>

View File

@@ -0,0 +1,96 @@
import type { VbenFormSchema } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent';
// 平台抽佣列表列配置
export function usePlatformDeductionColumns(): VxeTableGridOptions['columns'] {
return [
{
title: 'ID',
field: 'id',
width: 80,
},
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '抽佣金额',
field: 'amount',
width: 120,
formatter: ({ cellValue }) => {
return `¥${cellValue.toFixed(2)}`;
},
},
{
title: '抽佣类型',
field: 'type',
width: 120,
formatter: ({
cellValue,
}: {
cellValue: AgentApi.AgentPlatformDeductionListItem['type'];
}) => {
const typeMap = {
cost: '成本抽佣',
pricing: '定价抽佣',
};
return typeMap[cellValue] || cellValue;
},
},
{
title: '状态',
field: 'status',
width: 100,
formatter: ({ cellValue }) => {
const statusMap = {
0: { text: '待处理', type: 'warning' },
1: { text: '已处理', type: 'success' },
2: { text: '已取消', type: 'error' },
};
const status = statusMap[cellValue as keyof typeof statusMap];
return status
? `<a-tag color="${status.type}">${status.text}</a-tag>`
: cellValue;
},
},
{
title: '创建时间',
field: 'create_time',
width: 180,
},
];
}
// 平台抽佣列表搜索表单配置
export function usePlatformDeductionFormSchema(): VbenFormSchema[] {
return [
{
component: 'Select',
fieldName: 'type',
label: '抽佣类型',
componentProps: {
options: [
{ label: '成本抽佣', value: 'cost' },
{ label: '定价抽佣', value: 'pricing' },
],
allowClear: true,
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
options: [
{ label: '待处理', value: 0 },
{ label: '已处理', value: 1 },
{ label: '已取消', value: 2 },
],
allowClear: true,
},
},
];
}

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentPlatformDeductionList } from '#/api/agent';
import {
usePlatformDeductionColumns,
usePlatformDeductionFormSchema,
} from './data';
interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: usePlatformDeductionFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: usePlatformDeductionColumns(),
proxyConfig: {
ajax: {
query: async ({
form,
page,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentPlatformDeductionList({
...queryParams.value,
...form,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '平台抽佣列表' : '所有平台抽佣记录'" />
</Page>
</template>

View File

@@ -0,0 +1,131 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
// 代理产品配置列表列配置
export function useColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'product_name',
title: '产品名称',
},
{
field: 'cost_price',
title: '成本',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'price_range_min',
title: '最低定价',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'price_range_max',
title: '最高定价',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'pricing_standard',
title: '定价标准',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'overpricing_ratio',
title: '超标抽佣比例',
formatter: ({ cellValue }: { cellValue: number }) =>
`${(cellValue * 100).toFixed(2)}%`,
},
{
align: 'center',
slots: { default: 'operation' },
field: 'operation',
fixed: 'right',
title: '操作',
width: 120,
},
] as const;
}
// 代理产品配置搜索表单配置
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'product_name',
label: '产品名称',
},
];
}
// 代理产品配置编辑表单配置
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'cost_price',
label: '成本',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'price_range_min',
label: '最低定价',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'price_range_max',
label: '最高定价',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'pricing_standard',
label: '定价标准',
rules: 'required',
componentProps: {
min: 0,
precision: 2,
step: 0.01,
},
},
{
component: 'InputNumber',
fieldName: 'overpricing_ratio',
label: '超标抽佣比例',
rules: 'required',
componentProps: {
min: 0,
max: 100,
precision: 2,
step: 0.01,
addonAfter: '%',
controls: true,
validateTrigger: ['blur', 'change'],
},
},
];
}

View File

@@ -0,0 +1,127 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeGridListeners,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Button, Space } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentProductionConfigList } from '#/api/agent';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
// 表单抽屉
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
submitOnChange: true,
},
gridEvents: {
sortChange: () => {
gridApi.query();
},
} as VxeGridListeners<AgentApi.AgentProductionConfigItem>,
gridOptions: {
columns: useColumns(),
height: 'auto',
keepSource: true,
sortConfig: {
remote: true,
multiple: false,
trigger: 'default',
orders: ['asc', 'desc', null],
resetPage: true,
},
proxyConfig: {
ajax: {
query: async ({ page, sort }, formValues) => {
const sortParams = sort
? {
order_by: sort.field,
order_type: sort.order,
}
: {};
const params: AgentApi.GetAgentProductionConfigListParams = {
page: page.currentPage,
pageSize: page.pageSize,
product_name: formValues.product_name,
...sortParams,
};
const res = await getAgentProductionConfigList(params);
return {
...res,
sort: sort || null,
};
},
},
props: {
result: 'items',
total: 'total',
},
autoLoad: true,
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<AgentApi.AgentProductionConfigItem>,
});
// 操作处理函数
function onActionClick(
e: OnActionClickParams<AgentApi.AgentProductionConfigItem>,
) {
switch (e.code) {
case 'edit': {
onEdit(e.row);
break;
}
}
}
// 编辑处理
function onEdit(row: AgentApi.AgentProductionConfigItem) {
formDrawerApi.setData(row).open();
}
// 刷新处理
function onRefresh() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid table-title="代理产品配置列表">
<template #operation="{ row }">
<Space>
<Button type="link" @click="onActionClick({ code: 'edit', row })">
配置
</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,68 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { ref } from 'vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { updateAgentProductionConfig } from '#/api/agent';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<AgentApi.AgentProductionConfigItem>();
const id = ref<number>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const drawerTitle = ref('产品配置');
const [Drawer, drawerApi] = useVbenDrawer({
title: drawerTitle.value,
destroyOnClose: true,
async onConfirm() {
const valid = await formApi.validate();
if (!valid || !id.value) return;
const values = await formApi.getValues();
const params: AgentApi.UpdateAgentProductionConfigParams = {
id: id.value,
cost_price: values.cost_price,
price_range_min: values.price_range_min,
price_range_max: values.price_range_max,
pricing_standard: values.pricing_standard,
overpricing_ratio: values.overpricing_ratio / 100,
};
await updateAgentProductionConfig(params);
emit('success');
drawerApi.close();
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<AgentApi.AgentProductionConfigItem>();
formApi.resetForm();
if (data) {
formData.value = data;
id.value = data.id;
formApi.setValues({
...data,
overpricing_ratio: data.overpricing_ratio * 100,
});
} else {
id.value = undefined;
}
}
},
});
</script>
<template>
<Drawer :title="drawerTitle">
<Form />
</Drawer>
</template>

View File

@@ -0,0 +1,74 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useRewardColumns(): VxeTableGridOptions['columns'] {
return [
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '奖励类型',
field: 'type',
width: 120,
},
{
title: '奖励金额',
field: 'amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '关联订单',
field: 'order_id',
width: 120,
},
{
title: '状态',
field: 'status',
width: 100,
},
{
title: '创建时间',
field: 'create_time',
width: 180,
},
{
title: '发放时间',
field: 'pay_time',
width: 180,
},
];
}
export function useRewardFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'type',
label: '奖励类型',
component: 'Select',
componentProps: {
options: [
{ label: '注册奖励', value: 'register' },
{ label: '首单奖励', value: 'first_order' },
{ label: '升级奖励', value: 'level_up' },
],
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '待发放', value: 'pending' },
{ label: '已发放', value: 'paid' },
{ label: '发放失败', value: 'failed' },
],
allowClear: true,
},
},
];
}

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentRewardList } from '#/api/agent';
import { useRewardColumns, useRewardFormSchema } from './data';
interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useRewardFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useRewardColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentRewardList({
...queryParams.value,
...form,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '奖励记录列表' : '所有奖励记录'" />
</Page>
</template>

View File

@@ -0,0 +1,161 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
return [
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '提现金额',
field: 'amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue?.toFixed(2) || '0.00'}`,
},
{
title: '扣税金额',
field: 'tax_amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue?.toFixed(2) || '0.00'}`,
},
{
title: '实际转账金额',
field: 'actual_amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue?.toFixed(2) || '0.00'}`,
},
{
title: '提现类型',
field: 'withdraw_type',
width: 100,
formatter: ({ cellValue }) => {
const typeMap: Record<number, string> = {
1: '支付宝',
2: '银行卡',
};
return typeMap[cellValue] || '未知';
},
},
{
title: '收款信息',
field: 'payee_info',
width: 200,
formatter: ({ row }) => {
if (row.withdraw_type === 1) {
// 支付宝提现
return row.payee_account || '-';
} else if (row.withdraw_type === 2) {
// 银行卡提现
if (row.bank_card_no) {
const cardNo = row.bank_card_no.replaceAll(/\s/g, '');
const masked =
cardNo.length > 8
? `${cardNo.slice(0, 4)} **** **** ${cardNo.slice(
Math.max(0, cardNo.length - 4),
)}`
: cardNo;
return `${masked}${row.bank_name ? ` (${row.bank_name})` : ''}`;
}
return '-';
}
return '-';
},
},
{
title: '收款人',
field: 'payee_name',
width: 120,
formatter: ({ cellValue, row }) => {
if (row.withdraw_type === 2 && cellValue) {
return cellValue;
}
return '-';
},
},
{
title: '状态',
field: 'status',
width: 100,
formatter: ({ cellValue }) => {
const statusMap: Record<number, string> = {
1: '申请中',
2: '成功',
3: '失败',
};
return statusMap[cellValue] || '未知';
},
cellRender: {
name: 'VxeTag',
props: ({ row }) => {
const statusConfig: Record<
number,
{ content: string; type: string }
> = {
1: { type: 'warning', content: '申请中' },
2: { type: 'success', content: '成功' },
3: { type: 'error', content: '失败' },
};
const config = statusConfig[row.status] || {
type: 'info',
content: '未知',
};
return {
type: config.type,
content: config.content,
};
},
},
},
{
title: '申请时间',
field: 'create_time',
width: 180,
},
{
title: '备注',
field: 'remark',
width: 200,
formatter: ({ cellValue }) => cellValue || '-',
},
{
align: 'center',
slots: { default: 'operation' },
field: 'operation',
fixed: 'right',
title: '操作',
width: 120,
},
];
}
export function useWithdrawalFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'withdraw_type',
label: '提现类型',
component: 'Select',
componentProps: {
options: [
{ label: '支付宝', value: 1 },
{ label: '银行卡', value: 2 },
],
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '申请中', value: 1 },
{ label: '成功', value: 2 },
{ label: '失败', value: 3 },
],
allowClear: true,
},
},
];
}

View File

@@ -0,0 +1,105 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Button, Space } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentWithdrawalList } from '#/api/agent';
import ReviewModal from './modules/review-modal.vue';
import { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
interface Props {
agentId?: number;
}
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const props = defineProps<Props>();
const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
// 审核弹窗
const [ReviewDrawer, reviewDrawerApi] = useVbenDrawer({
connectedComponent: ReviewModal,
destroyOnClose: true,
});
// 操作处理
function onActionClick(e: any) {
const { code, row } = e;
switch (code) {
case 'review':
// 打开审核弹窗
reviewDrawerApi.setData(row).open();
break;
}
}
// 刷新列表
function onRefresh() {
gridApi.query();
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useWithdrawalFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useWithdrawalColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
}: {
page: QueryParams;
}) => {
return await getAgentWithdrawalList({
...queryParams.value,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
},
});
</script>
<template>
<Page :auto-content-height="!agentId">
<ReviewDrawer @success="onRefresh" />
<Grid :table-title="agentId ? '提现记录列表' : '所有提现记录'">
<template #operation="{ row }">
<Space>
<Button
v-if="row.status === 1"
type="link"
@click="onActionClick({ code: 'review', row })"
>
审核
</Button>
</Space>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,249 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { reviewBankCardWithdrawal } from '#/api/agent';
import type { VbenFormSchema } from '#/adapter/form';
const emit = defineEmits<{
(e: 'success'): void;
}>();
const formData = ref<any>(null);
// 表单配置(包含所有字段)
const getFormSchema = (): VbenFormSchema[] => [
{
component: 'Input',
fieldName: 'withdraw_no',
label: '提现单号',
componentProps: {
disabled: true,
},
},
{
component: 'Input',
fieldName: 'withdraw_type',
label: '提现类型',
componentProps: {
disabled: true,
},
},
{
component: 'InputNumber',
fieldName: 'amount',
label: '提现金额',
componentProps: {
disabled: true,
min: 0,
precision: 2,
style: { width: '100%' },
addonBefore: '¥',
},
},
// 银行卡和支付宝通用字段:扣税金额
{
component: 'InputNumber' ,
fieldName: 'tax_amount',
label: '扣税金额',
componentProps: {
disabled: true,
min: 0,
precision: 2,
style: { width: '100%' },
addonBefore: '¥',
},
},
// 银行卡和支付宝通用字段:实际转账金额
{
component: 'InputNumber',
fieldName: 'actual_amount',
label: '实际转账金额',
componentProps: {
disabled: true,
min: 0,
precision: 2,
style: { width: '100%' },
addonBefore: '¥',
},
},
// 银行卡和支付宝都有:收款账号
{
component: 'Input',
fieldName: 'payee_account',
label: '收款账号',
componentProps: {
disabled: true,
},
},
// 银行卡提现特有字段:开户支行
{
component: 'Input',
fieldName: 'bank_name',
label: '开户支行',
componentProps: {
disabled: true,
},
dependencies: {
show: (values) => values.withdraw_type === '银行卡',
triggerFields: ['withdraw_type'],
},
},
// 银行卡提现特有字段:收款人姓名
{
component: 'Input',
fieldName: 'payee_name',
label: '收款人姓名',
componentProps: {
disabled: true,
},
dependencies: {
show: (values) => values.withdraw_type === '银行卡',
triggerFields: ['withdraw_type'],
},
},
{
component: 'RadioGroup',
fieldName: 'action',
label: '审核操作',
defaultValue: 1,
componentProps: {
options: [
{ label: '确认提现', value: 1 },
{ label: '拒绝提现', value: 2 },
],
},
rules: 'required',
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
componentProps: {
rows: 4,
placeholder: '拒绝提现时,请填写拒绝原因',
maxLength: 200,
showCount: true,
style: { width: '100%' },
},
},
];
const [Form, formApi] = useVbenForm({
schema: getFormSchema(),
showDefaultActions: false,
wrapperClass: 'agent-withdrawal-form-wrapper',
});
const [Drawer, drawerApi] = useVbenDrawer({
class: 'agent-withdrawal-review-drawer',
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
if (!formData.value?.id) {
message.error('提现记录ID不存在');
return;
}
const values = await formApi.getValues<{
action: 1 | 2;
remark: string;
}>();
// 验证拒绝时必须填写原因
if (values.action === 2 && !values.remark?.trim()) {
message.error('拒绝提现必须填写拒绝原因');
return;
}
try {
await reviewBankCardWithdrawal({
action: values.action,
remark: values.remark || '',
withdrawal_id: formData.value.id,
});
message.success(values.action === 1 ? '确认提现成功' : '拒绝提现成功');
drawerApi.close();
emit('success');
} catch (error: any) {
message.error(error?.message || '操作失败');
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<any>();
formApi.resetForm();
if (data) {
formData.value = data;
// 根据提现类型设置不同的表单初始值
const typeMap: Record<number, string> = {
1: '支付宝',
2: '银行卡',
};
const initialValues: any = {
withdraw_no: data.withdraw_no || '',
withdraw_type: typeMap[data.withdraw_type] || '未知',
amount: data.amount || 0,
action: 1, // 默认选择确认
remark: '',
};
// 银行卡提现特有字段
if (data.withdraw_type === 2) {
initialValues.tax_amount = data.tax_amount || 0;
initialValues.actual_amount = data.actual_amount || 0;
initialValues.payee_account = data.bank_card_no || '';
initialValues.bank_name = data.bank_name || '';
initialValues.payee_name = data.payee_name || '';
} else {
// 支付宝提现
initialValues.tax_amount = data.tax_amount || 0;
initialValues.actual_amount = data.actual_amount || 0;
initialValues.payee_account = data.payee_account || '';
}
formApi.setValues(initialValues);
}
}
},
});
const getDrawerTitle = computed(() => {
const typeMap: Record<number, string> = {
1: '支付宝',
2: '银行卡',
};
const typeName = typeMap[formData.value?.withdraw_type] || '未知';
return `${typeName}提现审核 ${formData.value?.withdraw_no || ''}`;
});
</script>
<template>
<Drawer :title="getDrawerTitle" :width="800">
<div class="agent-withdrawal-review-content">
<Form />
</div>
</Drawer>
</template>
<style scoped>
.agent-withdrawal-review-content {
padding: 0 4px;
max-height: calc(100vh - 120px);
overflow-y: auto;
}
.agent-withdrawal-review-content :deep(.ant-form-item) {
margin-bottom: 16px;
}
.agent-withdrawal-review-content :deep(.ant-form-item-label) {
padding-bottom: 4px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More