This commit is contained in:
2026-02-08 17:01:33 +08:00
parent 0668eea99b
commit 3211cc32ce
76 changed files with 5054 additions and 2423 deletions

View File

@@ -4,19 +4,19 @@ export namespace AgentApi {
export interface AgentListItem {
id: number;
user_id: number;
agent_code: number;
level: number; // 1=普通2=黄金3=钻石
level_name: string;
region: string;
mobile: string;
membership_expiry_time: string;
wechat_id?: string;
team_leader_id?: number;
balance: number;
total_earnings: number;
frozen_balance: number;
withdrawn_amount: number;
is_real_name: boolean;
create_time: string;
is_real_name_verified: boolean;
real_name: string;
id_card: string;
real_name_status: 'approved' | 'pending' | 'rejected';
}
export interface AgentList {
@@ -29,7 +29,8 @@ export namespace AgentApi {
pageSize: number;
mobile?: string;
region?: string;
parent_agent_id?: number;
level?: number;
team_leader_id?: number;
id?: number;
create_time_start?: string;
create_time_end?: string;
@@ -39,8 +40,10 @@ export namespace AgentApi {
export interface AgentLinkListItem {
agent_id: number;
product_id: number;
product_name: string;
price: number;
set_price: number;
actual_base_price: number;
link_identifier: string;
create_time: string;
}
@@ -54,6 +57,7 @@ export namespace AgentApi {
page: number;
pageSize: number;
agent_id?: number;
product_id?: number;
product_name?: string;
link_identifier?: string;
}
@@ -82,27 +86,28 @@ export namespace AgentApi {
status?: number;
}
// 代理奖励相关接口
export interface AgentRewardListItem {
// 代理返佣相关接口
export interface AgentRebateListItem {
id: number;
agent_id: number;
relation_agent_id: number;
source_agent_id: number;
order_id: number;
rebate_type: number; // 1=直接上级返佣2=钻石上级返佣3=黄金上级返佣
amount: number;
type: string;
create_time: string;
}
export interface AgentRewardList {
export interface AgentRebateList {
total: number;
items: AgentRewardListItem[];
items: AgentRebateListItem[];
}
export interface GetAgentRewardListParams {
export interface GetAgentRebateListParams {
page: number;
pageSize: number;
agent_id?: number;
relation_agent_id?: number;
type?: string;
source_agent_id?: number;
rebate_type?: number;
}
// 代理提现相关接口
@@ -111,8 +116,11 @@ export namespace AgentApi {
agent_id: number;
withdraw_no: string;
amount: number;
status: number;
tax_amount: number;
actual_amount: number;
status: number; // 1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败
payee_account: string;
payee_name: string;
remark: string;
create_time: string;
}
@@ -130,66 +138,22 @@ export namespace AgentApi {
withdraw_no?: string;
}
// 代理上级抽佣相关接口
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 AuditWithdrawalParams {
withdrawal_id: number;
status: number; // 2=通过3=拒绝
remark: string;
}
// 代理产品配置列表项
export interface AgentProductionConfigItem {
id: number;
product_id: number;
product_name: string;
cost_price: number;
base_price: number;
price_range_min: number;
price_range_max: number;
pricing_standard: number;
overpricing_ratio: number;
price_threshold: number;
price_fee_rate: number;
create_time: string;
}
@@ -204,17 +168,17 @@ export namespace AgentApi {
page: number;
pageSize: number;
product_name?: string;
product_id?: number;
id?: number;
}
// 更新代理产品配置参数
export interface UpdateAgentProductionConfigParams {
id: number;
cost_price: number;
price_range_min: number;
base_price: number;
price_range_max: number;
pricing_standard: number;
overpricing_ratio: number;
price_threshold?: number;
price_fee_rate?: number;
}
// 更新代理产品配置响应
@@ -222,76 +186,213 @@ export namespace AgentApi {
success: boolean;
}
export interface MembershipRechargeOrderListItem {
// 代理升级记录相关接口
export interface AgentUpgradeListItem {
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';
from_level: number;
to_level: number;
upgrade_type: number; // 1=自主付费2=钻石升级下级
upgrade_fee: number;
rebate_amount: number;
status: number; // 1=待处理2=已完成3=已失败
create_time: string;
}
export interface GetMembershipRechargeOrderListParams {
page: number;
pageSize: number;
user_id?: number;
agent_id?: number;
level_name?: string;
status?: string;
}
export interface MembershipRechargeOrderList {
export interface AgentUpgradeList {
total: number;
items: MembershipRechargeOrderListItem[];
items: AgentUpgradeListItem[];
}
// 代理会员配置相关接口
export interface AgentMembershipConfigListItem {
export interface GetAgentUpgradeListParams {
page: number;
pageSize: number;
agent_id?: number;
upgrade_type?: number;
status?: number;
}
// 代理订单相关接口
export interface AgentOrderListItem {
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;
agent_id: number;
order_id: number;
product_id: number;
product_name: string;
order_amount: number;
set_price: number;
actual_base_price: number;
price_cost: number;
agent_profit: number;
process_status: number; // 0=待处理1=处理成功2=处理失败
create_time: string;
}
export interface GetAgentMembershipConfigListParams {
page: number;
pageSize: number;
level_name?: string;
export interface AgentOrderList {
total: number;
items: AgentOrderListItem[];
}
// 代理会员配置编辑请求参数
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 GetAgentOrderListParams {
page: number;
pageSize: number;
agent_id?: number;
order_id?: number;
process_status?: number;
}
// 邀请码管理相关接口
export interface InviteCodeListItem {
id: number;
code: string;
agent_id: number; // 0表示平台发放
agent_mobile: string;
target_level: number;
status: number; // 0=未使用1=已使用2=已失效
used_user_id?: number;
used_agent_id?: number;
used_time?: string;
expire_time?: string;
remark?: string;
create_time: string;
}
export interface InviteCodeList {
total: number;
items: InviteCodeListItem[];
}
export interface GetInviteCodeListParams {
page: number;
pageSize: number;
code?: string;
agent_id?: number;
target_level?: number;
status?: number;
}
export interface GenerateDiamondInviteCodeParams {
count: number;
expire_days?: number; // 可选0表示不过期
remark?: string;
}
export interface GenerateDiamondInviteCodeResp {
codes: string[];
}
// 系统配置相关接口(价格配置已移除,改为产品配置表管理)
export interface AgentConfig {
level_bonus: {
diamond: number;
gold: number;
normal: number;
};
upgrade_fee: {
normal_to_gold: number;
normal_to_diamond: number;
gold_to_diamond: number;
};
upgrade_rebate: {
normal_to_gold_rebate: number;
to_diamond_rebate: number;
};
direct_parent_rebate: {
diamond: number; // 直接上级是钻石的返佣金额6元
gold: number; // 直接上级是黄金的返佣金额3元
normal: number; // 直接上级是普通的返佣金额2元
};
max_gold_rebate_amount: number; // 黄金代理最大返佣金额3元
commission_freeze: {
ratio: number; // 佣金冻结比例例如0.1表示10%
threshold: number; // 佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)
days: number; // 佣金冻结解冻天数单位例如30表示30天后解冻
};
tax_rate: number;
tax_exemption_amount: number;
gold_max_uplift_amount: number;
diamond_max_uplift_amount: number;
}
export interface UpdateAgentConfigParams {
level_bonus?: {
diamond?: number;
gold?: number;
normal?: number;
};
upgrade_fee?: {
normal_to_gold?: number;
normal_to_diamond?: number;
gold_to_diamond?: number;
};
upgrade_rebate?: {
normal_to_gold_rebate?: number;
to_diamond_rebate?: number;
};
direct_parent_rebate?: {
diamond?: number; // 直接上级是钻石的返佣金额6元
gold?: number; // 直接上级是黄金的返佣金额3元
normal?: number; // 直接上级是普通的返佣金额2元
};
max_gold_rebate_amount?: number; // 黄金代理最大返佣金额3元
commission_freeze?: {
ratio?: number; // 佣金冻结比例例如0.1表示10%
threshold?: number; // 佣金冻结阈值(订单单价达到此金额才触发冻结,单位:元)
days?: number; // 佣金冻结解冻天数单位例如30表示30天后解冻
};
tax_rate?: number;
tax_exemption_amount?: number;
gold_max_uplift_amount?: number;
diamond_max_uplift_amount?: number;
}
// 实名认证相关接口
export interface AgentRealNameListItem {
id: number;
agent_id: number;
name: string;
id_card: string; // 加密,需要脱敏显示
mobile: string; // 加密
status: number; // 1=未验证2=已通过
verify_time?: string;
create_time: string;
}
export interface AgentRealNameList {
total: number;
items: AgentRealNameListItem[];
}
export interface GetAgentRealNameListParams {
page: number;
pageSize: number;
agent_id?: number;
status?: number;
}
// 代理奖励相关接口
export interface AgentRewardListItem {
id: number;
agent_id: number;
type: string; // 奖励类型register=注册奖励first_order=首单奖励level_up=升级奖励
amount: number;
order_id?: number;
status: string; // pending=待发放paid=已发放failed=发放失败
create_time: string;
pay_time?: string;
}
export interface AgentRewardList {
total: number;
items: AgentRewardListItem[];
}
export interface GetAgentRewardListParams {
page: number;
pageSize: number;
agent_id?: number;
type?: string;
status?: string;
}
}
@@ -309,7 +410,7 @@ async function getAgentList(params: AgentApi.GetAgentListParams) {
* 获取代理推广链接列表
*/
async function getAgentLinkList(params: AgentApi.GetAgentLinkListParams) {
return requestClient.get<AgentApi.AgentLinkList>('/agent/agent-link/list', {
return requestClient.get<AgentApi.AgentLinkList>('/agent/link/list', {
params,
});
}
@@ -321,19 +422,7 @@ async function getAgentCommissionList(
params: AgentApi.GetAgentCommissionListParams,
) {
return requestClient.get<AgentApi.AgentCommissionList>(
'/agent/agent-commission/list',
{
params,
},
);
}
/**
* 获取代理奖励列表
*/
async function getAgentRewardList(params: AgentApi.GetAgentRewardListParams) {
return requestClient.get<AgentApi.AgentRewardList>(
'/agent/agent-reward/list',
'/agent/commission/list',
{
params,
},
@@ -347,35 +436,7 @@ 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',
'/agent/withdrawal/list',
{
params,
},
@@ -389,7 +450,7 @@ async function getAgentProductionConfigList(
params: AgentApi.GetAgentProductionConfigListParams,
) {
return requestClient.get<AgentApi.AgentProductionConfigList>(
'/agent/agent-production-config/list',
'/agent/product_config/list',
{
params,
},
@@ -403,19 +464,46 @@ async function updateAgentProductionConfig(
params: AgentApi.UpdateAgentProductionConfigParams,
) {
return requestClient.post<AgentApi.UpdateAgentProductionConfigResp>(
'/agent/agent-production-config/update',
'/agent/product_config/update',
params,
);
}
/**
* 获取会员充值订单列表
* 获取代理返佣记录列表
*/
async function getMembershipRechargeOrderList(
params: AgentApi.GetMembershipRechargeOrderListParams,
async function getAgentRebateList(params: AgentApi.GetAgentRebateListParams) {
return requestClient.get<AgentApi.AgentRebateList>('/agent/rebate/list', {
params,
});
}
/**
* 获取代理升级记录列表
*/
async function getAgentUpgradeList(
params: AgentApi.GetAgentUpgradeListParams,
) {
return requestClient.get<AgentApi.MembershipRechargeOrderList>(
'/agent/agent-membership-recharge-order/list',
return requestClient.get<AgentApi.AgentUpgradeList>('/agent/upgrade/list', {
params,
});
}
/**
* 获取代理订单列表
*/
async function getAgentOrderList(params: AgentApi.GetAgentOrderListParams) {
return requestClient.get<AgentApi.AgentOrderList>('/agent/order/list', {
params,
});
}
/**
* 获取邀请码列表
*/
async function getInviteCodeList(params: AgentApi.GetInviteCodeListParams) {
return requestClient.get<AgentApi.InviteCodeList>(
'/agent/invite_code/list',
{
params,
},
@@ -423,40 +511,84 @@ async function getMembershipRechargeOrderList(
}
/**
* 获取代理会员配置列表
* 生成钻石邀请码
*/
async function getAgentMembershipConfigList(
params: AgentApi.GetAgentMembershipConfigListParams,
async function generateDiamondInviteCode(
params: AgentApi.GenerateDiamondInviteCodeParams,
) {
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',
return requestClient.post<AgentApi.GenerateDiamondInviteCodeResp>(
'/agent/invite_code/diamond/generate',
params,
);
}
/**
* 获取系统配置
*/
async function getAgentConfig() {
return requestClient.get<AgentApi.AgentConfig>('/agent/config');
}
/**
* 更新系统配置
*/
async function updateAgentConfig(params: AgentApi.UpdateAgentConfigParams) {
return requestClient.post<{ success: boolean }>(
'/agent/config/update',
params,
);
}
/**
* 获取实名认证列表
*/
async function getAgentRealNameList(
params: AgentApi.GetAgentRealNameListParams,
) {
return requestClient.get<AgentApi.AgentRealNameList>(
'/agent/real_name/list',
{
params,
},
);
}
/**
* 审核提现
*/
async function auditWithdrawal(params: AgentApi.AuditWithdrawalParams) {
return requestClient.post<{ success: boolean }>(
'/agent/withdrawal/audit',
params,
);
}
/**
* 获取代理奖励列表
*/
async function getAgentRewardList(
params: AgentApi.GetAgentRewardListParams,
) {
return requestClient.get<AgentApi.AgentRewardList>('/agent/reward/list', {
params,
});
}
export {
getAgentCommissionDeductionList,
auditWithdrawal,
generateDiamondInviteCode,
getAgentCommissionList,
getAgentConfig,
getAgentLinkList,
getAgentList,
getAgentMembershipConfigList,
getAgentPlatformDeductionList,
getAgentOrderList,
getAgentProductionConfigList,
getAgentRebateList,
getAgentRealNameList,
getAgentRewardList,
getAgentUpgradeList,
getAgentWithdrawalList,
getMembershipRechargeOrderList,
updateAgentMembershipConfig,
getInviteCodeList,
updateAgentConfig,
updateAgentProductionConfig,
};

View File

@@ -0,0 +1,160 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace ComplaintApi {
export interface Complaint {
id: string;
type: 'alipay' | 'manual';
order_id: string;
name: string;
contact: string;
content: string;
status: 'pending' | 'processing' | 'resolved' | 'closed';
status_description: string;
remark: string;
handler_id: string;
handle_time: string;
create_time: string;
update_time: string;
// 支付宝投诉特有字段
task_id?: string;
trade_no?: string;
complain_amount?: string;
gmt_complain?: string;
// 主动投诉特有字段
subject?: string;
priority?: string;
source?: string;
}
export interface ComplaintList {
total: number;
items: Complaint[];
}
export interface ComplaintDetail {
id: string;
type: 'alipay' | 'manual';
order_id: string;
name: string;
contact: string;
content: string;
status: string;
status_description: string;
remark: string;
handler_id: string;
handle_time: string;
create_time: string;
update_time: string;
alipay_complaint?: AlipayComplaintDetail;
manual_complaint?: ManualComplaintDetail;
}
export interface AlipayComplaintDetail {
id: string;
alipay_id: number;
task_id: string;
opposite_pid: string;
opposite_name: string;
complain_amount: string;
gmt_complain: string;
gmt_process: string;
complain_content: string;
trade_no: string;
status: string;
status_description: string;
process_code: string;
process_message: string;
process_remark: string;
process_img_url_list: string[];
gmt_risk_finish_time: string;
complain_url: string;
certify_info: string[];
trade_info_list: AlipayComplaintTradeInfo[];
}
export interface AlipayComplaintTradeInfo {
id: string;
alipay_trade_id: string;
alipay_complaint_record_id: string;
trade_no: string;
out_no: string;
gmt_trade: string;
gmt_refund: string;
status: string;
status_description: string;
amount: string;
}
export interface ManualComplaintDetail {
id: string;
user_id: string;
subject: string;
priority: string;
source: string;
attachment_urls: string[];
}
export interface UpdateStatusRequest {
status: 'pending' | 'processing' | 'resolved' | 'closed';
status_description?: string;
handler_id?: string;
}
export interface UpdateRemarkRequest {
remark: string;
}
}
/**
* 获取投诉列表
*/
async function getComplaintList(params: Recordable<any>) {
return requestClient.get<ComplaintApi.ComplaintList>('/complaint/list', {
params,
});
}
/**
* 获取投诉详情
* @param id 投诉 ID
*/
async function getComplaintDetail(id: string) {
return requestClient.get<ComplaintApi.ComplaintDetail>(
`/complaint/detail/${id}`,
);
}
/**
* 更新投诉状态
* @param id 投诉 ID
* @param data 更新状态请求数据
*/
async function updateComplaintStatus(
id: string,
data: ComplaintApi.UpdateStatusRequest,
) {
return requestClient.put<{ success: boolean }>(
`/complaint/update-status/${id}`,
data,
);
}
/**
* 更新投诉备注
* @param id 投诉 ID
* @param data 更新备注请求数据
*/
async function updateComplaintRemark(
id: string,
data: ComplaintApi.UpdateRemarkRequest,
) {
return requestClient.put<{ success: boolean }>(
`/complaint/update-remark/${id}`,
data,
);
}
export { getComplaintList, getComplaintDetail, updateComplaintStatus, updateComplaintRemark };

View File

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

View File

@@ -0,0 +1,74 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace DashboardApi {
export interface OrderStatistics {
today_count: number;
month_count: number;
total_count: number;
yesterday_count: number;
change_rate: number;
}
export interface RevenueStatistics {
today_amount: number;
month_amount: number;
total_amount: number;
yesterday_amount: number;
change_rate: number;
}
export interface AgentStatistics {
total_count: number;
today_new: number;
month_new: number;
}
export interface ProfitDetail {
revenue: number; // 营收
commission: number; // 佣金
rebate: number; // 返利
company_tax: number; // 税务成本
api_cost: number; // API调用成本
tax_income: number; // 提现收税
profit: number; // 利润
profit_rate: number; // 利润率
}
export interface ProfitStatistics {
today_profit: number;
month_profit: number;
total_profit: number;
today_profit_rate: number;
month_profit_rate: number;
total_profit_rate: number;
today_detail: ProfitDetail;
month_detail: ProfitDetail;
total_detail: ProfitDetail;
}
export interface TrendData {
date: string;
value: number;
}
export interface DashboardStatistics {
order_stats: OrderStatistics;
revenue_stats: RevenueStatistics;
agent_stats: AgentStatistics;
profit_stats: ProfitStatistics;
order_trend: TrendData[];
revenue_trend: TrendData[];
}
}
/**
* 获取统计面板数据
*/
export async function getDashboardStatistics(): Promise<DashboardApi.DashboardStatistics> {
return await requestClient.get<DashboardApi.DashboardStatistics>(
'/dashboard/statistics',
);
}

View File

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

View File

@@ -1,10 +1,11 @@
export * from './agent';
export * from './complaint';
export * from './core';
export * from './dashboard';
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;

View File

@@ -4,7 +4,7 @@ import { requestClient } from '#/api/request';
export namespace OrderApi {
export interface Order {
id: number;
id: string;
order_no: string;
platform_order_id: string;
product_name: string;
@@ -16,7 +16,6 @@ export namespace OrderApi {
create_time: string;
pay_time: null | string;
refund_time: null | string;
is_promotion: 0 | 1;
}
export interface OrderList {
@@ -50,7 +49,7 @@ async function getOrderList(params: Recordable<any>) {
* @param id 订单 ID
* @param data 退款请求数据
*/
async function refundOrder(id: number, data: OrderApi.RefundOrderRequest) {
async function refundOrder(id: string, data: OrderApi.RefundOrderRequest) {
return requestClient.post<OrderApi.RefundOrderResponse>(
`/order/refund/${id}`,
data,

View File

@@ -9,9 +9,9 @@ export namespace OrderQueryApi {
}
export interface QueryDetail {
id: number;
order_id: number;
user_id: number;
id: string;
order_id: string;
user_id: string;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
@@ -21,13 +21,13 @@ export namespace OrderQueryApi {
}
export interface GetQueryDetailRequest {
order_id: number;
order_id: string;
}
export interface GetQueryDetailResponse {
id: number;
order_id: number;
user_id: number;
id: string;
order_id: string;
user_id: string;
product_name: string;
query_params: Recordable<any>;
query_data: QueryItem[];
@@ -119,7 +119,7 @@ export namespace OrderQueryApi {
* 获取订单查询详情
* @param orderId 订单ID
*/
async function getOrderQueryDetail(orderId: number) {
async function getOrderQueryDetail(orderId: string) {
return requestClient.get<OrderQueryApi.GetQueryDetailResponse>(
`/query/detail/${orderId}`,
);

View File

@@ -7,6 +7,8 @@ export namespace FeatureApi {
id: number;
api_id: string;
name: string;
whitelist_price: number;
cost_price: number;
create_time: string;
update_time: string;
}
@@ -19,11 +21,15 @@ export namespace FeatureApi {
export interface CreateFeatureRequest {
api_id: string;
name: string;
whitelist_price?: number;
cost_price?: number;
}
export interface UpdateFeatureRequest {
api_id?: string;
name?: string;
whitelist_price?: number;
cost_price?: number;
}
export interface FeatureExampleItem {

View File

@@ -1,45 +0,0 @@
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

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

View File

@@ -1,67 +0,0 @@
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

@@ -4,9 +4,9 @@ import { requestClient } from '#/api/request';
export namespace SystemApiApi {
export interface SystemApiItem {
id: number;
role_id?: number;
api_id?: number;
id: string;
role_id?: string;
api_id?: string;
api_name: string;
api_code: string;
method: string;
@@ -31,9 +31,9 @@ export namespace SystemApiApi {
}
export interface RoleApiItem {
id: number;
role_id: number;
api_id: number;
id: string;
role_id: string;
api_id: string;
api_name: string;
api_code: string;
method: string;
@@ -60,7 +60,7 @@ async function getApiList(params: Recordable<any>) {
* 获取API详情
* @param id API ID
*/
async function getApiDetail(id: number) {
async function getApiDetail(id: string) {
return requestClient.get<SystemApiApi.SystemApiItem>(`/api/detail/${id}`);
}
@@ -80,7 +80,7 @@ async function createApi(
* @param data API数据
*/
async function updateApi(
id: number,
id: string,
data: Omit<SystemApiApi.SystemApiItem, 'create_time' | 'id' | 'update_time'>,
) {
return requestClient.put(`/api/update/${id}`, data);
@@ -107,7 +107,7 @@ async function batchUpdateApiStatus(data: { ids: number[]; status: 0 | 1 }) {
* 获取角色API权限列表
* @param roleId 角色ID
*/
async function getRoleApiList(roleId: number) {
async function getRoleApiList(roleId: string) {
return requestClient.get<SystemApiApi.SystemRoleApiResponse>(
`/role/${roleId}/api/list`,
);
@@ -118,7 +118,7 @@ async function getRoleApiList(roleId: number) {
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function assignRoleApi(data: { api_ids: number[]; role_id: number }) {
async function assignRoleApi(data: { api_ids: string[]; role_id: string }) {
return requestClient.post('/role/api/assign', data);
}
@@ -127,7 +127,7 @@ async function assignRoleApi(data: { api_ids: number[]; role_id: number }) {
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function removeRoleApi(data: { api_ids: number[]; role_id: number }) {
async function removeRoleApi(data: { api_ids: string[]; role_id: string }) {
return requestClient.post('/role/api/remove', data);
}
@@ -136,7 +136,7 @@ async function removeRoleApi(data: { api_ids: number[]; role_id: number }) {
* @param data.api_ids API ID数组
* @param data.role_id 角色ID
*/
async function updateRoleApi(data: { api_ids: number[]; role_id: number }) {
async function updateRoleApi(data: { api_ids: string[]; role_id: string }) {
return requestClient.put('/role/api/update', data);
}

View File

@@ -4,14 +4,14 @@ import { requestClient } from '#/api/request';
export namespace SystemRoleApi {
export interface SystemRoleItem {
id: number;
id: string;
role_name: string;
role_code: string;
description?: string;
status: 0 | 1;
sort: number;
create_time: string;
menu_ids: number[];
menu_ids: string[];
}
export interface SystemRole {
@@ -44,7 +44,7 @@ async function createRole(data: Omit<SystemRoleApi.SystemRoleItem, 'id'>) {
* @param data 角色数据
*/
async function updateRole(
id: number,
id: string,
data: Omit<SystemRoleApi.SystemRoleItem, 'id'>,
) {
return requestClient.put(`/role/update/${id}`, data);

View File

@@ -12,13 +12,13 @@ export const overridesPreferences = defineOverridesPreferences({
accessMode: 'backend',
},
logo: {
source: 'https://ctrlph.tianyuandb.com/logo.png',
source: 'https://ctrlph.onecha.cn/logo.png',
},
copyright: {
companyName: '海南天远大数据科技有限公司',
companySiteLink: 'https://www.tianyuandb.com',
companyName: '海南海宇大数据有限公司',
companySiteLink: 'https://www.onecha.cn',
date: '2025',
icp: '琼ICP备2024048057号-1',
icp: '琼ICP备2024048057号-2',
icpLink: 'https://beian.miit.gov.cn/',
},
footer: {

View File

@@ -0,0 +1,117 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'mdi:account-group',
order: 2000,
title: '代理管理',
},
name: 'Agent',
path: '/agent',
children: [
{
path: '/agent/list',
name: 'AgentList',
meta: {
icon: 'mdi:account-multiple',
title: '代理列表',
},
component: () => import('#/views/agent/agent-list/list.vue'),
},
{
path: '/agent/links',
name: 'AgentLinks',
meta: {
icon: 'mdi:link-variant',
title: '推广链接',
},
component: () => import('#/views/agent/agent-links/list.vue'),
},
{
path: '/agent/commission',
name: 'AgentCommission',
meta: {
icon: 'mdi:cash-multiple',
title: '佣金记录',
},
component: () => import('#/views/agent/agent-commission/list.vue'),
},
{
path: '/agent/rebate',
name: 'AgentRebate',
meta: {
icon: 'mdi:currency-usd',
title: '返佣记录',
},
component: () => import('#/views/agent/agent-rebate/list.vue'),
},
{
path: '/agent/upgrade',
name: 'AgentUpgrade',
meta: {
icon: 'mdi:arrow-up-circle',
title: '升级记录',
},
component: () => import('#/views/agent/agent-upgrade/list.vue'),
},
{
path: '/agent/order',
name: 'AgentOrder',
meta: {
icon: 'mdi:package-variant',
title: '订单记录',
},
component: () => import('#/views/agent/agent-order/list.vue'),
},
{
path: '/agent/withdrawal',
name: 'AgentWithdrawal',
meta: {
icon: 'mdi:bank-transfer-out',
title: '提现记录',
},
component: () => import('#/views/agent/agent-withdrawal/list.vue'),
},
{
path: '/agent/invite-code',
name: 'AgentInviteCode',
meta: {
icon: 'mdi:ticket-confirmation',
title: '邀请码管理',
},
component: () => import('#/views/agent/agent-invite-code/list.vue'),
},
{
path: '/agent/config',
name: 'AgentConfig',
meta: {
icon: 'mdi:cog',
title: '系统配置',
},
component: () => import('#/views/agent/agent-config/list.vue'),
},
{
path: '/agent/real-name',
name: 'AgentRealName',
meta: {
icon: 'mdi:account-check',
title: '实名认证',
},
component: () => import('#/views/agent/agent-real-name/list.vue'),
},
{
path: '/agent/product-config',
name: 'AgentProductConfig',
meta: {
icon: 'mdi:package-variant-closed',
title: '产品配置',
},
component: () => import('#/views/agent/agent-product-config/list.vue'),
},
],
},
];
export default routes;

View File

@@ -0,0 +1,17 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:message-square-warning',
order: 2000,
title: '投诉管理',
},
name: 'Complaint',
path: '/complaint',
component: () => import('#/views/complaint/list/index.vue'),
},
];
export default routes;

View File

@@ -11,6 +11,15 @@ const routes: RouteRecordRaw[] = [
path: '/order',
component: () => import('#/views/order/order/index.vue'),
},
{
meta: {
hideInMenu: true,
title: '订单查询详情',
},
name: 'OrderQueryDetail',
path: '/order/query/detail/:id',
component: () => import('#/views/order/query/query-details.vue'),
},
];
export default routes;

View File

@@ -0,0 +1,113 @@
/**
* 等级数字转中文名称
*/
export function getLevelName(level: number): string {
const map: Record<number, string> = {
1: '普通代理',
2: '黄金代理',
3: '钻石代理',
};
return map[level] || '未知';
}
/**
* 返佣类型转中文
*/
export function getRebateTypeName(type: number): string {
const map: Record<number, string> = {
1: '直接上级返佣',
2: '钻石上级返佣',
3: '黄金上级返佣',
};
return map[type] || '未知';
}
/**
* 升级类型转中文
*/
export function getUpgradeTypeName(type: number): string {
const map: Record<number, string> = {
1: '自主付费',
2: '钻石升级下级',
};
return map[type] || '未知';
}
/**
* 提现状态转中文
*/
export function getWithdrawalStatusName(status: number): string {
const map: Record<number, string> = {
1: '待审核',
2: '审核通过',
3: '审核拒绝',
4: '提现中',
5: '提现成功',
6: '提现失败',
};
return map[status] || '未知';
}
/**
* 订单处理状态转中文
*/
export function getOrderProcessStatusName(status: number): string {
const map: Record<number, string> = {
0: '待处理',
1: '处理成功',
2: '处理失败',
};
return map[status] || '未知';
}
/**
* 升级状态转中文
*/
export function getUpgradeStatusName(status: number): string {
const map: Record<number, string> = {
1: '待处理',
2: '已完成',
3: '已失败',
};
return map[status] || '未知';
}
/**
* 邀请码状态转中文
*/
export function getInviteCodeStatusName(status: number): string {
const map: Record<number, string> = {
0: '未使用',
1: '已使用',
2: '已失效',
};
return map[status] || '未知';
}
/**
* 实名认证状态转中文
*/
export function getRealNameStatusName(status: number): string {
const map: Record<number, string> = {
1: '未验证',
2: '已通过',
};
return map[status] || '未知';
}
/**
* 身份证号脱敏显示
*/
export function maskIdCard(idCard: string): string {
if (!idCard || idCard.length < 14) return idCard;
return `${idCard.slice(0, 6)}${'*'.repeat(8)}${idCard.slice(-4)}`;
}
/**
* 手机号脱敏显示
*/
export function maskMobile(mobile: string): string {
if (!mobile || mobile.length !== 11) return mobile;
return `${mobile.slice(0, 3)}****${mobile.slice(-4)}`;
}

View File

@@ -1,101 +0,0 @@
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

@@ -32,9 +32,9 @@ export function useCommissionColumns(): VxeTableGridOptions['columns'] {
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
0: '待结算',
1: '已结',
2: '已取消',
1: '已发放',
2: '已结',
3: '已取消(已退款)',
};
return statusMap[cellValue] || '未知';
},
@@ -64,9 +64,9 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
componentProps: {
allowClear: true,
options: [
{ label: '待结算', value: 0 },
{ label: '已结', value: 1 },
{ label: '已取消', value: 2 },
{ label: '已发放', value: 1 },
{ label: '已结', value: 2 },
{ label: '已取消(已退款)', value: 3 },
],
},
},

View File

@@ -0,0 +1,274 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Col, Form, InputNumber, Row, Space, message } from 'ant-design-vue';
import { getAgentConfig, updateAgentConfig } from '#/api/agent';
const loading = ref(false);
const config = ref<AgentApi.AgentConfig | null>(null);
// 使用 reactive 管理表单数据(价格配置已移除,改为产品配置表管理)
const formData = reactive<AgentApi.AgentConfig>({
level_bonus: {
normal: 0,
gold: 0,
diamond: 0,
},
upgrade_fee: {
normal_to_gold: 0,
normal_to_diamond: 0,
gold_to_diamond: 0,
},
upgrade_rebate: {
normal_to_gold_rebate: 0,
to_diamond_rebate: 0,
},
direct_parent_rebate: {
diamond: 0,
gold: 0,
normal: 0,
},
max_gold_rebate_amount: 0,
commission_freeze: {
ratio: 0,
threshold: 0,
days: 0,
},
tax_rate: 0,
tax_exemption_amount: 0,
gold_max_uplift_amount: 0,
diamond_max_uplift_amount: 0,
});
// 加载配置
async function loadConfig() {
loading.value = true;
try {
const res = await getAgentConfig();
config.value = res;
Object.assign(formData, res);
} catch (error) {
console.error('加载配置失败:', error);
} finally {
loading.value = false;
}
}
// 保存配置
async function handleSave() {
try {
const params: AgentApi.UpdateAgentConfigParams = {
level_bonus: {
normal: formData.level_bonus.normal,
gold: formData.level_bonus.gold,
diamond: formData.level_bonus.diamond,
},
upgrade_fee: {
normal_to_gold: formData.upgrade_fee.normal_to_gold,
normal_to_diamond: formData.upgrade_fee.normal_to_diamond,
},
upgrade_rebate: {
normal_to_gold_rebate: formData.upgrade_rebate.normal_to_gold_rebate,
to_diamond_rebate: formData.upgrade_rebate.to_diamond_rebate,
},
direct_parent_rebate: {
diamond: formData.direct_parent_rebate.diamond,
gold: formData.direct_parent_rebate.gold,
normal: formData.direct_parent_rebate.normal,
},
max_gold_rebate_amount: formData.max_gold_rebate_amount,
commission_freeze: {
ratio: formData.commission_freeze.ratio,
threshold: formData.commission_freeze.threshold,
days: formData.commission_freeze.days,
},
tax_rate: formData.tax_rate,
tax_exemption_amount: formData.tax_exemption_amount,
gold_max_uplift_amount: formData.gold_max_uplift_amount,
diamond_max_uplift_amount: formData.diamond_max_uplift_amount,
};
await updateAgentConfig(params);
message.success('配置保存成功');
loadConfig();
} catch (error) {
console.error('保存配置失败:', error);
}
}
// 重置配置
function handleReset() {
if (config.value) {
Object.assign(formData, config.value);
}
}
onMounted(() => {
loadConfig();
});
</script>
<template>
<Page auto-content-height>
<Card title="系统配置" :loading="loading">
<Form layout="vertical">
<Card title="等级奖金" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['level_bonus', 'normal']" label="普通代理奖金">
<InputNumber v-model:value="formData.level_bonus.normal" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['level_bonus', 'gold']" label="黄金代理奖金">
<InputNumber v-model:value="formData.level_bonus.gold" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['level_bonus', 'diamond']" label="钻石代理奖金">
<InputNumber v-model:value="formData.level_bonus.diamond" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="等级最高价上调金额" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item name="gold_max_uplift_amount" label="黄金代理最高价上调金额">
<InputNumber v-model:value="formData.gold_max_uplift_amount" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item name="diamond_max_uplift_amount" label="钻石代理最高价上调金额">
<InputNumber v-model:value="formData.diamond_max_uplift_amount" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="升级费用" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['upgrade_fee', 'normal_to_gold']" label="普通→黄金">
<InputNumber v-model:value="formData.upgrade_fee.normal_to_gold" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['upgrade_fee', 'normal_to_diamond']" label="普通→钻石">
<InputNumber v-model:value="formData.upgrade_fee.normal_to_diamond" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="升级返佣" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item :name="['upgrade_rebate', 'normal_to_gold_rebate']" label="普通→黄金返佣">
<InputNumber v-model:value="formData.upgrade_rebate.normal_to_gold_rebate" :min="0" :precision="2"
:step="0.01" style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item :name="['upgrade_rebate', 'to_diamond_rebate']" label="→钻石返佣">
<InputNumber v-model:value="formData.upgrade_rebate.to_diamond_rebate" :min="0" :precision="2"
:step="0.01" style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="直接上级返佣配置" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['direct_parent_rebate', 'diamond']" label="直接上级是钻石的返佣金额">
<InputNumber v-model:value="formData.direct_parent_rebate.diamond" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['direct_parent_rebate', 'gold']" label="直接上级是黄金的返佣金额">
<InputNumber v-model:value="formData.direct_parent_rebate.gold" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['direct_parent_rebate', 'normal']" label="直接上级是普通的返佣金额">
<InputNumber v-model:value="formData.direct_parent_rebate.normal" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="返佣限额配置" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item name="max_gold_rebate_amount" label="黄金代理最大返佣金额">
<InputNumber v-model:value="formData.max_gold_rebate_amount" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="佣金冻结配置" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['commission_freeze', 'ratio']" label="佣金冻结比例例如0.1表示10%">
<InputNumber v-model:value="formData.commission_freeze.ratio" :min="0" :max="1" :precision="4"
:step="0.0001" style="width: 100%" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['commission_freeze', 'threshold']" label="佣金冻结阈值">
<InputNumber v-model:value="formData.commission_freeze.threshold" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<Form.Item :name="['commission_freeze', 'days']" label="佣金冻结解冻天数">
<InputNumber v-model:value="formData.commission_freeze.days" :min="0" :precision="0" :step="1"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="税费配置" size="small" class="mb-4">
<Row :gutter="[16, 16]">
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item name="tax_rate" label="税率例如0.06表示6%">
<InputNumber v-model:value="formData.tax_rate" :min="0" :max="1" :precision="4" :step="0.0001"
style="width: 100%" />
</Form.Item>
</Col>
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<Form.Item name="tax_exemption_amount" label="免税额度">
<InputNumber v-model:value="formData.tax_exemption_amount" :min="0" :precision="2" :step="0.01"
style="width: 100%" addon-after="" />
</Form.Item>
</Col>
</Row>
</Card>
<Space>
<Button type="primary" @click="handleSave">保存</Button>
<Button @click="handleReset">重置</Button>
</Space>
</Form>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,109 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getLevelName } from '#/utils/agent';
export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'code',
title: '邀请码',
width: 200,
},
{
field: 'agent_id',
title: '发放代理ID',
width: 120,
},
{
field: 'agent_mobile',
title: '发放代理手机号',
width: 140,
},
{
field: 'target_level',
title: '目标等级',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
return getLevelName(cellValue);
},
},
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellTag',
options: [
{ value: 0, color: 'default', label: '未使用' },
{ value: 1, color: 'success', label: '已使用' },
{ value: 2, color: 'error', label: '已失效' },
],
},
},
{
field: 'used_user_id',
title: '使用用户ID',
width: 120,
},
{
field: 'used_agent_id',
title: '使用代理ID',
width: 120,
},
{
field: 'used_time',
title: '使用时间',
width: 160,
},
{
field: 'expire_time',
title: '过期时间',
width: 160,
},
{
field: 'remark',
title: '备注',
width: 200,
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
export function useInviteCodeFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'code',
label: '邀请码',
},
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '发放代理ID',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '未使用', value: 0 },
{ label: '已使用', value: 1 },
{ label: '已失效', value: 2 },
],
},
},
];
}

View File

@@ -0,0 +1,175 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Input, InputNumber, Modal, Space, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
generateDiamondInviteCode,
getInviteCodeList,
} from '#/api/agent';
import { useInviteCodeColumns, useInviteCodeFormSchema } from './data';
interface QueryParams {
currentPage: number;
pageSize: number;
[key: string]: any;
}
const generateModalVisible = ref(false);
const generatedCodes = ref<string[]>([]);
const generateForm = ref({
count: 1,
expire_days: 0,
remark: '',
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useInviteCodeFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useInviteCodeColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getInviteCodeList({
...form,
target_level: 3,
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
props: {
result: 'items',
total: 'total',
},
},
},
});
// 生成邀请码
function handleGenerate() {
generateForm.value = {
count: 1,
expire_days: 0,
remark: '',
};
generatedCodes.value = [];
generateModalVisible.value = true;
}
// 确认生成
async function confirmGenerate() {
try {
const res = await generateDiamondInviteCode({
count: generateForm.value.count,
expire_days:
generateForm.value.expire_days > 0
? generateForm.value.expire_days
: undefined,
remark: generateForm.value.remark || undefined,
});
generatedCodes.value = res.codes;
message.success('邀请码生成成功');
gridApi.query();
} catch (error) {
console.error('生成邀请码失败:', error);
}
}
// 复制邀请码
function copyCodes() {
const text = generatedCodes.value.join('\n');
navigator.clipboard.writeText(text).then(() => {
message.success('邀请码已复制到剪贴板');
});
}
// 关闭弹窗
function closeGenerateModal() {
generateModalVisible.value = false;
generatedCodes.value = [];
}
</script>
<template>
<Page auto-content-height>
<Grid table-title="邀请码列表">
<template #toolbar>
<Button type="primary" @click="handleGenerate">生成钻石邀请码</Button>
</template>
</Grid>
<Modal v-model:open="generateModalVisible" :title="generatedCodes.length > 0 ? '生成的邀请码' : '生成钻石邀请码'" :width="600"
@cancel="closeGenerateModal">
<div v-if="generatedCodes.length === 0">
<div class="mb-4">
<label>生成数量</label>
<InputNumber v-model:value="generateForm.count" :min="1" :max="100" class="w-full" />
</div>
<div class="mb-4">
<label>过期天数0表示不过期</label>
<InputNumber v-model:value="generateForm.expire_days" :min="0" class="w-full" />
</div>
<div class="mb-4">
<label>备注</label>
<Input.TextArea v-model:value="generateForm.remark" :rows="3" placeholder="请输入备注" />
</div>
</div>
<div v-else>
<div class="mb-4">
<strong>已生成 {{ generatedCodes.length }} 个邀请码</strong>
</div>
<div class="code-list">
<div v-for="(code, index) in generatedCodes" :key="index" class="code-item">
{{ code }}
</div>
</div>
</div>
<template #footer>
<Button v-if="generatedCodes.length > 0" @click="closeGenerateModal">
关闭
</Button>
<Button v-else @click="closeGenerateModal">取消</Button>
<Button v-if="generatedCodes.length > 0" type="primary" @click="copyCodes">
复制所有
</Button>
<Button v-else type="primary" @click="confirmGenerate">生成</Button>
</template>
</Modal>
</Page>
</template>
<style lang="less" scoped>
.code-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px;
}
.code-item {
padding: 4px 8px;
font-family: monospace;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
</style>

View File

@@ -9,14 +9,25 @@ export function useLinkColumns(): VxeTableGridOptions['columns'] {
title: '代理ID',
width: 100,
},
{
field: 'product_id',
title: '产品ID',
width: 100,
},
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'price',
title: '价格',
field: 'set_price',
title: '设定价格',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
field: 'actual_base_price',
title: '实际底价',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
@@ -37,6 +48,11 @@ export function useLinkColumns(): VxeTableGridOptions['columns'] {
// 推广链接搜索表单配置
export function useLinkFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'product_id',
label: '产品ID',
},
{
component: 'Input',
fieldName: 'product_name',

View File

@@ -1,6 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getLevelName } from '#/utils/agent';
// 表单配置
export function useFormSchema(): VbenFormSchema[] {
return [
@@ -11,10 +13,18 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
},
{
component: 'Input',
fieldName: 'level_name',
label: '等级名称',
component: 'Select',
fieldName: 'level',
label: '等级',
rules: 'required',
componentProps: {
disabled: true,
options: [
{ label: '普通代理', value: 1 },
{ label: '黄金代理', value: 2 },
{ label: '钻石代理', value: 3 },
],
},
},
{
component: 'Input',
@@ -23,13 +33,9 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
},
{
component: 'DatePicker',
fieldName: 'membership_expiry_time',
label: '会员到期时间',
rules: 'required',
componentProps: {
showTime: true,
},
component: 'Input',
fieldName: 'wechat_id',
label: '微信号',
},
];
}
@@ -47,6 +53,24 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'region',
label: '区域',
},
{
component: 'Select',
fieldName: 'level',
label: '等级',
componentProps: {
allowClear: true,
options: [
{ label: '普通代理', value: 1 },
{ label: '黄金代理', value: 2 },
{ label: '钻石代理', value: 3 },
],
},
},
{
component: 'InputNumber',
fieldName: 'team_leader_id',
label: '团队首领ID',
},
{
component: 'RangePicker',
fieldName: 'create_time',
@@ -71,14 +95,16 @@ export function useColumns(): VxeTableGridOptions['columns'] {
width: 100,
},
{
field: 'level_name',
title: '等级名称',
field: 'agent_code',
title: '代理编码',
width: 100,
},
{
field: 'level',
title: '等级',
width: 120,
formatter: ({ cellValue }: { cellValue: string }) => {
if (cellValue === '' || cellValue === 'normal') {
return '普通代理';
}
return cellValue;
formatter: ({ cellValue }: { cellValue: number }) => {
return getLevelName(cellValue);
},
},
{
@@ -95,41 +121,25 @@ export function useColumns(): VxeTableGridOptions['columns'] {
cellRender: {
name: 'CellTag',
options: [
{ value: 'approved', color: 'success', label: '已认证' },
{ value: 'pending', color: 'warning', label: '审核中' },
{ value: 'rejected', color: 'error', label: '已拒绝' },
{ value: '', color: 'default', label: '未认证' },
{ value: true, color: 'success', label: '已认证' },
{ value: false, color: 'default', label: '未认证' },
],
},
field: 'real_name_status',
field: 'is_real_name',
title: '实名认证状态',
width: 120,
},
{
field: 'real_name',
title: '实名姓名',
field: 'wechat_id',
title: '微信号',
width: 120,
formatter: ({ cellValue }: { cellValue: string }) => {
if (!cellValue) return '-';
return cellValue;
},
visible: false,
},
{
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: 'team_leader_id',
title: '团队首领ID',
width: 120,
visible: false,
},
{
field: 'balance',

View File

@@ -17,12 +17,12 @@ 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 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 OrderModal from './modules/order-modal.vue';
import RebateModal from './modules/rebate-modal.vue';
import UpgradeModal from './modules/upgrade-modal.vue';
import WithdrawalModal from './modules/withdrawal-modal.vue';
const route = useRoute();
@@ -46,9 +46,21 @@ const [CommissionModalComponent, commissionModalApi] = useVbenModal({
destroyOnClose: true,
});
// 奖励记录弹窗
const [RewardModalComponent, rewardModalApi] = useVbenModal({
connectedComponent: RewardModal,
// 返佣记录弹窗
const [RebateModalComponent, rebateModalApi] = useVbenModal({
connectedComponent: RebateModal,
destroyOnClose: true,
});
// 升级记录弹窗
const [UpgradeModalComponent, upgradeModalApi] = useVbenModal({
connectedComponent: UpgradeModal,
destroyOnClose: true,
});
// 订单记录弹窗
const [OrderModalComponent, orderModalApi] = useVbenModal({
connectedComponent: OrderModal,
destroyOnClose: true,
});
@@ -58,20 +70,6 @@ const [WithdrawalModalComponent, withdrawalModalApi] = useVbenModal({
destroyOnClose: true,
});
// 上级抽佣弹窗
const [CommissionDeductionModalComponent, commissionDeductionModalApi] =
useVbenModal({
connectedComponent: CommissionDeductionModal,
destroyOnClose: true,
});
// 平台抽佣弹窗
const [PlatformDeductionModalComponent, platformDeductionModalApi] =
useVbenModal({
connectedComponent: PlatformDeductionModal,
destroyOnClose: true,
});
// 表格配置
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -102,9 +100,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
query: async ({ page, sort }, formValues) => {
const sortParams = sort
? {
order_by: sort.field,
order_type: sort.order,
}
order_by: sort.field,
order_type: sort.order,
}
: {};
const res = await getAgentList({
@@ -112,8 +110,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
pageSize: page.pageSize,
...formValues,
...sortParams,
parent_agent_id: route.query.parent_agent_id
? Number(route.query.parent_agent_id)
team_leader_id: route.query.team_leader_id
? Number(route.query.team_leader_id)
: undefined,
});
@@ -148,21 +146,17 @@ const moreMenuItems = [
key: 'links',
label: '推广链接',
},
// {
// key: 'commission',
// label: '佣金记录',
// },
// {
// key: 'commission-deduction',
// label: '上级抽佣',
// },
// {
// key: 'platform-deduction',
// label: '平台抽佣',
// },
{
key: 'reward',
label: '奖励记录',
key: 'rebate',
label: '返佣记录',
},
{
key: 'upgrade',
label: '升级记录',
},
{
key: 'order',
label: '订单记录',
},
{
key: 'withdrawal',
@@ -170,15 +164,15 @@ const moreMenuItems = [
},
];
// 上级代理信息
const parentAgentId = computed(() => route.query.parent_agent_id);
// 团队首领信息
const teamLeaderId = computed(() => route.query.team_leader_id);
// 返回上级列表
// 返回团队首领列表
function onBackToParent() {
router.replace({
query: {
...route.query,
parent_agent_id: undefined,
team_leader_id: undefined,
},
});
}
@@ -194,10 +188,6 @@ function onActionClick(
onViewCommission(e.row);
break;
}
case 'commission-deduction': {
onViewCommissionDeduction(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
@@ -206,19 +196,23 @@ function onActionClick(
onViewLinks(e.row);
break;
}
case 'platform-deduction': {
onViewPlatformDeduction(e.row);
case 'rebate': {
onViewRebate(e.row);
break;
}
case 'reward': {
onViewReward(e.row);
case 'upgrade': {
onViewUpgrade(e.row);
break;
}
case 'order': {
onViewOrder(e.row);
break;
}
case 'view-sub-agent': {
router.replace({
query: {
...route.query,
parent_agent_id: e.row.id,
team_leader_id: e.row.id,
},
});
break;
@@ -245,9 +239,19 @@ function onViewCommission(row: AgentApi.AgentListItem) {
commissionModalApi.setData({ agentId: row.id }).open();
}
// 查看奖励记录
function onViewReward(row: AgentApi.AgentListItem) {
rewardModalApi.setData({ agentId: row.id }).open();
// 查看返佣记录
function onViewRebate(row: AgentApi.AgentListItem) {
rebateModalApi.setData({ agentId: row.id }).open();
}
// 查看升级记录
function onViewUpgrade(row: AgentApi.AgentListItem) {
upgradeModalApi.setData({ agentId: row.id }).open();
}
// 查看订单记录
function onViewOrder(row: AgentApi.AgentListItem) {
orderModalApi.setData({ agentId: row.id }).open();
}
// 查看提现记录
@@ -255,16 +259,6 @@ 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 onRefresh() {
gridApi.query();
@@ -276,16 +270,16 @@ function onRefresh() {
<FormDrawer @success="onRefresh" />
<LinkModalComponent />
<CommissionModalComponent />
<CommissionDeductionModalComponent />
<PlatformDeductionModalComponent />
<RewardModalComponent />
<RebateModalComponent />
<UpgradeModalComponent />
<OrderModalComponent />
<WithdrawalModalComponent />
<!-- 上级代理信息卡片 -->
<Card v-if="parentAgentId" class="mb-4">
<!-- 团队首领信息卡片 -->
<Card v-if="teamLeaderId" class="mb-4">
<div class="flex items-center gap-4">
<Button @click="onBackToParent">返回上级列表</Button>
<div>上级代理ID{{ parentAgentId }}</div>
<div>团队首领ID{{ teamLeaderId }}</div>
</div>
</Card>
@@ -295,10 +289,7 @@ function onRefresh() {
<Button type="link" @click="onActionClick({ code: 'edit', row })">
编辑
</Button>
<Button
type="link"
@click="onActionClick({ code: 'view-sub-agent', row })"
>
<Button type="link" @click="onActionClick({ code: 'view-sub-agent', row })">
查看下级
</Button>
<!-- <Button
@@ -310,10 +301,7 @@ function onRefresh() {
<Dropdown>
<Button type="link">更多操作</Button>
<template #overlay>
<Menu
:items="moreMenuItems"
@click="(e) => onActionClick({ code: String(e.key), row })"
/>
<Menu :items="moreMenuItems" @click="(e) => onActionClick({ code: String(e.key), row })" />
</template>
</Dropdown>
</div>

View File

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

View File

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

View File

@@ -3,15 +3,14 @@ import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import PlatformDeductionList from '../../agent-platform-deduction/list.vue';
import UpgradeList from '../../agent-upgrade/list.vue';
interface ModalData {
agentId: number;
}
const [Modal, modalApi] = useVbenModal({
title: '平台抽佣记录',
width: 1000,
title: '升级记录',
destroyOnClose: true,
});
@@ -19,13 +18,16 @@ const modalData = computed(() => modalApi.getData<ModalData>());
</script>
<template>
<Modal>
<PlatformDeductionList v-if="modalData" :agent-id="modalData.agentId" />
<Modal class="w-[calc(100vw-200px)]" :footer="false">
<div class="agent-upgrade-modal">
<UpgradeList :agent-id="modalData?.agentId" />
</div>
</Modal>
</template>
<style scoped>
:deep(.ant-modal-body) {
padding: 24px;
<style lang="less" scoped>
.agent-upgrade-modal {
padding: 16px;
}
</style>

View File

@@ -1,254 +0,0 @@
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

@@ -1,105 +0,0 @@
<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

@@ -1,88 +0,0 @@
<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

@@ -1,134 +0,0 @@
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

@@ -1,57 +0,0 @@
<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,120 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useOrderColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'order_id',
title: '订单ID',
width: 100,
},
{
field: 'product_id',
title: '产品ID',
width: 100,
},
{
field: 'product_name',
title: '产品名称',
width: 150,
},
{
field: 'order_amount',
title: '订单金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'set_price',
title: '设定价格',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'actual_base_price',
title: '实际底价',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'price_cost',
title: '提价成本',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'agent_profit',
title: '代理收益',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'order_status',
title: '订单状态',
width: 120,
cellRender: {
name: 'CellTag',
options: [
{ value: 'pending', color: 'warning', label: '待支付' },
{ value: 'paid', color: 'success', label: '已支付' },
{ value: 'refunded', color: 'error', label: '已退款' },
{ value: 'closed', color: 'default', label: '已关闭' },
{ value: 'failed', color: 'error', label: '支付失败' },
{ value: 'unknown', color: 'default', label: '未知' },
],
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
export function useOrderFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '代理ID',
},
{
component: 'InputNumber',
fieldName: 'order_id',
label: '订单ID',
},
{
component: 'Select',
fieldName: 'order_status',
label: '订单状态',
componentProps: {
allowClear: true,
options: [
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '已退款', value: 'refunded' },
{ label: '已关闭', value: 'closed' },
{ label: '支付失败', value: 'failed' },
],
},
},
];
}

View File

@@ -4,12 +4,9 @@ import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentPlatformDeductionList } from '#/api/agent';
import { getAgentOrderList } from '#/api/agent';
import {
usePlatformDeductionColumns,
usePlatformDeductionFormSchema,
} from './data';
import { useOrderColumns, useOrderFormSchema } from './data';
interface Props {
agentId?: number;
@@ -29,21 +26,21 @@ const queryParams = computed(() => ({
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: usePlatformDeductionFormSchema(),
schema: useOrderFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: usePlatformDeductionColumns(),
columns: useOrderColumns(),
proxyConfig: {
ajax: {
query: async ({
form,
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentPlatformDeductionList({
return await getAgentOrderList({
...queryParams.value,
...form,
page: page.currentPage,
@@ -62,6 +59,6 @@ const [Grid] = useVbenVxeGrid({
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '平台抽佣列表' : '所有平台抽佣记录'" />
<Grid :table-title="agentId ? '订单记录列表' : '所有订单记录'" />
</Page>
</template>

View File

@@ -1,96 +0,0 @@
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

@@ -1,6 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { h } from 'vue';
import { z } from '#/adapter/form';
// 代理产品配置列表列配置
export function useColumns(): VxeTableGridOptions['columns'] {
return [
@@ -14,14 +18,8 @@ export function useColumns(): VxeTableGridOptions['columns'] {
title: '产品名称',
},
{
field: 'cost_price',
title: '成本',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'price_range_min',
title: '最低定价',
field: 'base_price',
title: '基础底价',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
@@ -32,14 +30,14 @@ export function useColumns(): VxeTableGridOptions['columns'] {
`¥${cellValue.toFixed(2)}`,
},
{
field: 'pricing_standard',
title: '定价标准',
field: 'price_threshold',
title: '价格阈值',
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'overpricing_ratio',
title: '超标抽佣比例',
field: 'price_fee_rate',
title: '提价费率',
formatter: ({ cellValue }: { cellValue: number }) =>
`${(cellValue * 100).toFixed(2)}%`,
},
@@ -70,19 +68,8 @@ 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: '最低定价',
fieldName: 'base_price',
label: '基础底价',
rules: 'required',
componentProps: {
min: 0,
@@ -103,29 +90,104 @@ export function useFormSchema(): VbenFormSchema[] {
},
{
component: 'InputNumber',
fieldName: 'pricing_standard',
label: '定价标准',
rules: 'required',
fieldName: 'price_threshold',
label: '价格阈值',
defaultValue: undefined,
componentProps: {
min: 0,
precision: 2,
step: 0.01,
placeholder: '可选,不设置则不限制',
},
rules: z
.number({
invalid_type_error: '请输入有效的数字',
})
.min(0, '价格阈值不能小于0')
.optional()
.nullable(),
dependencies: {
triggerFields: ['base_price'],
rules(values) {
// 动态校验:价格阈值不能低于基础底价
const basePrice = values.base_price;
if (basePrice !== undefined && basePrice !== null) {
return z
.number({
invalid_type_error: '请输入有效的数字',
})
.min(0, '价格阈值不能小于0')
.refine(
(val) => {
if (val === undefined || val === null) return true;
return val >= basePrice;
},
{
message: `价格阈值不能低于基础底价 ${basePrice}`,
},
)
.optional()
.nullable();
}
return z
.number({
invalid_type_error: '请输入有效的数字',
})
.min(0, '价格阈值不能小于0')
.optional()
.nullable();
},
trigger(_values, formApi) {
// 当基础底价变化时,重新校验价格阈值
formApi.validateField('price_threshold');
},
},
suffix: () => h('span', { class: 'text-gray-400 text-xs' }, '可选'),
},
{
component: 'InputNumber',
fieldName: 'overpricing_ratio',
label: '超标抽佣比例',
rules: 'required',
fieldName: 'price_fee_rate',
label: '提价费率',
defaultValue: undefined,
componentProps: {
min: 0,
max: 100,
precision: 2,
precision: 4,
step: 0.01,
addonAfter: '%',
controls: true,
placeholder: '可选,不设置则不收费',
validateTrigger: ['blur', 'change'],
},
rules: z
.number({
invalid_type_error: '请输入有效的数字',
})
.min(0, '提价费率不能小于0')
.max(100, '提价费率不能大于100%')
.optional()
.nullable(),
dependencies: {
triggerFields: ['price_threshold'],
required(values) {
// 如果价格阈值有值,提价费率必填
return values.price_threshold !== undefined && values.price_threshold !== null;
},
rules(values) {
// 如果价格阈值有值,提价费率必填
if (values.price_threshold !== undefined && values.price_threshold !== null) {
return z.number().min(0, '提价费率不能小于0').max(100, '提价费率不能大于100%');
}
return z.number().optional().nullable();
},
trigger(values, formApi) {
// 当价格阈值清空时,也清空提价费率
if (values.price_threshold === undefined || values.price_threshold === null) {
formApi.setFieldValue('price_fee_rate', undefined);
}
},
},
suffix: () => h('span', { class: 'text-gray-400 text-xs' }, '可选'),
},
];
}

View File

@@ -3,6 +3,8 @@ import type { AgentApi } from '#/api/agent';
import { ref } from 'vue';
import { Button, InputNumber } from 'ant-design-vue';
import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
import { updateAgentProductionConfig } from '#/api/agent';
@@ -30,11 +32,10 @@ const [Drawer, drawerApi] = useVbenDrawer({
const values = await formApi.getValues();
const params: AgentApi.UpdateAgentProductionConfigParams = {
id: id.value,
cost_price: values.cost_price,
price_range_min: values.price_range_min,
base_price: values.base_price,
price_range_max: values.price_range_max,
pricing_standard: values.pricing_standard,
overpricing_ratio: values.overpricing_ratio / 100,
price_threshold: values.price_threshold ?? undefined,
price_fee_rate: values.price_fee_rate ? values.price_fee_rate / 100 : undefined,
};
await updateAgentProductionConfig(params);
@@ -51,7 +52,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
id.value = data.id;
formApi.setValues({
...data,
overpricing_ratio: data.overpricing_ratio * 100,
price_threshold: data.price_threshold ?? undefined,
price_fee_rate: data.price_fee_rate ? data.price_fee_rate * 100 : undefined,
});
} else {
id.value = undefined;
@@ -63,6 +65,31 @@ const [Drawer, drawerApi] = useVbenDrawer({
<template>
<Drawer :title="drawerTitle">
<Form />
<Form>
<template #price_threshold="slotProps">
<div class="flex items-center gap-2">
<InputNumber :value="slotProps.componentField.modelValue" :min="0" :precision="2" :step="0.01"
placeholder="可选,不设置则不限制" class="flex-1" @update:value="slotProps.componentField['onUpdate:modelValue']" />
<Button type="link" size="small" @click="() => {
formApi.setFieldValue('price_threshold', undefined);
formApi.setFieldValue('price_fee_rate', undefined);
}">
不设置
</Button>
</div>
</template>
<template #price_fee_rate="slotProps">
<div class="flex items-center gap-2">
<InputNumber :value="slotProps.componentField.modelValue" :min="0" :max="100" :precision="4" :step="0.01"
addon-after="%" placeholder="可选,不设置则不收费" class="flex-1"
@update:value="slotProps.componentField['onUpdate:modelValue']" />
<Button type="link" size="small" @click="() => {
formApi.setFieldValue('price_fee_rate', undefined);
}">
不设置
</Button>
</div>
</template>
</Form>
</Drawer>
</template>

View File

@@ -0,0 +1,86 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { maskIdCard, maskMobile, getRealNameStatusName } from '#/utils/agent';
export function useRealNameColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'name',
title: '姓名',
width: 120,
},
{
field: 'id_card',
title: '身份证号',
width: 180,
formatter: ({ cellValue }: { cellValue: string }) => {
return maskIdCard(cellValue);
},
},
{
field: 'mobile',
title: '手机号',
width: 140,
formatter: ({ cellValue }: { cellValue: string }) => {
return maskMobile(cellValue);
},
},
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellTag',
options: [
{ value: 1, color: 'warning', label: '未验证' },
{ value: 2, color: 'success', label: '已通过' },
],
},
},
{
field: 'verify_time',
title: '验证时间',
width: 160,
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
export function useRealNameFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '代理ID',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '未验证', value: 1 },
{ label: '已通过', value: 2 },
],
},
},
];
}

View File

@@ -4,12 +4,9 @@ import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentCommissionDeductionList } from '#/api/agent';
import { getAgentRealNameList } from '#/api/agent';
import {
useCommissionDeductionColumns,
useCommissionDeductionFormSchema,
} from './data';
import { useRealNameColumns, useRealNameFormSchema } from './data';
interface Props {
agentId?: number;
@@ -29,21 +26,21 @@ const queryParams = computed(() => ({
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCommissionDeductionFormSchema(),
schema: useRealNameFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useCommissionDeductionColumns(),
columns: useRealNameColumns(),
proxyConfig: {
ajax: {
query: async ({
form,
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentCommissionDeductionList({
return await getAgentRealNameList({
...queryParams.value,
...form,
page: page.currentPage,
@@ -62,6 +59,7 @@ const [Grid] = useVbenVxeGrid({
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '上级抽佣列表' : '所有上级抽佣记录'" />
<Grid :table-title="agentId ? '实名认证列表' : '所有实名认证'" />
</Page>
</template>

View File

@@ -0,0 +1,105 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getRebateTypeName } from '#/utils/agent';
export function useRebateColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'source_agent_id',
title: '来源代理ID',
width: 120,
},
{
field: 'order_id',
title: '订单ID',
width: 100,
},
{
field: 'rebate_type',
title: '返佣类型',
width: 140,
formatter: ({ cellValue }: { cellValue: number }) => {
return getRebateTypeName(cellValue);
},
},
{
field: 'amount',
title: '返佣金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'status',
title: '状态',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) => {
const statusMap: Record<number, string> = {
1: '已发放',
2: '已冻结',
3: '已取消(已退款)',
};
return statusMap[cellValue] || '未知';
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
export function useRebateFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '代理ID',
},
{
component: 'InputNumber',
fieldName: 'source_agent_id',
label: '来源代理ID',
},
{
component: 'Select',
fieldName: 'rebate_type',
label: '返佣类型',
componentProps: {
allowClear: true,
options: [
{ label: '直接上级返佣', value: 1 },
{ label: '钻石上级返佣', value: 2 },
{ label: '黄金上级返佣', value: 3 },
],
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '已发放', value: 1 },
{ label: '已冻结', value: 2 },
{ label: '已取消(已退款)', value: 3 },
],
},
},
];
}

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentRebateList } from '#/api/agent';
import { useRebateColumns, useRebateFormSchema } 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: useRebateFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useRebateColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentRebateList({
...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,116 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import {
getLevelName,
getUpgradeStatusName,
getUpgradeTypeName,
} from '#/utils/agent';
export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 80,
},
{
field: 'agent_id',
title: '代理ID',
width: 100,
},
{
field: 'from_level',
title: '原等级',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
return getLevelName(cellValue);
},
},
{
field: 'to_level',
title: '目标等级',
width: 100,
formatter: ({ cellValue }: { cellValue: number }) => {
return getLevelName(cellValue);
},
},
{
field: 'upgrade_type',
title: '升级类型',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) => {
return getUpgradeTypeName(cellValue);
},
},
{
field: 'upgrade_fee',
title: '升级费用',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'rebate_amount',
title: '返佣金额',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) =>
`¥${cellValue.toFixed(2)}`,
},
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellTag',
options: [
{ value: 1, color: 'warning', label: '待处理' },
{ value: 2, color: 'success', label: '已完成' },
{ value: 3, color: 'error', label: '已失败' },
],
},
},
{
field: 'create_time',
title: '创建时间',
width: 160,
sortable: true,
},
] as const;
}
export function useUpgradeFormSchema(): VbenFormSchema[] {
return [
{
component: 'InputNumber',
fieldName: 'agent_id',
label: '代理ID',
},
{
component: 'Select',
fieldName: 'upgrade_type',
label: '升级类型',
componentProps: {
allowClear: true,
options: [
{ label: '自主付费', value: 1 },
{ label: '钻石升级下级', value: 2 },
],
},
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {
allowClear: true,
options: [
{ label: '待处理', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已失败', value: 3 },
],
},
},
];
}

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentUpgradeList } from '#/api/agent';
import { useUpgradeColumns, useUpgradeFormSchema } 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: useUpgradeFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useUpgradeColumns(),
proxyConfig: {
ajax: {
query: async ({
page,
form,
}: {
form: Record<string, any>;
page: QueryParams;
}) => {
return await getAgentUpgradeList({
...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

@@ -1,16 +1,36 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
type WithdrawalMethod = 'alipay' | 'bank' | 'wechat';
type WithdrawalStatus = 'approved' | 'failed' | 'paid' | 'pending' | 'rejected';
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
return [
{
title: 'ID',
field: 'id',
width: 80,
},
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '提现单号',
field: 'withdraw_no',
width: 180,
},
{
title: '提现方式',
field: 'withdrawal_type',
width: 100,
cellRender: {
name: 'CellTag',
options: [
{ value: 1, color: 'blue', label: '支付宝' },
{ value: 2, color: 'green', label: '银行卡' },
],
},
},
{
title: '提现金额',
field: 'amount',
@@ -18,90 +38,98 @@ export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '提现方式',
field: 'method',
title: '税费金额',
field: 'tax_amount',
width: 120,
formatter: ({ cellValue }: { cellValue: WithdrawalMethod }) => {
const methodMap: Record<WithdrawalMethod, string> = {
alipay: '支付宝',
wechat: '微信',
bank: '银行卡',
};
return methodMap[cellValue] || cellValue;
},
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '实际到账金额',
field: 'actual_amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '收款账号',
field: 'account',
field: 'payee_account',
width: 180,
},
{
title: '银行卡号',
field: 'bank_card_no',
width: 180,
},
{
title: '开户行',
field: 'bank_name',
width: 150,
},
{
title: '收款人姓名',
field: 'payee_name',
width: 120,
},
{
title: '状态',
field: 'status',
width: 100,
formatter: ({ cellValue }: { cellValue: WithdrawalStatus }) => {
const statusMap: Record<WithdrawalStatus, string> = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
paid: '已打款',
failed: '打款失败',
};
return statusMap[cellValue] || cellValue;
cellRender: {
name: 'CellTag',
options: [
{ value: 1, color: 'warning', label: '待审核' },
{ value: 2, color: 'success', label: '审核通过' },
{ value: 3, color: 'error', label: '审核拒绝' },
{ value: 4, color: 'processing', label: '提现中' },
{ value: 5, color: 'success', label: '提现成功' },
{ value: 6, color: 'error', label: '提现失败' },
],
},
},
{
title: '申请时间',
field: 'create_time',
width: 180,
},
{
title: '审核时间',
field: 'audit_time',
width: 180,
},
{
title: '打款时间',
field: 'pay_time',
width: 180,
},
{
title: '备注',
field: 'remark',
width: 200,
},
{
title: '创建时间',
field: 'create_time',
width: 160,
sortable: true,
},
{
align: 'center',
slots: { default: 'operation' },
field: 'operation',
fixed: 'right',
title: '操作',
width: 120,
},
];
}
export function useWithdrawalFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'method',
label: '提现方式',
component: 'Select',
componentProps: {
options: [
{ label: '支付宝', value: 'alipay' },
{ label: '微信', value: 'wechat' },
{ label: '银行卡', value: 'bank' },
],
allowClear: true,
},
component: 'Input',
fieldName: 'withdraw_no',
label: '提现单号',
},
{
component: 'Select',
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '待审核', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已打款', value: 'paid' },
{ label: '打款失败', value: 'failed' },
],
allowClear: true,
options: [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核拒绝', value: 3 },
{ label: '提现中', value: 4 },
{ label: '提现成功', value: 5 },
{ label: '提现失败', value: 6 },
],
},
},
];
}

View File

@@ -1,10 +1,14 @@
<script lang="ts" setup>
import { computed } from 'vue';
import type { AgentApi } from '#/api/agent';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Input, Modal, Space, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAgentWithdrawalList } from '#/api/agent';
import { auditWithdrawal, getAgentWithdrawalList } from '#/api/agent';
import { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
@@ -24,7 +28,22 @@ const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const [Grid] = useVbenVxeGrid({
const auditRemark = ref('');
const auditModalVisible = ref(false);
const detailModalVisible = ref(false);
// 使用 any 以兼容后端新增字段withdrawal_type、bank_card_no、bank_name 等)
const currentWithdrawal = ref<any | null>(null);
const statusLabelMap: Record<number, string> = {
1: '待审核',
2: '审核通过',
3: '审核拒绝',
4: '提现中',
5: '提现成功',
6: '提现失败',
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useWithdrawalFormSchema(),
submitOnChange: true,
@@ -55,10 +74,155 @@ const [Grid] = useVbenVxeGrid({
},
},
});
// 审核提现
function handleAudit(row: AgentApi.AgentWithdrawalListItem) {
currentWithdrawal.value = row;
auditRemark.value = '';
auditModalVisible.value = true;
}
// 查看详情
function handleViewDetail(row: AgentApi.AgentWithdrawalListItem) {
currentWithdrawal.value = row;
detailModalVisible.value = true;
}
// 确认审核
async function confirmAudit(status: number) {
if (!currentWithdrawal.value) return;
try {
await auditWithdrawal({
withdrawal_id: currentWithdrawal.value.id,
status,
remark: auditRemark.value || '',
});
message.success('审核成功');
auditModalVisible.value = false;
gridApi.query();
} catch (error) {
console.error('审核失败:', error);
}
}
// 取消审核
function cancelAudit() {
auditModalVisible.value = false;
auditRemark.value = '';
currentWithdrawal.value = null;
}
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '提现记录列表' : '所有提现记录'" />
<Grid :table-title="agentId ? '提现记录列表' : '所有提现记录'">
<template #operation="{ row }">
<Space>
<Button v-if="row.status === 1" type="link" size="small" @click="handleAudit(row)">
审核
</Button>
<Button v-else type="link" size="small" @click="handleViewDetail(row)">
详情
</Button>
</Space>
</template>
</Grid>
<Modal v-model:open="auditModalVisible" title="审核提现" :width="600" @cancel="cancelAudit">
<div v-if="currentWithdrawal" class="audit-info">
<p><strong>提现单号</strong>{{ currentWithdrawal.withdraw_no }}</p>
<p>
<strong>提现方式</strong>
<span v-if="currentWithdrawal.withdrawal_type === 1" style="color: #1890ff;">支付宝</span>
<span v-else-if="currentWithdrawal.withdrawal_type === 2" style="color: #52c41a;">银行卡</span>
</p>
<p>
<strong>提现金额</strong>¥{{ currentWithdrawal.amount.toFixed(2) }}
</p>
<p>
<strong>税费金额</strong>¥{{ currentWithdrawal.tax_amount?.toFixed(2) ?? '0.00' }}
</p>
<p>
<strong>实际到账金额</strong>¥{{ currentWithdrawal.actual_amount?.toFixed(2) ?? '0.00' }}
</p>
<p v-if="currentWithdrawal.withdrawal_type === 1">
<strong>支付宝账号</strong>{{ currentWithdrawal.payee_account }}
</p>
<template v-else-if="currentWithdrawal.withdrawal_type === 2">
<p><strong>银行卡号</strong>{{ currentWithdrawal.bank_card_no || currentWithdrawal.payee_account }}</p>
<p><strong>开户行</strong>{{ currentWithdrawal.bank_name }}</p>
</template>
<p><strong>收款人姓名</strong>{{ currentWithdrawal.payee_name }}</p>
<p v-if="currentWithdrawal.withdrawal_type === 2" style="color: #ff4d4f; font-size: 12px; margin-top: 8px;">
请在确认已完成银行卡打款后再点击通过审核通过后将直接记为提现成功
</p>
</div>
<div class="audit-remark">
<p><strong>审核备注</strong></p>
<Input.TextArea v-model:value="auditRemark" :rows="4" placeholder="请输入审核备注" />
</div>
<template #footer>
<Button @click="cancelAudit">取消</Button>
<Button type="primary" danger @click="confirmAudit(3)">
拒绝
</Button>
<Button type="primary" @click="confirmAudit(2)">通过</Button>
</template>
</Modal>
<!-- 提现详情弹窗 -->
<Modal v-model:open="detailModalVisible" title="提现详情" :width="600" :footer="null"
@cancel="detailModalVisible = false">
<div v-if="currentWithdrawal" class="audit-info">
<p><strong>提现单号</strong>{{ currentWithdrawal.withdraw_no }}</p>
<p><strong>代理ID</strong>{{ currentWithdrawal.agent_id }}</p>
<p>
<strong>提现方式</strong>
<span v-if="currentWithdrawal.withdrawal_type === 1" style="color: #1890ff;">支付宝</span>
<span v-else-if="currentWithdrawal.withdrawal_type === 2" style="color: #52c41a;">银行卡</span>
</p>
<p>
<strong>提现金额</strong>¥{{ currentWithdrawal.amount.toFixed(2) }}
</p>
<p>
<strong>税费金额</strong>¥{{ currentWithdrawal.tax_amount?.toFixed(2) ?? '0.00' }}
</p>
<p>
<strong>实际到账金额</strong>¥{{ currentWithdrawal.actual_amount?.toFixed(2) ?? '0.00' }}
</p>
<p v-if="currentWithdrawal.withdrawal_type === 1">
<strong>支付宝账号</strong>{{ currentWithdrawal.payee_account }}
</p>
<template v-else-if="currentWithdrawal.withdrawal_type === 2">
<p><strong>银行卡号</strong>{{ currentWithdrawal.bank_card_no || currentWithdrawal.payee_account }}</p>
<p><strong>开户行</strong>{{ currentWithdrawal.bank_name }}</p>
</template>
<p><strong>收款人姓名</strong>{{ currentWithdrawal.payee_name }}</p>
<p>
<strong>状态</strong>{{ statusLabelMap[currentWithdrawal.status] ?? currentWithdrawal.status }}
</p>
<p><strong>备注</strong>{{ currentWithdrawal.remark }}</p>
<p><strong>创建时间</strong>{{ currentWithdrawal.create_time }}</p>
</div>
</Modal>
</Page>
</template>
<style lang="less" scoped>
.audit-info {
margin-bottom: 16px;
p {
margin-bottom: 8px;
}
}
.audit-remark {
margin-top: 16px;
p {
margin-bottom: 8px;
}
}
</style>

View File

@@ -0,0 +1,185 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ComplaintApi } from '#/api/complaint';
export function useColumns<T = ComplaintApi.Complaint>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: 'ID',
width: 100,
},
{
cellRender: {
name: 'CellTag',
options: [
{ value: 'alipay', color: 'blue', label: '支付宝投诉' },
{ value: 'manual', color: 'green', label: '主动投诉' },
],
},
field: 'type',
title: '投诉类型',
width: 120,
},
{
field: 'name',
title: '投诉人姓名',
minWidth: 120,
},
{
field: 'contact',
title: '联系方式',
minWidth: 150,
},
{
field: 'content',
title: '投诉内容',
minWidth: 200,
showOverflow: 'tooltip',
},
{
cellRender: {
name: 'CellTag',
options: [
{ value: 'pending', color: 'warning', label: '待处理' },
{ value: 'processing', color: 'processing', label: '处理中' },
{ value: 'resolved', color: 'success', label: '已解决' },
{ value: 'closed', color: 'default', label: '已关闭' },
],
},
field: 'status',
title: '投诉状态',
width: 120,
},
{
field: 'status_description',
title: '状态描述',
minWidth: 150,
showOverflow: 'tooltip',
},
{
field: 'task_id',
title: '投诉单号',
minWidth: 150,
},
{
field: 'trade_no',
title: '交易单号',
minWidth: 180,
},
{
field: 'complain_amount',
title: '投诉金额',
width: 120,
formatter: ({ row }) => {
if (row.complain_amount) {
return `¥${parseFloat(row.complain_amount).toFixed(2)}`;
}
return '-';
},
},
{
field: 'order_id',
title: '关联订单ID',
minWidth: 150,
},
{
field: 'create_time',
title: '创建时间',
width: 180,
},
{
field: 'handle_time',
title: '处理时间',
width: 180,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '投诉ID',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
text: '查看详情',
},
{
code: 'update_status',
text: '更新状态',
},
{
code: 'update_remark',
text: '更新备注',
},
],
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 200,
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '支付宝投诉', value: 'alipay' },
{ label: '主动投诉', value: 'manual' },
],
},
fieldName: 'type',
label: '投诉类型',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已解决', value: 'resolved' },
{ label: '已关闭', value: 'closed' },
],
},
fieldName: 'status',
label: '投诉状态',
},
{
component: 'Input',
fieldName: 'name',
label: '投诉人姓名',
},
{
component: 'Input',
fieldName: 'contact',
label: '联系方式',
},
{
component: 'Input',
fieldName: 'order_id',
label: '关联订单ID',
},
{
component: 'RangePicker',
fieldName: 'create_time',
label: '创建时间',
},
{
component: 'RangePicker',
fieldName: 'handle_time',
label: '处理时间',
},
];
}

View File

@@ -0,0 +1,131 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { ComplaintApi } from '#/api/complaint';
import { ref } from 'vue';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getComplaintDetail,
getComplaintList,
updateComplaintRemark,
updateComplaintStatus,
} from '#/api/complaint';
import { useColumns, useGridFormSchema } from './data';
import DetailModal from './modules/detail-modal.vue';
import StatusModal from './modules/status-modal.vue';
import RemarkModal from './modules/remark-modal.vue';
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [
['create_time', ['create_time_start', 'create_time_end']],
['handle_time', ['handle_time_start', 'handle_time_end']],
],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getComplaintList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: true,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<ComplaintApi.Complaint>,
});
const detailData = ref<ComplaintApi.ComplaintDetail | null>(null);
const [DetailDrawer, detailDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
const [StatusModalInstance, statusModalApi] = useVbenModal({
connectedComponent: StatusModal,
destroyOnClose: true,
});
const [RemarkModalInstance, remarkModalApi] = useVbenModal({
connectedComponent: RemarkModal,
destroyOnClose: true,
});
function onActionClick(e: OnActionClickParams<ComplaintApi.Complaint>) {
switch (e.code) {
case 'detail': {
onViewDetail(e.row);
break;
}
case 'update_status': {
onUpdateStatus(e.row);
break;
}
case 'update_remark': {
onUpdateRemark(e.row);
break;
}
}
}
async function onViewDetail(row: ComplaintApi.Complaint) {
const detail = await getComplaintDetail(row.id);
detailData.value = detail;
detailDrawerApi.open();
}
function onUpdateStatus(row: ComplaintApi.Complaint) {
statusModalApi.setData(row).open();
}
function onUpdateRemark(row: ComplaintApi.Complaint) {
remarkModalApi.setData(row).open();
}
function onRefresh() {
gridApi.query();
}
function onStatusSuccess() {
onRefresh();
}
function onRemarkSuccess() {
onRefresh();
}
</script>
<template>
<Page auto-content-height>
<DetailDrawer>
<DetailModal :data="detailData || undefined" />
</DetailDrawer>
<StatusModalInstance @success="onStatusSuccess" />
<RemarkModalInstance @success="onRemarkSuccess" />
<Grid table-title="投诉列表" />
</Page>
</template>

View File

@@ -0,0 +1,289 @@
<script lang="ts" setup>
import type { ComplaintApi } from '#/api/complaint';
interface Props {
/**
* 外层抽屉通过 connectedComponent 传入的详情数据
*/
data?: ComplaintApi.ComplaintDetail;
}
const props = defineProps<Props>();
/**
* 投诉状态字典映射
* 通用状态pending=待处理processing=处理中resolved=已解决closed=已关闭
* 支付宝状态WAIT_PROCESS=待处理 等(优先使用 status_description如果没有则映射
*/
function getStatusText(status: string): string {
if (!status) return '-';
// 通用状态映射
const statusMap: Record<string, string> = {
pending: '待处理',
processing: '处理中',
resolved: '已解决',
closed: '已关闭',
// 支付宝常见状态(如果后端返回的是支付宝状态值)
WAIT_PROCESS: '待处理',
PROCESSING: '处理中',
RESOLVED: '已解决',
CLOSED: '已关闭',
};
return statusMap[status] || status;
}
</script>
<template>
<!-- 作为 connectedComponent 的内容组件只负责渲染不再自己创建 Drawer -->
<div v-if="props.data" class="complaint-detail">
<!-- 基础信息 -->
<section class="block">
<h3 class="block-title">基础信息</h3>
<div class="row">
<span class="label">投诉ID</span>
<span class="value">{{ props.data.id }}</span>
</div>
<div class="row">
<span class="label">投诉类型</span>
<span class="value">
{{ props.data.type === 'alipay' ? '支付宝投诉' : '主动投诉' }}
</span>
</div>
<div class="row">
<span class="label">投诉人姓名</span>
<span class="value">{{ props.data.name || '-' }}</span>
</div>
<div class="row">
<span class="label">联系方式</span>
<span class="value">{{ props.data.contact || '-' }}</span>
</div>
<div class="row">
<span class="label">投诉内容</span>
<span class="value">{{ props.data.content || '-' }}</span>
</div>
<div class="row">
<span class="label">投诉状态</span>
<span class="value">
{{ getStatusText(props.data.status) }}
</span>
</div>
<div class="row">
<span class="label">状态描述</span>
<span class="value">{{ props.data.status_description || '-' }}</span>
</div>
<div class="row">
<span class="label">处理备注</span>
<span class="value">{{ props.data.remark || '-' }}</span>
</div>
<div class="row">
<span class="label">关联订单ID</span>
<span class="value">{{ props.data.order_id || '-' }}</span>
</div>
<div class="row">
<span class="label">创建时间</span>
<span class="value">{{ props.data.create_time }}</span>
</div>
<div class="row">
<span class="label">处理时间</span>
<span class="value">{{ props.data.handle_time || '-' }}</span>
</div>
<div class="row">
<span class="label">更新时间</span>
<span class="value">{{ props.data.update_time }}</span>
</div>
</section>
<!-- 支付宝投诉详情 -->
<section v-if="props.data.type === 'alipay' && props.data.alipay_complaint" class="block">
<h3 class="block-title">支付宝投诉详情</h3>
<div class="row">
<span class="label">投诉单号</span>
<span class="value">{{ props.data.alipay_complaint.task_id }}</span>
</div>
<div class="row">
<span class="label">支付宝ID</span>
<span class="value">{{ props.data.alipay_complaint.alipay_id }}</span>
</div>
<div class="row">
<span class="label">被投诉人PID</span>
<span class="value">{{ props.data.alipay_complaint.opposite_pid || '-' }}</span>
</div>
<div class="row">
<span class="label">被投诉方名称</span>
<span class="value">{{ props.data.alipay_complaint.opposite_name || '-' }}</span>
</div>
<div class="row">
<span class="label">投诉金额</span>
<span class="value">
¥{{ parseFloat(props.data.alipay_complaint.complain_amount || '0').toFixed(2) }}
</span>
</div>
<div class="row">
<span class="label">交易单号</span>
<span class="value">{{ props.data.alipay_complaint.trade_no || '-' }}</span>
</div>
<div class="row">
<span class="label">投诉时间</span>
<span class="value">{{ props.data.alipay_complaint.gmt_complain || '-' }}</span>
</div>
<div class="row">
<span class="label">处理时间</span>
<span class="value">{{ props.data.alipay_complaint.gmt_process || '-' }}</span>
</div>
<div class="row">
<span class="label">投诉网址</span>
<span class="value">
<a v-if="props.data.alipay_complaint.complain_url" :href="props.data.alipay_complaint.complain_url"
target="_blank">
{{ props.data.alipay_complaint.complain_url }}
</a>
<span v-else>-</span>
</span>
</div>
<!-- 交易信息列表 -->
<div v-if="props.data.alipay_complaint.trade_info_list?.length" class="trade-table">
<div class="table-title">交易信息</div>
<table>
<thead>
<tr>
<th>交易单号</th>
<th>商家订单号</th>
<th>交易时间</th>
<th>退款时间</th>
<th>金额</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="item in props.data.alipay_complaint.trade_info_list" :key="item.id">
<td>{{ item.trade_no }}</td>
<td>{{ item.out_no }}</td>
<td>{{ item.gmt_trade }}</td>
<td>{{ item.gmt_refund }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.status }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 主动投诉详情 -->
<section v-if="props.data.type === 'manual' && props.data.manual_complaint" class="block">
<h3 class="block-title">主动投诉详情</h3>
<div class="row">
<span class="label">投诉主题</span>
<span class="value">{{ props.data.manual_complaint.subject || '-' }}</span>
</div>
<div class="row">
<span class="label">优先级</span>
<span class="value">
{{
props.data.manual_complaint.priority === 'urgent'
? '紧急'
: props.data.manual_complaint.priority === 'high'
? '高'
: props.data.manual_complaint.priority === 'medium'
? '中'
: '低'
}}
</span>
</div>
<div class="row">
<span class="label">投诉来源</span>
<span class="value">
{{
props.data.manual_complaint.source === 'web'
? '网站'
: props.data.manual_complaint.source === 'phone'
? '电话'
: props.data.manual_complaint.source === 'email'
? '邮件'
: '其他'
}}
</span>
</div>
<div class="row">
<span class="label">用户ID</span>
<span class="value">{{ props.data.manual_complaint.user_id || '-' }}</span>
</div>
<div class="row">
<span class="label">附件</span>
<span class="value">
<template v-if="props.data.manual_complaint.attachment_urls?.length">
<a v-for="(url, index) in props.data.manual_complaint.attachment_urls" :key="index" :href="url"
target="_blank" class="mr-2">
附件{{ index + 1 }}
</a>
</template>
<span v-else>-</span>
</span>
</div>
</section>
</div>
</template>
<style scoped>
.complaint-detail {
padding: 16px;
}
.block {
margin-bottom: 16px;
padding: 12px 16px;
border: 1px solid #f0f0f0;
border-radius: 4px;
background-color: #fff;
}
.block-title {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
}
.row {
display: flex;
margin-bottom: 4px;
font-size: 13px;
}
.label {
flex: 0 0 90px;
color: #888;
}
.value {
flex: 1;
color: #333;
word-break: break-all;
}
.trade-table {
margin-top: 8px;
}
.trade-table .table-title {
margin-bottom: 4px;
font-size: 13px;
font-weight: 500;
}
.trade-table table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.trade-table th,
.trade-table td {
border: 1px solid #f0f0f0;
padding: 4px 6px;
text-align: left;
}
</style>

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { ComplaintApi } from '#/api/complaint';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { updateComplaintRemark } from '#/api/complaint';
interface Props {
data?: ComplaintApi.Complaint;
}
const props = defineProps<Props>();
const emit = defineEmits<{
success: [];
}>();
const [Modal, modalApi] = useVbenModal();
const formData = ref<ComplaintApi.UpdateRemarkRequest>({
remark: '',
});
const loading = ref(false);
watch(
() => props.data,
(data) => {
if (data) {
formData.value = {
remark: data.remark || '',
};
}
},
{ immediate: true },
);
async function handleSubmit() {
if (!props.data) return;
loading.value = true;
try {
await updateComplaintRemark(props.data.id, formData.value);
message.success('更新备注成功');
emit('success');
modalApi.close();
} catch (error) {
message.error('更新备注失败');
} finally {
loading.value = false;
}
}
defineExpose({
setData: (data: ComplaintApi.Complaint) => {
formData.value = {
remark: data.remark || '',
};
modalApi.open();
return modalApi;
},
open: () => modalApi.open(),
close: () => modalApi.close(),
});
</script>
<template>
<Modal title="更新投诉备注" width="600px">
<a-form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="处理备注">
<a-textarea v-model:value="formData.remark" :rows="5" placeholder="请输入处理备注" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="modalApi.close()">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleSubmit">
确定
</a-button>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import type { ComplaintApi } from '#/api/complaint';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { updateComplaintStatus } from '#/api/complaint';
interface Props {
data?: ComplaintApi.Complaint;
}
const props = defineProps<Props>();
const emit = defineEmits<{
success: [];
}>();
const [Modal, modalApi] = useVbenModal();
const formData = ref<ComplaintApi.UpdateStatusRequest>({
status: 'pending',
status_description: '',
handler_id: '',
});
const loading = ref(false);
watch(
() => props.data,
(data) => {
if (data) {
formData.value = {
status: (data.status as any) || 'pending',
status_description: data.status_description || '',
handler_id: data.handler_id || '',
};
}
},
{ immediate: true },
);
async function handleSubmit() {
if (!props.data) return;
loading.value = true;
try {
await updateComplaintStatus(props.data.id, formData.value);
message.success('更新状态成功');
emit('success');
modalApi.close();
} catch (error) {
message.error('更新状态失败');
} finally {
loading.value = false;
}
}
defineExpose({
setData: (data: ComplaintApi.Complaint) => {
formData.value = {
status: (data.status as any) || 'pending',
status_description: data.status_description || '',
handler_id: data.handler_id || '',
};
modalApi.open();
return modalApi;
},
open: () => modalApi.open(),
close: () => modalApi.close(),
});
</script>
<template>
<Modal title="更新投诉状态" width="600px">
<a-form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="投诉状态" required>
<a-select v-model:value="formData.status">
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="resolved">已解决</a-select-option>
<a-select-option value="closed">已关闭</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态描述">
<a-textarea v-model:value="formData.status_description" :rows="3" placeholder="请输入状态描述" />
</a-form-item>
<a-form-item label="处理人ID">
<a-input v-model:value="formData.handler_id" placeholder="请输入处理人ID" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="modalApi.close()">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleSubmit">
确定
</a-button>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,362 @@
<script lang="ts" setup>
import type { DashboardApi } from '#/api/dashboard';
interface Props {
profitStats?: DashboardApi.ProfitStatistics;
}
const props = defineProps<Props>();
// 格式化金额
const formatAmount = (amount: number) => {
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
</script>
<template>
<div v-if="profitStats" class="revenue-panel">
<div class="revenue-panel-header">
<h3 class="text-lg font-semibold text-gray-800">收入统计</h3>
</div>
<div class="revenue-panel-content">
<!-- 三个时间段的格子 -->
<div class="revenue-grid">
<!-- 今日 -->
<div class="revenue-card">
<div class="card-header">
<span class="card-title">今日</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.today_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.today_profit) }}
</span>
</div>
</div>
<div class="card-content">
<!-- 收入 -->
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.today_detail?.revenue || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.today_detail?.tax_income || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.today_detail?.revenue || 0) + (profitStats.today_detail?.tax_income ||
0)) }}
</span>
</div>
</div>
<!-- 成本 -->
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.today_detail?.commission || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.today_detail?.rebate || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.today_detail?.company_tax || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.today_detail?.api_cost || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.today_detail?.commission || 0) + (profitStats.today_detail?.rebate ||
0) + (profitStats.today_detail?.company_tax || 0) + (profitStats.today_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
<!-- 当月 -->
<div class="revenue-card">
<div class="card-header">
<span class="card-title">当月</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.month_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.month_profit) }}
</span>
</div>
</div>
<div class="card-content">
<!-- 收入 -->
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.month_detail?.revenue || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.month_detail?.tax_income || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.month_detail?.revenue || 0) + (profitStats.month_detail?.tax_income ||
0)) }}
</span>
</div>
</div>
<!-- 成本 -->
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.month_detail?.commission || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.month_detail?.rebate || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.month_detail?.company_tax || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.month_detail?.api_cost || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.month_detail?.commission || 0) + (profitStats.month_detail?.rebate ||
0) + (profitStats.month_detail?.company_tax || 0) + (profitStats.month_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
<!-- 总计 -->
<div class="revenue-card">
<div class="card-header">
<span class="card-title">总计</span>
<div class="card-profit">
<span class="profit-label">利润</span>
<span class="profit-value" :class="profitStats.total_profit >= 0 ? 'text-green-600' : 'text-red-600'">
¥{{ formatAmount(profitStats.total_profit) }}
</span>
</div>
</div>
<div class="card-content">
<!-- 收入 -->
<div class="detail-section">
<div class="section-title">收入</div>
<div class="detail-item">
<span class="detail-label">营收</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.total_detail?.revenue || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">提现收税</span>
<span class="detail-value text-green-600">
+¥{{ formatAmount(profitStats.total_detail?.tax_income || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">收入合计</span>
<span class="detail-value font-semibold text-green-600">
+¥{{ formatAmount((profitStats.total_detail?.revenue || 0) + (profitStats.total_detail?.tax_income ||
0)) }}
</span>
</div>
</div>
<!-- 成本 -->
<div class="detail-section">
<div class="section-title">成本</div>
<div class="detail-item">
<span class="detail-label">代理佣金</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.total_detail?.commission || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">代理返利</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.total_detail?.rebate || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">税务成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.total_detail?.company_tax || 0) }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">API调用成本</span>
<span class="detail-value text-red-600">
-¥{{ formatAmount(profitStats.total_detail?.api_cost || 0) }}
</span>
</div>
<div class="detail-item total">
<span class="detail-label font-semibold">成本合计</span>
<span class="detail-value font-semibold text-red-600">
-¥{{ formatAmount((profitStats.total_detail?.commission || 0) + (profitStats.total_detail?.rebate ||
0) + (profitStats.total_detail?.company_tax || 0) + (profitStats.total_detail?.api_cost || 0)) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.revenue-panel {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.revenue-panel-header {
margin-bottom: 16px;
}
.revenue-panel-content {
.revenue-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
}
.revenue-card {
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
.card-header {
background: #fff;
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
.card-profit {
display: flex;
flex-direction: column;
align-items: flex-end;
.profit-label {
font-size: 11px;
color: #999;
margin-bottom: 2px;
}
.profit-value {
font-size: 16px;
font-weight: 600;
}
}
}
.card-content {
padding: 12px 16px;
.detail-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
&.total {
border-bottom: 2px solid #e0e0e0;
margin-top: 8px;
padding-top: 10px;
}
.detail-label {
color: #666;
font-size: 12px;
}
.detail-value {
font-size: 12px;
font-weight: 500;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,113 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2%',
},
series: [
{
areaStyle: {},
data: values,
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
name: '营收金额',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
return `${param.name}<br/>${param.seriesName}: ¥${param.value.toFixed(2)}`;
},
},
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category',
},
yAxis: [
{
axisTick: {
show: false,
},
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (value >= 10000) {
return `${(value / 10000).toFixed(1)}`;
}
return value.toString();
},
},
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -1,14 +1,28 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
@@ -20,17 +34,13 @@ onMounted(() => {
series: [
{
areaStyle: {},
data: [
120, 300, 500, 800, 1200, 1800, 2500, 3000, 2800, 2600, 2400, 2200,
2000, 1800, 1600, 1400, 1200, 1000, 800, 600, 400, 200, 100, 50, 30,
20, 10, 5, 2, 1,
],
data: values,
itemStyle: {
color: '#5ab1ef',
},
smooth: true,
type: 'line',
name: '访问量',
name: '订单数',
},
],
tooltip: {
@@ -47,9 +57,7 @@ onMounted(() => {
show: false,
},
boundaryGap: false,
data: Array.from({ length: 30 }).map(
(_item, index) => `Day ${index + 1}`,
),
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
@@ -64,7 +72,6 @@ onMounted(() => {
axisTick: {
show: false,
},
max: 3000,
splitArea: {
show: true,
},
@@ -73,6 +80,18 @@ onMounted(() => {
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>

View File

@@ -1,102 +1,207 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import { onMounted, ref } from 'vue';
import {
AnalysisChartCard,
AnalysisChartsTabs,
AnalysisOverview,
} from '@vben/common-ui';
import {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { message } from 'ant-design-vue';
import { getDashboardStatistics } from '#/api/dashboard';
import type { DashboardApi } from '#/api/dashboard';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsRevenueTrend from './analytics-revenue-trend.vue';
import AnalyticsProfitPanel from './analytics-profit-panel.vue';
const overviewItems: AnalysisOverviewItem[] = [
{
icon: SvgCardIcon,
title: '平台用户数',
totalTitle: '总用户数',
totalValue: 120_000,
value: 2000,
},
{
icon: SvgCakeIcon,
title: '推广访问量',
totalTitle: '总推广访问量',
totalValue: 500_000,
value: 20_000,
},
{
icon: SvgDownloadIcon,
title: '产品数量',
totalTitle: '总产品数量',
totalValue: 120,
value: 8,
},
{
icon: SvgBellIcon,
title: '代理数量',
totalTitle: '总代理数量',
totalValue: 5000,
value: 500,
},
];
const loading = ref(false);
const statistics = ref<DashboardApi.DashboardStatistics | null>(null);
// 格式化金额
const formatAmount = (amount: number) => {
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
// 格式化数字
const formatNumber = (num: number) => {
if (num >= 10000) {
return `${(num / 10000).toFixed(2)}`;
}
return num.toString();
};
// 加载统计数据
const loadStatistics = async () => {
loading.value = true;
try {
const data = await getDashboardStatistics();
statistics.value = data;
} catch (error) {
message.error('加载统计数据失败');
} finally {
loading.value = false;
}
};
const chartTabs: TabOption[] = [
{
label: '推广访问趋势',
value: 'trends',
label: '订单趋势',
value: 'order',
},
{
label: '订单趋势',
value: 'visits',
label: '营收趋势',
value: 'revenue',
},
];
onMounted(() => {
loadStatistics();
});
</script>
<template>
<div class="p-5">
<div class="mb-4 ml-4 text-lg text-gray-500">
该数据为演示模拟生成不为真实数据
</div>
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<div class="p-5 dashboard-wrapper">
<a-spin :spinning="loading">
<div v-if="statistics" class="dashboard-container">
<!-- 左侧统计卡片和图表 -->
<div class="dashboard-main">
<!-- 合并后的统计卡片 -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- 订单统计卡片 -->
<AnalysisChartCard title="订单统计">
<div class="py-2">
<!-- 今日数据 -->
<div class="mb-3">
<div class="text-xs text-gray-500 mb-1">今日订单</div>
<div class="flex items-baseline justify-between">
<div class="text-3xl font-bold text-blue-600">
{{ statistics.order_stats.today_count }}<span
class="text-lg font-normal ml-1 text-gray-500"></span>
</div>
<div class="text-xs" v-if="statistics.order_stats.change_rate">
<span :class="statistics.order_stats.change_rate > 0 ? 'text-green-500' : 'text-red-500'">
{{ statistics.order_stats.change_rate > 0 ? '↑' : '↓' }} {{
Math.abs(statistics.order_stats.change_rate).toFixed(1) }}%
</span>
</div>
</div>
</div>
<!-- 当月和总计 - 两列布局 -->
<div class="grid grid-cols-2 gap-3 pt-3 border-t">
<div>
<div class="text-xs text-gray-500 mb-1">当月</div>
<div class="text-lg font-semibold text-gray-700">
{{ formatNumber(statistics.order_stats.month_count) }}<span
class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">总计</div>
<div class="text-lg font-semibold text-gray-700">
{{ formatNumber(statistics.order_stats.total_count) }}<span
class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
</div>
</div>
</AnalysisChartCard>
<div class="mt-5 w-full md:flex">
<AnalysisChartCard
class="mt-5 md:mr-4 md:mt-0 md:w-1/3"
title="推广数据分析"
>
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard
class="mt-5 md:mr-4 md:mt-0 md:w-1/3"
title="订单来源分析"
>
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard
class="mt-5 md:mt-0 md:w-1/3"
title="佣金/奖励/提现统计"
>
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
<!-- 代理统计卡片 -->
<AnalysisChartCard title="代理统计">
<div class="py-2">
<!-- 总数数据 -->
<div class="mb-3">
<div class="text-xs text-gray-500 mb-1">代理总数</div>
<div class="text-3xl font-bold text-purple-600">
{{ statistics.agent_stats.total_count }}<span
class="text-lg font-normal ml-1 text-gray-500"></span>
</div>
</div>
<!-- 新增数据 - 两列布局 -->
<div class="grid grid-cols-2 gap-3 pt-3 border-t">
<div>
<div class="text-xs text-gray-500 mb-1">今日新增</div>
<div class="text-lg font-semibold text-gray-700">
{{ statistics.agent_stats.today_new }}<span
class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">当月新增</div>
<div class="text-lg font-semibold text-gray-700">
{{ statistics.agent_stats.month_new }}<span
class="text-sm font-normal text-gray-500 ml-1"></span>
</div>
</div>
</div>
</div>
</AnalysisChartCard>
</div>
<!-- 趋势图表 -->
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #order>
<AnalyticsTrends :data="statistics.order_trend" />
</template>
<template #revenue>
<AnalyticsRevenueTrend :data="statistics.revenue_trend" />
</template>
</AnalysisChartsTabs>
</div>
<!-- 右侧收入统计面板 -->
<div class="dashboard-sidebar">
<AnalyticsProfitPanel :profit-stats="statistics.profit_stats" />
</div>
</div>
</a-spin>
</div>
</template>
<style scoped lang="less">
.dashboard-wrapper {
/* 让整个页面区域占满视口高度,方便右侧面板自适应 */
min-height: 100vh;
box-sizing: border-box;
}
.dashboard-container {
display: flex;
gap: 16px;
align-items: flex-start;
.dashboard-main {
flex: 1;
min-width: 0;
}
.dashboard-sidebar {
/* 右侧面板根据视口高度拉满,高度=视口高度-上下padding(假设24px) */
width: 800px;
flex-shrink: 0;
position: sticky;
top: 16px;
max-height: calc(100vh - 32px);
overflow: auto;
}
}
@media (max-width: 1600px) {
.dashboard-container {
flex-direction: column;
.dashboard-sidebar {
width: 100%;
position: static;
max-height: none;
overflow: visible;
}
}
}
</style>

View File

@@ -107,18 +107,6 @@ export function useColumns<T = OrderApi.Order>(
title: '退款时间',
width: 180,
},
{
cellRender: {
name: 'CellTag',
options: [
{ value: 0, color: 'default', label: '否' },
{ value: 1, color: 'success', label: '是' },
],
},
field: 'is_promotion',
title: '推广订单',
width: 100,
},
{
align: 'center',
cellRender: {
@@ -212,18 +200,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'status',
label: '支付状态',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
},
fieldName: 'is_promotion',
label: '推广订单',
},
{
component: 'RangePicker',
fieldName: 'create_time',

View File

@@ -22,7 +22,7 @@ import { getOrderQueryDetail } from '#/api/order/query';
const route = useRoute();
const router = useRouter();
const orderId = Number(route.params.id);
const orderId = route.params.id as string;
const loading = ref(false);
const queryDetail = ref<OrderQueryApi.QueryDetail>();
@@ -111,7 +111,9 @@ onMounted(() => {
<div class="p-4">
<div class="mb-4 flex items-center">
<Button @click="handleBack">
<template #icon><MdiArrowLeft /></template>
<template #icon>
<MdiArrowLeft />
</template>
返回订单管理
</Button>
</div>
@@ -122,10 +124,7 @@ onMounted(() => {
<span class="text-lg font-medium">订单查询详情</span>
<div class="flex items-center gap-2">
<span class="text-gray-500">查询状态:</span>
<Tag
v-if="queryDetail"
:color="getQueryStateConfig(queryDetail.query_state).color"
>
<Tag v-if="queryDetail" :color="getQueryStateConfig(queryDetail.query_state).color">
{{ getQueryStateConfig(queryDetail.query_state).label }}
</Tag>
</div>
@@ -160,11 +159,8 @@ onMounted(() => {
</template>
<template v-if="queryDetail.query_params">
<Descriptions :column="2" bordered>
<Descriptions.Item
v-for="(value, key) in queryDetail.query_params"
:key="key"
:label="getFieldDisplayName(key)"
>
<Descriptions.Item v-for="(value, key) in queryDetail.query_params" :key="key"
:label="getFieldDisplayName(key)">
{{ value }}
</Descriptions.Item>
</Descriptions>
@@ -177,30 +173,21 @@ onMounted(() => {
<span class="text-lg font-medium">查询数据</span>
</template>
<template v-if="queryDetail.query_data?.length">
<Collapse
:default-active-key="
queryDetail.query_data.map((_, index) => index)
"
>
<Collapse.Panel
v-for="(item, index) in queryDetail.query_data"
:key="index"
>
<Collapse :default-active-key="queryDetail.query_data.map((_, index) => index)
">
<Collapse.Panel v-for="(item, index) in queryDetail.query_data" :key="index">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-lg font-medium">{{
item.feature.featureName
}}</span>
}}</span>
<Tag color="blue">API: {{ item.data.apiID }}</Tag>
</div>
<Tag
:color="
String(item.data.success) === 'true'
? 'success'
: 'error'
"
>
<Tag :color="String(item.data.success) === 'true'
? 'success'
: 'error'
">
{{
String(item.data.success) === 'true'
? '查询成功'
@@ -213,13 +200,7 @@ onMounted(() => {
<div class="grid gap-4">
<div v-if="item.data.data">
<div class="mb-2 font-medium">查询结果:</div>
<JsonViewer
:value="item.data.data"
copyable
:expand-depth="2"
boxed
@copied="handleCopied"
/>
<JsonViewer :value="item.data.data" copyable :expand-depth="2" boxed @copied="handleCopied" />
</div>
<div class="text-gray-500">
查询时间: {{ item.data.timestamp }}

View File

@@ -17,6 +17,39 @@ export function useFormSchema(): VbenFormSchema[] {
label: '描述',
rules: 'required',
},
{
component: 'Switch',
fieldName: 'no_offline',
label: '不支持下架',
defaultValue: false,
help: '勾选后该模块不开放下架功能,提交时白名单价格传 -1',
},
{
component: 'InputNumber',
fieldName: 'whitelist_price',
label: '白名单屏蔽价格(元)',
componentProps: {
min: 0,
precision: 2,
placeholder: '0=免费下架,>0=付费下架;勾选「不支持下架」时此项忽略',
},
dependencies: {
triggerFields: ['no_offline'],
if(values) {
return !values?.no_offline;
},
},
},
{
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
},
fieldName: 'cost_price',
label: '成本价(元)',
rules: 'required',
},
];
}
@@ -51,6 +84,28 @@ export function useColumns<T = FeatureApi.FeatureItem>(
title: '描述',
minWidth: 200,
},
{
field: 'whitelist_price',
title: '白名单屏蔽价格(元)',
minWidth: 150,
cellRender: {
name: 'VxeCellRender',
props: {
render: ({ row }: { row: FeatureApi.FeatureItem }) => {
const price = (row as FeatureApi.FeatureItem).whitelist_price ?? 0;
if (price < 0) return '不支持下架';
if (price === 0) return '免费下架';
return `¥${price.toFixed(2)}`;
},
},
},
},
{
field: 'cost_price',
formatter: ({ cellValue }) => `¥${(cellValue || 0).toFixed(2)}`,
title: '成本价(元)',
minWidth: 120,
},
{
field: 'create_time',
title: '创建时间',

View File

@@ -24,7 +24,12 @@ const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
const values = await formApi.getValues() as Record<string, unknown>;
// 「不支持下架」时给后端传 whitelist_price: -1
if (values.no_offline) {
values.whitelist_price = -1;
}
delete values.no_offline;
drawerApi.lock();
try {
await (id.value
@@ -43,7 +48,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
if (data) {
formData.value = data;
id.value = data.id;
formApi.setValues(data);
// 回显whitelist_price < 0 表示不支持下架,勾选「不支持下架」并隐藏价格输入
const noOffline = (data.whitelist_price ?? 0) < 0;
formApi.setValues({
...data,
no_offline: noOffline,
whitelist_price: noOffline ? 0 : (data.whitelist_price ?? 0),
});
} else {
id.value = undefined;
}

View File

@@ -28,16 +28,6 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'notes',
label: '备注',
},
{
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
},
fieldName: 'cost_price',
label: '成本价',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
@@ -86,12 +76,6 @@ export function useColumns<T = ProductApi.ProductItem>(
field: 'description',
title: '描述',
},
{
field: 'cost_price',
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
title: '成本价',
width: 120,
},
{
field: 'sell_price',
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,

View File

@@ -1,124 +0,0 @@
<script lang="ts" setup>
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { PromotionAnalyticsApi } from '#/api/promotion/analytics';
import { onMounted, ref } from 'vue';
import { AnalysisChartCard, AnalysisOverview } from '@vben/common-ui';
import { SvgCakeIcon, SvgCardIcon, SvgDownloadIcon } from '@vben/icons';
import { DatePicker } from 'ant-design-vue';
import dayjs from 'dayjs';
import { statsHistory, statsTotal } from '#/api/promotion/analytics';
import PromotionTrends from './promotion-trends.vue';
const overviewItems = ref<AnalysisOverviewItem[]>([
{
icon: SvgCardIcon,
title: '今日点击数',
totalTitle: '累计',
totalValue: 0,
value: 0,
},
{
icon: SvgCakeIcon,
title: '今日付费次数',
totalTitle: '累计',
totalValue: 0,
value: 0,
},
{
icon: SvgDownloadIcon,
title: '今日付费金额',
totalTitle: '累计',
totalValue: 0,
value: 0,
},
]);
const trendData = ref<PromotionAnalyticsApi.TrendData[]>([]);
const dateRange = ref<[dayjs.Dayjs, dayjs.Dayjs]>([
dayjs().subtract(7, 'day'),
dayjs(),
]);
const fetchOverview = async () => {
try {
const data = await statsTotal();
overviewItems.value = [
{
icon: SvgCardIcon,
title: '今日点击数',
totalTitle: '累计',
totalValue: data.total_click_count,
value: data.today_click_count,
decimals: 0,
},
{
icon: SvgCakeIcon,
title: '今日付费次数',
totalTitle: '累计',
totalValue: data.total_pay_count,
value: data.today_pay_count,
decimals: 0,
},
{
icon: SvgDownloadIcon,
title: '今日付费金额',
totalTitle: '累计',
totalValue: data.total_pay_amount,
value: data.today_pay_amount,
decimals: 2,
},
];
} catch (error) {
console.error('获取概览数据失败:', error);
}
};
const fetchTrendData = async () => {
try {
const data = await statsHistory({
start_date: dateRange.value[0].format('YYYY-MM-DD'),
end_date: dateRange.value[1].format('YYYY-MM-DD'),
});
trendData.value = data;
} catch (error) {
console.error('获取趋势数据失败:', error);
}
};
const handleDateChange = () => {
fetchTrendData();
};
onMounted(() => {
fetchOverview();
fetchTrendData();
});
</script>
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<div class="mt-5">
<AnalysisChartCard title="数据趋势">
<div class="flex flex-col gap-4">
<div class="flex justify-end">
<DatePicker.RangePicker
v-model:value="dateRange"
@change="handleDateChange"
/>
</div>
<PromotionTrends type="count" :data="trendData" />
<PromotionTrends type="amount" :data="trendData" />
</div>
</AnalysisChartCard>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,92 +0,0 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { PromotionAnalyticsApi } from '#/api/promotion/analytics';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
const props = defineProps<{
data: PromotionAnalyticsApi.TrendData[];
}>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const renderChart = () => {
// 计算转化率数据
const totalClicks = props.data.reduce(
(sum, item) => sum + item.click_count,
0,
);
const totalPays = props.data.reduce((sum, item) => sum + item.pay_count, 0);
const totalAmount = props.data.reduce(
(sum, item) => sum + item.pay_amount,
0,
);
const conversionRate = totalClicks > 0 ? (totalPays / totalClicks) * 100 : 0;
const averageAmount = totalPays > 0 ? totalAmount / totalPays : 0;
renderEcharts({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}%',
},
series: [
{
name: '转化率',
type: 'funnel',
left: '10%',
top: 60,
bottom: 60,
width: '80%',
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside',
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid',
},
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1,
},
emphasis: {
label: {
fontSize: 20,
},
},
data: [
{ value: 100, name: '点击量' },
{ value: conversionRate, name: '付费转化率' },
{ value: averageAmount, name: '平均付费金额' },
],
},
],
});
};
watch(() => props.data, renderChart, { deep: true });
onMounted(() => {
renderChart();
});
</script>
<template>
<EchartsUI ref="chartRef" class="h-[400px]" />
</template>
<style scoped></style>

View File

@@ -1,146 +0,0 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { PromotionAnalyticsApi } from '#/api/promotion/analytics';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
const props = defineProps<{
data: PromotionAnalyticsApi.TrendData[];
type: 'amount' | 'count';
}>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const renderChart = () => {
const dates = props.data.map((item) => item.stats_date);
const clickCounts = props.data.map((item) => item.click_count);
const payCounts = props.data.map((item) => item.pay_count);
const payAmounts = props.data.map((item) => item.pay_amount);
renderEcharts({
grid: {
top: 60,
left: 50,
right: 50,
bottom: 50,
},
legend: {
data: props.type === 'count' ? ['点击数', '付费次数'] : ['付费金额'],
top: 20,
},
series:
props.type === 'count'
? [
{
name: '点击数',
type: 'line',
data: clickCounts,
smooth: true,
showSymbol: false,
itemStyle: {
color: '#5ab1ef',
},
},
{
name: '付费次数',
type: 'line',
data: payCounts,
smooth: true,
showSymbol: false,
itemStyle: {
color: '#019680',
},
},
]
: [
{
name: '付费金额',
type: 'line',
data: payAmounts,
smooth: true,
showSymbol: false,
itemStyle: {
color: '#b6a2de',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(182, 162, 222, 0.3)',
},
{
offset: 1,
color: 'rgba(182, 162, 222, 0.1)',
},
],
},
},
},
],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
xAxis: {
type: 'category',
data: dates,
axisLabel: {
formatter: (value: string) => value.slice(5), // 只显示月-日
},
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (props.type === 'amount') {
return `¥${value}`;
}
return value.toString();
},
},
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
},
});
};
watch(() => props.data, renderChart, { deep: true });
watch(() => props.type, renderChart);
onMounted(() => {
renderChart();
});
</script>
<template>
<EchartsUI ref="chartRef" class="h-[400px]" />
</template>
<style scoped></style>

View File

@@ -1,92 +0,0 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemRoleApi } from '#/api';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: 'required',
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '名称',
},
{
component: 'Input',
fieldName: 'url',
label: '链接',
},
];
}
export function useColumns<T = SystemRoleApi.SystemRole>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '名称',
minWidth: 120,
},
{
field: 'url',
title: '链接',
minWidth: 250,
slots: { default: 'url' },
},
{
field: 'click_count',
title: '累计点击数',
minWidth: 120,
},
{
field: 'pay_count',
title: '付费次数',
minWidth: 120,
},
{
field: 'pay_amount',
title: '付费金额',
minWidth: 120,
},
{
field: 'create_time',
title: '创建时间',
minWidth: 180,
},
{
field: 'last_click_time',
title: '最后点击时间',
minWidth: 180,
},
{
field: 'last_pay_time',
title: '最后付费时间',
minWidth: 180,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '名称',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 130,
},
];
}

View File

@@ -1,142 +0,0 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { PromotionLinkApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Copy, Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deletePromotionLink, getPromotionLinkList } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPromotionLinkList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<PromotionLinkApi.PromotionLink>,
});
function onActionClick(
e: OnActionClickParams<PromotionLinkApi.PromotionLinkItem>,
) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
function onEdit(row: PromotionLinkApi.PromotionLinkItem) {
formDrawerApi.setData(row).open();
}
function onDelete(row: PromotionLinkApi.PromotionLinkItem) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
deletePromotionLink(row.id.toString())
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(
() => {
message.success('复制成功');
},
() => {
message.error('复制失败');
},
);
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid table-title="链接列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
创建链接
</Button>
</template>
<template #url="{ row }">
<div class="flex items-center gap-2">
<span class="truncate">{{ row.url }}</span>
<Button
type="link"
size="small"
class="!p-0"
@click="copyToClipboard(row.url)"
>
<Copy class="size-4" />
</Button>
</div>
</template>
</Grid>
</Page>
</template>

View File

@@ -1,85 +0,0 @@
<script lang="ts" setup>
import type { PromotionLinkApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { createPromotionLink, updatePromotionLink } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const formData = ref<PromotionLinkApi.PromotionLinkItem>();
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<Omit<PromotionLinkApi.PromotionLinkItem, 'id'>>();
drawerApi.lock();
(id.value
? updatePromotionLink(id.value, values)
: createPromotionLink(values)
)
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<PromotionLinkApi.PromotionLinkItem>();
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
? $t('common.edit', $t('system.role.name'))
: $t('common.create', $t('system.role.name'));
});
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form />
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</style>

View File

@@ -15,9 +15,9 @@ const emits = defineEmits(['success']);
const loading = ref(false);
const allApiList = ref<any[]>([]);
const roleApiList = ref<any[]>([]);
const selectedApiIds = ref<number[]>([]);
const selectedApiIds = ref<string[]>([]);
const formData = ref<SystemRoleApi.SystemRoleItem>();
const roleId = ref<number>();
const roleId = ref<string>();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
@@ -141,11 +141,7 @@ const getDrawerTitle = computed(() => {
<div class="space-y-4">
<!-- 全选操作 -->
<div class="flex items-center gap-4 rounded-lg bg-gray-50 p-3">
<Checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
>
<Checkbox :checked="isAllSelected" :indeterminate="isIndeterminate" @change="toggleSelectAll">
全选
</Checkbox>
<span class="text-sm text-gray-500">
@@ -155,38 +151,29 @@ const getDrawerTitle = computed(() => {
<!-- API列表 -->
<div class="max-h-96 overflow-y-auto rounded-lg border">
<div
v-for="api in allApiList"
:key="api.api_id"
class="flex items-center gap-3 border-b p-3 last:border-b-0 hover:bg-gray-50"
>
<Checkbox
:checked="selectedApiIdsSet.has(api.api_id)"
@change="
(e) => {
if (e.target.checked) {
selectedApiIds.push(api.api_id);
} else {
const index = selectedApiIds.indexOf(api.api_id);
if (index > -1) {
selectedApiIds.splice(index, 1);
}
<div v-for="api in allApiList" :key="api.api_id"
class="flex items-center gap-3 border-b p-3 last:border-b-0 hover:bg-gray-50">
<Checkbox :checked="selectedApiIdsSet.has(api.api_id)" @change="
(e) => {
if (e.target.checked) {
selectedApiIds.push(api.api_id);
} else {
const index = selectedApiIds.indexOf(api.api_id);
if (index > -1) {
selectedApiIds.splice(index, 1);
}
}
"
/>
}
" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium">{{ api.api_name }}</span>
<span
class="rounded px-2 py-1 text-xs"
:class="{
'bg-blue-100 text-blue-800': api.method === 'GET',
'bg-green-100 text-green-800': api.method === 'POST',
'bg-orange-100 text-orange-800': api.method === 'PUT',
'bg-red-100 text-red-800': api.method === 'DELETE',
}"
>
<span class="rounded px-2 py-1 text-xs" :class="{
'bg-blue-100 text-blue-800': api.method === 'GET',
'bg-green-100 text-green-800': api.method === 'POST',
'bg-orange-100 text-orange-800': api.method === 'PUT',
'bg-red-100 text-red-800': api.method === 'DELETE',
}">
{{ api.method }}
</span>
</div>

View File

@@ -10,10 +10,8 @@ export default defineConfig(async () => {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
// target: 'http://localhost:8888/api',
// target: 'https://www.tianyuandb.com/api',
// target: 'https://www.zhinengcha.cn/api',
target: 'https://www.quannengcha./api',
target: 'http://localhost:8888/api',
// target: 'https://www.onecha.cn/api',
ws: true,
},
},

View File

@@ -0,0 +1,57 @@
import type { Recordable } from '@vben/types';
import { requestClient } from '#/api/request';
export namespace DashboardApi {
export interface OrderStatistics {
today_count: number;
month_count: number;
total_count: number;
yesterday_count: number;
change_rate: number;
}
export interface RevenueStatistics {
today_amount: number;
month_amount: number;
total_amount: number;
yesterday_amount: number;
change_rate: number;
}
export interface AgentStatistics {
total_count: number;
today_new: number;
month_new: number;
}
export interface ProfitStatistics {
today_profit: number;
month_profit: number;
total_profit: number;
today_profit_rate: number;
month_profit_rate: number;
total_profit_rate: number;
}
export interface TrendData {
date: string;
value: number;
}
export interface DashboardStatistics {
order_stats: OrderStatistics;
revenue_stats: RevenueStatistics;
agent_stats: AgentStatistics;
profit_stats: ProfitStatistics;
order_trend: TrendData[];
revenue_trend: TrendData[];
}
}
export async function getDashboardStatistics(): Promise<DashboardApi.DashboardStatistics> {
return await requestClient.get<DashboardApi.DashboardStatistics>(
'/api/v1/admin/dashboard/statistics',
);
}

View File

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

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2%',
},
series: [
{
areaStyle: {},
data: values,
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
width: 1,
},
},
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
return `${param.name}<br/>${param.seriesName}: ¥${param.value.toFixed(2)}`;
},
},
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
width: 1,
},
show: true,
},
type: 'category',
},
yAxis: [
{
axisTick: {
show: false,
},
splitArea: {
show: true,
},
splitNumber: 4,
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (value >= 10000) {
return `${(value / 10000).toFixed(1)}`;
}
return value.toString();
},
},
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -1,72 +1,62 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { DashboardApi } from '#/api/dashboard';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
interface Props {
data?: DashboardApi.TrendData[];
}
const props = defineProps<Props>();
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);
onMounted(() => {
const updateChart = () => {
if (!props.data || props.data.length === 0) {
return;
}
const dates = props.data.map((item) => item.date);
const values = props.data.map((item) => item.value);
renderEcharts({
grid: {
bottom: 0,
containLabel: true,
left: '1%',
right: '1%',
top: '2 %',
top: '2%',
},
series: [
{
areaStyle: {},
data: [
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
111,
],
data: values,
itemStyle: {
color: '#5ab1ef',
},
smooth: true,
type: 'line',
},
{
areaStyle: {},
data: [
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
],
itemStyle: {
color: '#019680',
},
smooth: true,
type: 'line',
},
],
tooltip: {
axisPointer: {
lineStyle: {
color: '#019680',
color: '#5ab1ef',
width: 1,
},
},
trigger: 'axis',
},
// xAxis: {
// axisTick: {
// show: false,
// },
// boundaryGap: false,
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
// type: 'category',
// },
xAxis: {
axisTick: {
show: false,
},
boundaryGap: false,
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
data: dates,
splitLine: {
lineStyle: {
type: 'solid',
@@ -81,7 +71,6 @@ onMounted(() => {
axisTick: {
show: false,
},
max: 80_000,
splitArea: {
show: true,
},
@@ -90,6 +79,18 @@ onMounted(() => {
},
],
});
};
watch(
() => props.data,
() => {
updateChart();
},
{ deep: true },
);
onMounted(() => {
updateChart();
});
</script>

View File

@@ -2,6 +2,8 @@
import type { AnalysisOverviewItem } from '@vben/common-ui';
import type { TabOption } from '@vben/types';
import { computed, onMounted, ref } from 'vue';
import {
AnalysisChartCard,
AnalysisChartsTabs,
@@ -13,78 +15,211 @@ import {
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
import { message } from 'ant-design-vue';
import { getDashboardStatistics } from '#/api/dashboard';
import type { DashboardApi } from '#/api/dashboard';
import AnalyticsTrends from './analytics-trends.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.vue';
import AnalyticsVisits from './analytics-visits.vue';
import AnalyticsRevenueTrend from './analytics-revenue-trend.vue';
const overviewItems: AnalysisOverviewItem[] = [
{
icon: SvgCardIcon,
title: '用户量',
totalTitle: '总用户量',
totalValue: 120_000,
value: 2000,
},
{
icon: SvgCakeIcon,
title: '访问量',
totalTitle: '总访问量',
totalValue: 500_000,
value: 20_000,
},
{
icon: SvgDownloadIcon,
title: '下载量',
totalTitle: '总下载量',
totalValue: 120_000,
value: 8000,
},
{
icon: SvgBellIcon,
title: '使用量',
totalTitle: '总使用量',
totalValue: 50_000,
value: 5000,
},
];
const loading = ref(false);
const statistics = ref<DashboardApi.DashboardStatistics | null>(null);
// 格式化金额
const formatAmount = (amount: number) => {
if (amount >= 10000) {
return `${(amount / 10000).toFixed(2)}`;
}
return amount.toFixed(2);
};
// 格式化数字
const formatNumber = (num: number) => {
if (num >= 10000) {
return `${(num / 10000).toFixed(2)}`;
}
return num.toString();
};
// 加载统计数据
const loadStatistics = async () => {
loading.value = true;
try {
const data = await getDashboardStatistics();
statistics.value = data;
} catch (error) {
message.error('加载统计数据失败');
} finally {
loading.value = false;
}
};
// 计算概览卡片数据
const overviewItems = computed<AnalysisOverviewItem[]>(() => {
if (!statistics.value) {
return [];
}
const { order_stats, revenue_stats, agent_stats, profit_stats } =
statistics.value;
return [
{
icon: SvgCardIcon,
title: '今日订单',
totalTitle: '总订单',
totalValue: order_stats.total_count,
value: order_stats.today_count,
suffix: order_stats.change_rate
? ` ${order_stats.change_rate > 0 ? '↑' : '↓'} ${Math.abs(
order_stats.change_rate,
).toFixed(1)}%`
: '',
},
{
icon: SvgCakeIcon,
title: '今日营收',
totalTitle: '总营收',
totalValue: revenue_stats.total_amount,
value: revenue_stats.today_amount,
suffix: revenue_stats.change_rate
? ` ${revenue_stats.change_rate > 0 ? '↑' : '↓'} ${Math.abs(
revenue_stats.change_rate,
).toFixed(1)}%`
: '',
},
{
icon: SvgBellIcon,
title: '代理总数',
totalTitle: '代理总数',
totalValue: agent_stats.total_count,
value: agent_stats.today_new,
suffix: ` 今日新增: ${agent_stats.today_new}`,
},
{
icon: SvgDownloadIcon,
title: '今日利润',
totalTitle: '总利润',
totalValue: profit_stats.total_profit,
value: profit_stats.today_profit,
suffix: ` 利润率: ${profit_stats.today_profit_rate.toFixed(1)}%`,
},
];
});
const chartTabs: TabOption[] = [
{
label: '流量趋势',
value: 'trends',
label: '订单趋势',
value: 'order',
},
{
label: '月访问量',
value: 'visits',
label: '营收趋势',
value: 'revenue',
},
];
onMounted(() => {
loadStatistics();
});
</script>
<template>
<div class="p-5">
<AnalysisOverview :items="overviewItems" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<AnalyticsTrends />
</template>
<template #visits>
<AnalyticsVisits />
</template>
</AnalysisChartsTabs>
<a-spin :spinning="loading">
<div v-if="statistics">
<!-- 统计卡片 -->
<AnalysisOverview :items="overviewItems" />
<div class="mt-5 w-full md:flex">
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
<AnalyticsVisitsData />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
<AnalyticsVisitsSales />
</AnalysisChartCard>
</div>
<!-- 趋势图表 -->
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #order>
<AnalyticsTrends :data="statistics.order_trend" />
</template>
<template #revenue>
<AnalyticsRevenueTrend :data="statistics.revenue_trend" />
</template>
</AnalysisChartsTabs>
<!-- 详细统计信息 -->
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 订单统计卡片 -->
<AnalysisChartCard title="订单统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日订单:</span>
<span class="font-semibold">{{ statistics.order_stats.today_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月订单:</span>
<span class="font-semibold">{{ formatNumber(statistics.order_stats.month_count) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总订单:</span>
<span class="font-semibold">{{ formatNumber(statistics.order_stats.total_count) }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 营收统计卡片 -->
<AnalysisChartCard title="营收统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.today_amount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.month_amount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总营收:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.revenue_stats.total_amount) }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 代理统计卡片 -->
<AnalysisChartCard title="代理统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">代理总数:</span>
<span class="font-semibold">{{ statistics.agent_stats.total_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">今日新增:</span>
<span class="font-semibold">{{ statistics.agent_stats.today_new }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月新增:</span>
<span class="font-semibold">{{ statistics.agent_stats.month_new }}</span>
</div>
</div>
</AnalysisChartCard>
<!-- 利润统计卡片 -->
<AnalysisChartCard title="利润统计">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">今日利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.today_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">当月利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.month_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">总利润:</span>
<span class="font-semibold">¥{{ formatAmount(statistics.profit_stats.total_profit) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">利润率:</span>
<span class="font-semibold">{{ statistics.profit_stats.total_profit_rate.toFixed(1) }}%</span>
</div>
</div>
</AnalysisChartCard>
</div>
</div>
</a-spin>
</div>
</template>

View File

@@ -0,0 +1,534 @@
# 代理管理菜单路由配置清单
本文档列出了重构后的代理管理菜单路由配置,用于配置到数据库的 `admin_menu` 表中。
## 菜单结构
```
代理管理 (父菜单)
├── 代理列表
├── 推广链接
├── 佣金记录
├── 返佣记录 (新增)
├── 升级记录 (新增)
├── 订单记录 (新增)
├── 提现记录
├── 邀请码管理 (新增)
├── 系统配置 (新增)
├── 实名认证 (新增)
└── 产品配置
```
## 数据库配置说明
**表名**: `admin_menu`
**字段说明**:
- `pid`: 父菜单ID0表示顶级菜单
- `name`: 路由名称Route Name
- `path`: 路由路径
- `component`: 组件路径(前端视图组件路径)
- `redirect`: 重定向路径(可选)
- `meta`: 元数据JSON字符串包含 `title`(标题)、`icon`(图标)、`order`(排序)
- `status`: 状态1=启用0=禁用)
- `type`: 菜单类型catalog=目录menu=菜单)
- `sort`: 排序号
---
## 1. 父菜单:代理管理
```json
{
"pid": 0,
"name": "Agent",
"path": "/agent",
"component": "",
"redirect": "",
"meta": "{\"title\":\"代理管理\",\"icon\":\"mdi:account-group\",\"order\":2000}",
"status": 1,
"type": "catalog",
"sort": 2000
}
```
**说明**:
- 父菜单ID设为 `0` 或插入后获取的实际ID
- `type` 使用字典值 `catalog`(目录类型)
- `component` 留空(因为这是父菜单,不直接渲染组件)
---
## 2. 子菜单配置
### 2.1 代理列表
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentList",
"path": "/agent/list",
"component": "/views/agent/agent-list/list",
"redirect": "",
"meta": "{\"title\":\"代理列表\",\"icon\":\"mdi:account-multiple\",\"order\":2001}",
"status": 1,
"type": "menu",
"sort": 2001
}
```
### 2.2 推广链接
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentLinks",
"path": "/agent/links",
"component": "/views/agent/agent-links/list",
"redirect": "",
"meta": "{\"title\":\"推广链接\",\"icon\":\"mdi:link-variant\",\"order\":2002}",
"status": 1,
"type": "menu",
"sort": 2002
}
```
### 2.3 佣金记录
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentCommission",
"path": "/agent/commission",
"component": "/views/agent/agent-commission/list",
"redirect": "",
"meta": "{\"title\":\"佣金记录\",\"icon\":\"mdi:cash-multiple\",\"order\":2003}",
"status": 1,
"type": "menu",
"sort": 2003
}
```
### 2.4 返佣记录 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentRebate",
"path": "/agent/rebate",
"component": "/views/agent/agent-rebate/list",
"redirect": "",
"meta": "{\"title\":\"返佣记录\",\"icon\":\"mdi:currency-usd\",\"order\":2004}",
"status": 1,
"type": "menu",
"sort": 2004
}
```
### 2.5 升级记录 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentUpgrade",
"path": "/agent/upgrade",
"component": "/views/agent/agent-upgrade/list",
"redirect": "",
"meta": "{\"title\":\"升级记录\",\"icon\":\"mdi:arrow-up-circle\",\"order\":2005}",
"status": 1,
"type": "menu",
"sort": 2005
}
```
### 2.6 订单记录 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentOrder",
"path": "/agent/order",
"component": "/views/agent/agent-order/list",
"redirect": "",
"meta": "{\"title\":\"订单记录\",\"icon\":\"mdi:package-variant\",\"order\":2006}",
"status": 1,
"type": "menu",
"sort": 2006
}
```
### 2.7 提现记录
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentWithdrawal",
"path": "/agent/withdrawal",
"component": "/views/agent/agent-withdrawal/list",
"redirect": "",
"meta": "{\"title\":\"提现记录\",\"icon\":\"mdi:bank-transfer-out\",\"order\":2007}",
"status": 1,
"type": "menu",
"sort": 2007
}
```
### 2.8 邀请码管理 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentInviteCode",
"path": "/agent/invite-code",
"component": "/views/agent/agent-invite-code/list",
"redirect": "",
"meta": "{\"title\":\"邀请码管理\",\"icon\":\"mdi:ticket-confirmation\",\"order\":2008}",
"status": 1,
"type": "menu",
"sort": 2008
}
```
### 2.9 系统配置 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentConfig",
"path": "/agent/config",
"component": "/views/agent/agent-config/list",
"redirect": "",
"meta": "{\"title\":\"系统配置\",\"icon\":\"mdi:cog\",\"order\":2009}",
"status": 1,
"type": "menu",
"sort": 2009
}
```
### 2.10 实名认证 (新增)
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentRealName",
"path": "/agent/real-name",
"component": "/views/agent/agent-real-name/list",
"redirect": "",
"meta": "{\"title\":\"实名认证\",\"icon\":\"mdi:account-check\",\"order\":2010}",
"status": 1,
"type": "menu",
"sort": 2010
}
```
### 2.11 产品配置
```json
{
"pid": "[父菜单ID - Agent的ID]",
"name": "AgentProductConfig",
"path": "/agent/product-config",
"component": "/views/agent/agent-product-config/list",
"redirect": "",
"meta": "{\"title\":\"产品配置\",\"icon\":\"mdi:package-variant-closed\",\"order\":2011}",
"status": 1,
"type": "menu",
"sort": 2011
}
```
---
## 3. 需要删除的旧菜单 (如果存在)
以下菜单应该从数据库中删除(已移除的功能):
1. **上级抽佣记录** - `AgentCommissionDeduction`
- 路径: `/agent/commission-deduction` 或类似路径
2. **平台抽佣记录** - `AgentPlatformDeduction`
- 路径: `/agent/platform-deduction` 或类似路径
3. **会员充值订单** - `MembershipRechargeOrder`
- 路径: `/agent/membership-recharge-order` 或类似路径
4. **会员配置** - `AgentMembershipConfig`
- 路径: `/agent/membership-config` 或类似路径
5. **奖励记录** - `AgentReward` (如果存在独立菜单)
- 路径: `/agent/reward` 或类似路径
- **注意**: 已被"返佣记录"替代
---
## 4. SQL 插入示例 (MySQL)
假设父菜单ID为 `100`(请根据实际情况替换):
```sql
-- 插入代理列表
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentList',
'/agent/list',
'/views/agent/agent-list/list',
'',
'{"title":"代理列表","icon":"mdi:account-multiple","order":2001}',
1,
'menu',
2001,
NOW(),
NOW(),
0,
1
);
-- 插入推广链接
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentLinks',
'/agent/links',
'/views/agent/agent-links/list',
'',
'{"title":"推广链接","icon":"mdi:link-variant","order":2002}',
1,
'menu',
2002,
NOW(),
NOW(),
0,
1
);
-- 插入佣金记录
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentCommission',
'/agent/commission',
'/views/agent/agent-commission/list',
'',
'{"title":"佣金记录","icon":"mdi:cash-multiple","order":2003}',
1,
'menu',
2003,
NOW(),
NOW(),
0,
1
);
-- 插入返佣记录 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentRebate',
'/agent/rebate',
'/views/agent/agent-rebate/list',
'',
'{"title":"返佣记录","icon":"mdi:currency-usd","order":2004}',
1,
'menu',
2004,
NOW(),
NOW(),
0,
1
);
-- 插入升级记录 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentUpgrade',
'/agent/upgrade',
'/views/agent/agent-upgrade/list',
'',
'{"title":"升级记录","icon":"mdi:arrow-up-circle","order":2005}',
1,
'menu',
2005,
NOW(),
NOW(),
0,
1
);
-- 插入订单记录 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentOrder',
'/agent/order',
'/views/agent/agent-order/list',
'',
'{"title":"订单记录","icon":"mdi:package-variant","order":2006}',
1,
'menu',
2006,
NOW(),
NOW(),
0,
1
);
-- 插入提现记录
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentWithdrawal',
'/agent/withdrawal',
'/views/agent/agent-withdrawal/list',
'',
'{"title":"提现记录","icon":"mdi:bank-transfer-out","order":2007}',
1,
'menu',
2007,
NOW(),
NOW(),
0,
1
);
-- 插入邀请码管理 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentInviteCode',
'/agent/invite-code',
'/views/agent/agent-invite-code/list',
'',
'{"title":"邀请码管理","icon":"mdi:ticket-confirmation","order":2008}',
1,
'menu',
2008,
NOW(),
NOW(),
0,
1
);
-- 插入系统配置 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentConfig',
'/agent/config',
'/views/agent/agent-config/list',
'',
'{"title":"系统配置","icon":"mdi:cog","order":2009}',
1,
'menu',
2009,
NOW(),
NOW(),
0,
1
);
-- 插入实名认证 (新增)
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentRealName',
'/agent/real-name',
'/views/agent/agent-real-name/list',
'',
'{"title":"实名认证","icon":"mdi:account-check","order":2010}',
1,
'menu',
2010,
NOW(),
NOW(),
0,
1
);
-- 插入产品配置
INSERT INTO `admin_menu` (`pid`, `name`, `path`, `component`, `redirect`, `meta`, `status`, `type`, `sort`, `create_time`, `update_time`, `del_state`, `version`)
VALUES (
100,
'AgentProductConfig',
'/agent/product-config',
'/views/agent/agent-product-config/list',
'',
'{"title":"产品配置","icon":"mdi:package-variant-closed","order":2011}',
1,
'menu',
2011,
NOW(),
NOW(),
0,
1
);
```
---
## 5. 配置步骤
1. **查找或创建父菜单**:
- 查询数据库中是否已存在 `name = 'Agent'``path = '/agent'` 的菜单
- 如果不存在,先插入父菜单(`pid = 0``type = 'catalog'`
- 记录父菜单的ID后续子菜单会使用
2. **删除旧菜单** (如果存在):
- 删除"上级抽佣记录"、"平台抽佣记录"、"会员充值订单"、"会员配置"等旧菜单
- 如果存在"奖励记录"独立菜单,也删除(已改为"返佣记录"
3. **更新现有菜单**:
- 检查并更新现有菜单的路径、组件路径等信息(如有变化)
4. **插入新菜单**:
- 按照上面的SQL示例插入新的菜单项
- 确保 `pid` 使用正确的父菜单ID
- 确保 `sort``meta.order` 保持一致的排序
5. **验证**:
- 登录后台管理系统,检查菜单是否正常显示
- 点击每个菜单项,确认路由跳转正常
---
## 6. 注意事项
1. **菜单类型 (type)**:
- 父菜单使用 `catalog`(目录类型)
- 子菜单使用 `menu`(菜单类型)
- 需要确保这些类型值在系统的字典表 `admin_menu_type` 中存在
2. **组件路径 (component)**:
- 路径格式为:`/views/agent/xxx/list`
- 前端会自动添加 `.vue` 后缀
3. **Meta字段格式**:
- 必须是有效的JSON字符串
- 包含 `title`(标题)、`icon`(图标)、`order`(排序)
4. **排序号 (sort)**:
- 建议与 `meta.order` 保持一致
- 数值越小,排序越靠前
5. **状态 (status)**:
- `1` = 启用
- `0` = 禁用
---
## 7. 快速检查清单
- [ ] 父菜单"代理管理"已存在或已创建
- [ ] 已删除旧的菜单项(上级抽佣、平台抽佣、会员充值订单、会员配置)
- [ ] 所有新菜单项已插入数据库
- [ ] 所有菜单项的 `pid` 正确指向父菜单ID
- [ ] 所有菜单项的 `component` 路径正确
- [ ] 所有菜单项的 `meta` JSON格式正确
- [ ] 菜单排序号sort设置合理
- [ ] 菜单状态status为启用1
---
**配置完成后,刷新后台管理系统页面,新的菜单结构就会生效。**