Compare commits
4 Commits
0668eea99b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f2194e08d0 | |||
| df6f91e512 | |||
| b6542ddbc3 | |||
| 3211cc32ce |
@@ -1,6 +0,0 @@
|
||||
echo Start running commit-msg hook...
|
||||
|
||||
# Check whether the git commit information is standardized
|
||||
pnpm exec commitlint --edit "$1"
|
||||
|
||||
echo Run commit-msg hook done.
|
||||
@@ -1,3 +0,0 @@
|
||||
# 每次 git pull 之后, 安装依赖
|
||||
|
||||
pnpm install
|
||||
@@ -1,7 +0,0 @@
|
||||
# update `.vscode/vben-admin.code-workspace` file
|
||||
pnpm vsh code-workspace --auto-commit
|
||||
|
||||
# Format and submit code according to lintstagedrc.js configuration
|
||||
pnpm exec lint-staged
|
||||
|
||||
echo Run pre-commit hook done.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,8 +409,9 @@ 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 +423,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 +437,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 +451,7 @@ async function getAgentProductionConfigList(
|
||||
params: AgentApi.GetAgentProductionConfigListParams,
|
||||
) {
|
||||
return requestClient.get<AgentApi.AgentProductionConfigList>(
|
||||
'/agent/agent-production-config/list',
|
||||
'/agent/product_config/list',
|
||||
{
|
||||
params,
|
||||
},
|
||||
@@ -403,19 +465,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 +512,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,
|
||||
};
|
||||
|
||||
160
apps/web-antd/src/api/complaint/complaint.ts
Normal file
160
apps/web-antd/src/api/complaint/complaint.ts
Normal 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 };
|
||||
|
||||
2
apps/web-antd/src/api/complaint/index.ts
Normal file
2
apps/web-antd/src/api/complaint/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './complaint';
|
||||
|
||||
74
apps/web-antd/src/api/dashboard/dashboard.ts
Normal file
74
apps/web-antd/src/api/dashboard/dashboard.ts
Normal 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',
|
||||
);
|
||||
}
|
||||
|
||||
2
apps/web-antd/src/api/dashboard/index.ts
Normal file
2
apps/web-antd/src/api/dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './dashboard';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './analytics';
|
||||
export * from './link';
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
117
apps/web-antd/src/router/routes/modules/agent.ts
Normal file
117
apps/web-antd/src/router/routes/modules/agent.ts
Normal 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;
|
||||
|
||||
17
apps/web-antd/src/router/routes/modules/complaint.ts
Normal file
17
apps/web-antd/src/router/routes/modules/complaint.ts
Normal 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
113
apps/web-antd/src/utils/agent.ts
Normal file
113
apps/web-antd/src/utils/agent.ts
Normal 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)}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
274
apps/web-antd/src/views/agent/agent-config/list.vue
Normal file
274
apps/web-antd/src/views/agent/agent-config/list.vue
Normal 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>
|
||||
109
apps/web-antd/src/views/agent/agent-invite-code/data.ts
Normal file
109
apps/web-antd/src/views/agent/agent-invite-code/data.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
175
apps/web-antd/src/views/agent/agent-invite-code/list.vue
Normal file
175
apps/web-antd/src/views/agent/agent-invite-code/list.vue
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
120
apps/web-antd/src/views/agent/agent-order/data.ts
Normal file
120
apps/web-antd/src/views/agent/agent-order/data.ts
Normal 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' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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' }, '可选'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
86
apps/web-antd/src/views/agent/agent-real-name/data.ts
Normal file
86
apps/web-antd/src/views/agent/agent-real-name/data.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
105
apps/web-antd/src/views/agent/agent-rebate/data.ts
Normal file
105
apps/web-antd/src/views/agent/agent-rebate/data.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
65
apps/web-antd/src/views/agent/agent-rebate/list.vue
Normal file
65
apps/web-antd/src/views/agent/agent-rebate/list.vue
Normal 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>
|
||||
|
||||
116
apps/web-antd/src/views/agent/agent-upgrade/data.ts
Normal file
116
apps/web-antd/src/views/agent/agent-upgrade/data.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
65
apps/web-antd/src/views/agent/agent-upgrade/list.vue
Normal file
65
apps/web-antd/src/views/agent/agent-upgrade/list.vue
Normal 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>
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
185
apps/web-antd/src/views/complaint/list/data.ts
Normal file
185
apps/web-antd/src/views/complaint/list/data.ts
Normal 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: '处理时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
131
apps/web-antd/src/views/complaint/list/index.vue
Normal file
131
apps/web-antd/src/views/complaint/list/index.vue
Normal 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>
|
||||
289
apps/web-antd/src/views/complaint/list/modules/detail-modal.vue
Normal file
289
apps/web-antd/src/views/complaint/list/modules/detail-modal.vue
Normal 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>
|
||||
@@ -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>
|
||||
100
apps/web-antd/src/views/complaint/list/modules/status-modal.vue
Normal file
100
apps/web-antd/src/views/complaint/list/modules/status-modal.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: '创建时间',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
57
playground/src/api/dashboard/dashboard.ts
Normal file
57
playground/src/api/dashboard/dashboard.ts
Normal 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',
|
||||
);
|
||||
}
|
||||
|
||||
2
playground/src/api/dashboard/index.ts
Normal file
2
playground/src/api/dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './dashboard';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
534
代理管理菜单路由配置清单.md
Normal file
534
代理管理菜单路由配置清单.md
Normal file
@@ -0,0 +1,534 @@
|
||||
# 代理管理菜单路由配置清单
|
||||
|
||||
本文档列出了重构后的代理管理菜单路由配置,用于配置到数据库的 `admin_menu` 表中。
|
||||
|
||||
## 菜单结构
|
||||
|
||||
```
|
||||
代理管理 (父菜单)
|
||||
├── 代理列表
|
||||
├── 推广链接
|
||||
├── 佣金记录
|
||||
├── 返佣记录 (新增)
|
||||
├── 升级记录 (新增)
|
||||
├── 订单记录 (新增)
|
||||
├── 提现记录
|
||||
├── 邀请码管理 (新增)
|
||||
├── 系统配置 (新增)
|
||||
├── 实名认证 (新增)
|
||||
└── 产品配置
|
||||
```
|
||||
|
||||
## 数据库配置说明
|
||||
|
||||
**表名**: `admin_menu`
|
||||
|
||||
**字段说明**:
|
||||
- `pid`: 父菜单ID(0表示顶级菜单)
|
||||
- `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)
|
||||
|
||||
---
|
||||
|
||||
**配置完成后,刷新后台管理系统页面,新的菜单结构就会生效。**
|
||||
|
||||
Reference in New Issue
Block a user