f
This commit is contained in:
@@ -62,14 +62,27 @@ setupVbenVxeTable({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
// 表格配置项可以用 cellRender: { name: 'CellLink' };未传 text 时默认展示当前列字段值
|
||||||
vxeUI.renderer.add('CellLink', {
|
vxeUI.renderer.add('CellLink', {
|
||||||
renderTableDefault(renderOpts) {
|
renderTableDefault(renderOpts, params) {
|
||||||
const { props } = renderOpts;
|
const { props } = renderOpts;
|
||||||
|
const { column, row } = params;
|
||||||
|
const raw =
|
||||||
|
props?.text !== undefined && props?.text !== null && props?.text !== ''
|
||||||
|
? props.text
|
||||||
|
: get(row, column.field);
|
||||||
|
const display =
|
||||||
|
raw === null || raw === undefined || raw === '' ? '—' : String(raw);
|
||||||
return h(
|
return h(
|
||||||
Button,
|
Button,
|
||||||
{ size: 'small', type: 'link' },
|
{
|
||||||
{ default: () => props?.text },
|
size: 'small',
|
||||||
|
type: 'link',
|
||||||
|
onClick: isFunction(props?.onClick)
|
||||||
|
? () => (props.onClick as (r: Recordable) => void)(row)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{ default: () => display },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -177,6 +190,17 @@ setupVbenVxeTable({
|
|||||||
})
|
})
|
||||||
.filter((opt) => opt.show !== false);
|
.filter((opt) => opt.show !== false);
|
||||||
|
|
||||||
|
function isOperationDisabled(opt: Recordable<any>, r: Recordable<any>) {
|
||||||
|
const d = opt.disabled;
|
||||||
|
if (d === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isFunction(d)) {
|
||||||
|
return !!d(r);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function renderBtn(opt: Recordable<any>, listen = true) {
|
function renderBtn(opt: Recordable<any>, listen = true) {
|
||||||
return h(
|
return h(
|
||||||
Button,
|
Button,
|
||||||
@@ -185,11 +209,15 @@ setupVbenVxeTable({
|
|||||||
...opt,
|
...opt,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
onClick: listen
|
onClick: listen
|
||||||
? () =>
|
? () => {
|
||||||
|
if (isOperationDisabled(opt, row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
attrs?.onClick?.({
|
attrs?.onClick?.({
|
||||||
code: opt.code,
|
code: opt.code,
|
||||||
row,
|
row,
|
||||||
})
|
});
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,6 +253,9 @@ setupVbenVxeTable({
|
|||||||
...opt,
|
...opt,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
|
if (isOperationDisabled(opt, row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
attrs?.onClick?.({
|
attrs?.onClick?.({
|
||||||
code: opt.code,
|
code: opt.code,
|
||||||
row,
|
row,
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { requestClient } from '#/api/request';
|
|||||||
|
|
||||||
export namespace AgentApi {
|
export namespace AgentApi {
|
||||||
export interface AgentListItem {
|
export interface AgentListItem {
|
||||||
id: number;
|
id: string | number;
|
||||||
user_id: number;
|
user_id: string | number;
|
||||||
|
level?: number;
|
||||||
|
agent_code?: number;
|
||||||
|
team_leader_id?: string;
|
||||||
level_name: string;
|
level_name: string;
|
||||||
region: string;
|
region: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
@@ -13,10 +16,17 @@ export namespace AgentApi {
|
|||||||
frozen_balance: number;
|
frozen_balance: number;
|
||||||
withdrawn_amount: number;
|
withdrawn_amount: number;
|
||||||
create_time: string;
|
create_time: string;
|
||||||
is_real_name_verified: boolean;
|
is_real_name_verified?: boolean;
|
||||||
|
/** 与后端 json 字段 is_real_name 一致 */
|
||||||
|
is_real_name?: boolean;
|
||||||
real_name: string;
|
real_name: string;
|
||||||
id_card: string;
|
id_card: string;
|
||||||
real_name_status: 'approved' | 'pending' | 'rejected';
|
real_name_status: 'approved' | 'pending' | 'rejected';
|
||||||
|
// 订单统计相关字段
|
||||||
|
total_orders?: number; // 订单总数
|
||||||
|
total_order_amount?: number; // 订单总金额
|
||||||
|
total_agent_profit?: number; // 代理总收益
|
||||||
|
subordinate_count?: number; // 下级数量
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentList {
|
export interface AgentList {
|
||||||
@@ -24,17 +34,34 @@ export namespace AgentApi {
|
|||||||
items: AgentListItem[];
|
items: AgentListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 后台代理团队树节点 */
|
||||||
|
export interface AgentTeamTreeNode {
|
||||||
|
id: string;
|
||||||
|
agent_code: number;
|
||||||
|
level: number;
|
||||||
|
level_name: string;
|
||||||
|
mobile: string;
|
||||||
|
is_anchor: boolean;
|
||||||
|
children?: AgentTeamTreeNode[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetAgentListParams {
|
export interface GetAgentListParams {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
|
agent_code?: string;
|
||||||
|
agent_id?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
parent_agent_id?: number;
|
parent_agent_id?: number;
|
||||||
|
level?: number;
|
||||||
|
team_leader_id?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
create_time_start?: string;
|
create_time_start?: string;
|
||||||
create_time_end?: string;
|
create_time_end?: string;
|
||||||
order_by?: string;
|
order_by?: string;
|
||||||
order_type?: 'asc' | 'desc';
|
order_type?: 'asc' | 'desc';
|
||||||
|
/** 实名姓名模糊筛选(须已三要素核验) */
|
||||||
|
real_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentLinkListItem {
|
export interface AgentLinkListItem {
|
||||||
@@ -54,15 +81,18 @@ export namespace AgentApi {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number;
|
agent_id?: number;
|
||||||
|
agent_code?: string;
|
||||||
product_name?: string;
|
product_name?: string;
|
||||||
link_identifier?: string;
|
link_identifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理佣金相关接口
|
// 代理佣金相关接口
|
||||||
export interface AgentCommissionListItem {
|
export interface AgentCommissionListItem {
|
||||||
id: number;
|
id: string | number;
|
||||||
agent_id: number;
|
agent_id: string | number;
|
||||||
order_id: number;
|
agent_code?: number;
|
||||||
|
order_id: string | number;
|
||||||
|
order_no: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
product_name: string;
|
product_name: string;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -78,7 +108,8 @@ export namespace AgentApi {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number;
|
agent_id?: number;
|
||||||
product_name?: string;
|
agent_code?: string;
|
||||||
|
order_no?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +140,7 @@ export namespace AgentApi {
|
|||||||
export interface AgentWithdrawalListItem {
|
export interface AgentWithdrawalListItem {
|
||||||
id: string;
|
id: string;
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
|
agent_code?: number;
|
||||||
withdraw_no: string;
|
withdraw_no: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
tax_amount: number;
|
tax_amount: number;
|
||||||
@@ -132,9 +164,12 @@ export namespace AgentApi {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number | string;
|
agent_id?: number | string;
|
||||||
status?: number;
|
agent_code?: string;
|
||||||
withdraw_no?: string;
|
withdraw_no?: string;
|
||||||
withdrawal_type?: number;
|
status?: number | string;
|
||||||
|
withdrawal_type?: number | string;
|
||||||
|
payee_account?: string;
|
||||||
|
payee_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuditWithdrawalParams {
|
export interface AuditWithdrawalParams {
|
||||||
@@ -313,11 +348,55 @@ export namespace AgentApi {
|
|||||||
items: T[];
|
items: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 后台代理订单列表行(与 AdminGetAgentOrderList 对齐) */
|
||||||
|
export interface AgentOrderListItem {
|
||||||
|
id: string;
|
||||||
|
agent_id: string;
|
||||||
|
agent_code: number;
|
||||||
|
order_id: string;
|
||||||
|
order_no: string;
|
||||||
|
product_id: string;
|
||||||
|
product_name: string;
|
||||||
|
order_amount: number;
|
||||||
|
set_price: number;
|
||||||
|
actual_base_price: number;
|
||||||
|
price_cost: number;
|
||||||
|
agent_profit: number;
|
||||||
|
process_status: number;
|
||||||
|
order_status: string;
|
||||||
|
create_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetAgentOrderListParams {
|
export interface GetAgentOrderListParams {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number | string;
|
agent_id?: number | string;
|
||||||
[key: string]: any;
|
agent_code?: string;
|
||||||
|
order_id?: string;
|
||||||
|
order_no?: string;
|
||||||
|
process_status?: number;
|
||||||
|
order_status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单笔订单分账(佣金 + 返佣) */
|
||||||
|
export interface AdminGetAgentOrderSettlementResp {
|
||||||
|
commissions: AgentCommissionListItem[];
|
||||||
|
rebates: AgentRebateListItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台返佣列表行 */
|
||||||
|
export interface AgentRebateListItem {
|
||||||
|
id: string;
|
||||||
|
agent_id: string;
|
||||||
|
agent_code: number;
|
||||||
|
source_agent_id: string;
|
||||||
|
source_agent_code: number;
|
||||||
|
order_id: string;
|
||||||
|
order_no: string;
|
||||||
|
rebate_type: number;
|
||||||
|
amount: number;
|
||||||
|
status: number;
|
||||||
|
create_time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAgentRealNameListParams {
|
export interface GetAgentRealNameListParams {
|
||||||
@@ -331,21 +410,28 @@ export namespace AgentApi {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number | string;
|
agent_id?: number | string;
|
||||||
[key: string]: any;
|
agent_code?: string;
|
||||||
|
source_agent_code?: string;
|
||||||
|
order_no?: string;
|
||||||
|
rebate_type?: number;
|
||||||
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAgentUpgradeListParams {
|
export interface GetAgentUpgradeListParams {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
agent_id?: number | string;
|
agent_id?: number | string;
|
||||||
[key: string]: any;
|
agent_code?: string;
|
||||||
|
upgrade_type?: number;
|
||||||
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetInviteCodeListParams {
|
export interface GetInviteCodeListParams {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
code?: string;
|
||||||
|
status?: number | string;
|
||||||
target_level?: number;
|
target_level?: number;
|
||||||
[key: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateDiamondInviteCodeParams {
|
export interface GenerateDiamondInviteCodeParams {
|
||||||
@@ -408,6 +494,16 @@ async function getAgentList(params: AgentApi.GetAgentListParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 后台:代理团队树(同钻石团队 + 邀请关系) */
|
||||||
|
async function getAgentTeamTree(agentId: string) {
|
||||||
|
return requestClient.get<{ root: AgentApi.AgentTeamTreeNode }>(
|
||||||
|
'/agent/team/tree',
|
||||||
|
{
|
||||||
|
params: { agent_id: agentId },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取代理推广链接列表
|
* 获取代理推广链接列表
|
||||||
*/
|
*/
|
||||||
@@ -602,12 +698,20 @@ async function getInviteCodeList(params: AgentApi.GetInviteCodeListParams) {
|
|||||||
* 获取代理订单列表(后台)
|
* 获取代理订单列表(后台)
|
||||||
*/
|
*/
|
||||||
async function getAgentOrderList(params: AgentApi.GetAgentOrderListParams) {
|
async function getAgentOrderList(params: AgentApi.GetAgentOrderListParams) {
|
||||||
return requestClient.get<AgentApi.AdminListResp<Record<string, any>>>(
|
return requestClient.get<AgentApi.AdminListResp<AgentApi.AgentOrderListItem>>(
|
||||||
'/agent/order/list',
|
'/agent/order/list',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单笔订单分账视图(单次请求,避免双接口重复鉴权) */
|
||||||
|
async function getAgentOrderSettlement(params: { order_no: string }) {
|
||||||
|
return requestClient.get<AgentApi.AdminGetAgentOrderSettlementResp>(
|
||||||
|
'/agent/order/settlement',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取代理实名认证列表(后台)
|
* 获取代理实名认证列表(后台)
|
||||||
*/
|
*/
|
||||||
@@ -624,7 +728,7 @@ async function getAgentRealNameList(
|
|||||||
* 获取代理返佣列表(后台)
|
* 获取代理返佣列表(后台)
|
||||||
*/
|
*/
|
||||||
async function getAgentRebateList(params: AgentApi.GetAgentRebateListParams) {
|
async function getAgentRebateList(params: AgentApi.GetAgentRebateListParams) {
|
||||||
return requestClient.get<AgentApi.AdminListResp<Record<string, any>>>(
|
return requestClient.get<AgentApi.AdminListResp<AgentApi.AgentRebateListItem>>(
|
||||||
'/agent/rebate/list',
|
'/agent/rebate/list',
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
@@ -659,8 +763,10 @@ export {
|
|||||||
getAgentConfig,
|
getAgentConfig,
|
||||||
getAgentLinkList,
|
getAgentLinkList,
|
||||||
getAgentList,
|
getAgentList,
|
||||||
|
getAgentTeamTree,
|
||||||
getAgentMembershipConfigList,
|
getAgentMembershipConfigList,
|
||||||
getAgentOrderList,
|
getAgentOrderList,
|
||||||
|
getAgentOrderSettlement,
|
||||||
getAgentPlatformDeductionList,
|
getAgentPlatformDeductionList,
|
||||||
getAgentProductionConfigList,
|
getAgentProductionConfigList,
|
||||||
getAgentRealNameList,
|
getAgentRealNameList,
|
||||||
|
|||||||
@@ -58,8 +58,24 @@ async function getNotificationList(params: Recordable<any>) {
|
|||||||
return requestClient.get<NotificationApi.NotificationList>(
|
return requestClient.get<NotificationApi.NotificationList>(
|
||||||
'/notification/list',
|
'/notification/list',
|
||||||
{
|
{
|
||||||
params,
|
params: {
|
||||||
|
...params,
|
||||||
|
_ts: Date.now(),
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Pragma: 'no-cache',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通知详情(按 ID)
|
||||||
|
*/
|
||||||
|
async function getNotificationDetail(id: number) {
|
||||||
|
return requestClient.get<NotificationApi.NotificationItem>(
|
||||||
|
`/notification/detail/${id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +116,7 @@ async function deleteNotification(id: number) {
|
|||||||
export {
|
export {
|
||||||
createNotification,
|
createNotification,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
|
getNotificationDetail,
|
||||||
getNotificationList,
|
getNotificationList,
|
||||||
updateNotification,
|
updateNotification,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export namespace OrderApi {
|
|||||||
pay_time: null | string;
|
pay_time: null | string;
|
||||||
refund_time: null | string;
|
refund_time: null | string;
|
||||||
update_time: string;
|
update_time: string;
|
||||||
|
// 代理相关字段
|
||||||
|
is_agent_order: boolean; // 是否是代理订单
|
||||||
|
agent_code?: string; // 代理编号
|
||||||
|
agent_process_status?: string; // 代理处理状态
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderList {
|
export interface OrderList {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export namespace ProductApi {
|
|||||||
product_en: string;
|
product_en: string;
|
||||||
description: string;
|
description: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
cost_price: number;
|
|
||||||
sell_price: number;
|
sell_price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ export namespace ProductApi {
|
|||||||
product_en?: string;
|
product_en?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
cost_price?: number;
|
|
||||||
sell_price?: number;
|
sell_price?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +42,8 @@ export namespace ProductApi {
|
|||||||
feature_id: number;
|
feature_id: number;
|
||||||
api_id: string;
|
api_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** 模块成本(元),来自 feature,用于产品侧展示与汇总 */
|
||||||
|
cost_price: number;
|
||||||
sort: number;
|
sort: number;
|
||||||
enable: number;
|
enable: number;
|
||||||
is_important: number;
|
is_important: number;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ async function deleteUser(id: string) {
|
|||||||
* @param data.password 新密码
|
* @param data.password 新密码
|
||||||
*/
|
*/
|
||||||
async function resetPassword(id: string, data: { password: string }) {
|
async function resetPassword(id: string, data: { password: string }) {
|
||||||
return requestClient.post(`/reset-password/${id}`, data);
|
return requestClient.put(`/user/reset-password/${id}`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createUser, deleteUser, getUserList, resetPassword, updateUser };
|
export { createUser, deleteUser, getUserList, resetPassword, updateUser };
|
||||||
|
|||||||
121
apps/web-antd/src/composables/use-agent-list-navigate.ts
Normal file
121
apps/web-antd/src/composables/use-agent-list-navigate.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
/** 跨页进入代理列表时的一次性定位数据(不写 URL,避免重置后仍被 query 合并) */
|
||||||
|
export const AGENT_LIST_FOCUS_STORAGE_KEY = 'qnc_admin_agent_list_focus_v1';
|
||||||
|
|
||||||
|
export const AGENT_LIST_FOCUS_EVENT = 'qnc:agent-list-focus';
|
||||||
|
|
||||||
|
/** 旧版曾把面包屑栈写入 sessionStorage;已不再持久化,进入列表时应清除以免把 team_leader_id 写回 URL */
|
||||||
|
export const AGENT_LIST_NAV_STACK_KEY = 'qnc_admin_agent_list_nav_stack_v1';
|
||||||
|
|
||||||
|
export type AgentListNavEntry =
|
||||||
|
| { kind: 'full' }
|
||||||
|
| { kind: 'subs'; leaderId: string; leaderCode?: number }
|
||||||
|
| { kind: 'focus'; agentId?: string; agentCode?: number };
|
||||||
|
|
||||||
|
export type AgentListFocusPayload = {
|
||||||
|
agent_code?: string;
|
||||||
|
agent_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 清除历史上保存在 sessionStorage 的导航栈(避免新开/重进列表仍恢复下级视图 URL) */
|
||||||
|
export function clearStaleAgentListNavStackStorage() {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(AGENT_LIST_NAV_STACK_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从定位 payload 生成导航栈节点(用于面包屑) */
|
||||||
|
export function navEntryFromFocusPayload(
|
||||||
|
p: AgentListFocusPayload,
|
||||||
|
): AgentListNavEntry | null {
|
||||||
|
const agentId =
|
||||||
|
p.agent_id != null && String(p.agent_id).trim() !== ''
|
||||||
|
? String(p.agent_id)
|
||||||
|
: undefined;
|
||||||
|
let agentCode: number | undefined;
|
||||||
|
if (p.agent_code != null && String(p.agent_code).trim() !== '') {
|
||||||
|
const n = Number(p.agent_code);
|
||||||
|
if (!Number.isNaN(n)) {
|
||||||
|
agentCode = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!agentId && agentCode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { kind: 'focus', agentId, agentCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(opts: {
|
||||||
|
agentCode?: string | number | null;
|
||||||
|
agentId?: string | null;
|
||||||
|
}): AgentListFocusPayload | null {
|
||||||
|
const payload: AgentListFocusPayload = {};
|
||||||
|
if (opts.agentCode != null && opts.agentCode !== '') {
|
||||||
|
payload.agent_code = String(opts.agentCode);
|
||||||
|
}
|
||||||
|
if (opts.agentId != null && opts.agentId !== '') {
|
||||||
|
payload.agent_id = String(opts.agentId);
|
||||||
|
}
|
||||||
|
if (!payload.agent_code && !payload.agent_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 sessionStorage 读取并清除一次性定位数据(用于代理列表页首次进入)。
|
||||||
|
*/
|
||||||
|
export function consumeAgentListFocusFromStorage(): AgentListFocusPayload | null {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = sessionStorage.getItem(AGENT_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(AGENT_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(raw) as AgentListFocusPayload;
|
||||||
|
if (!v || typeof v !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到代理列表并定位到指定代理。
|
||||||
|
* 不写 agent_code / agent_id 到 URL,避免点击「重置」后仍被地址栏参数合并进请求。
|
||||||
|
* 已在代理列表页内跳转时通过 CustomEvent 通知当前页应用定位。
|
||||||
|
*/
|
||||||
|
export function navigateToAgentList(
|
||||||
|
router: Router,
|
||||||
|
opts: { agentCode?: string | number | null; agentId?: string | null },
|
||||||
|
) {
|
||||||
|
const payload = buildPayload(opts);
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadyOnList = router.currentRoute.value.name === 'AgentList';
|
||||||
|
|
||||||
|
if (isAlreadyOnList) {
|
||||||
|
// 先同步写入 ephemeral,再清空 URL。否则仍带着 team_leader_id 时,第一次请求后 ephemeral 被清空,
|
||||||
|
// 后续请求会重新按 URL 筛「下级列表」,表现为「查看上级」后再点「查看下级」无效或列表错乱。
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(AGENT_LIST_FOCUS_EVENT, { detail: payload }),
|
||||||
|
);
|
||||||
|
void router.replace({ name: 'AgentList', query: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(AGENT_LIST_FOCUS_STORAGE_KEY, JSON.stringify(payload));
|
||||||
|
router.push({
|
||||||
|
name: 'AgentList',
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
64
apps/web-antd/src/composables/use-order-list-navigate.ts
Normal file
64
apps/web-antd/src/composables/use-order-list-navigate.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
/** 跨页进入订单列表时的一次性定位数据(不写 URL,避免重置后仍被 query 合并) */
|
||||||
|
export const ORDER_LIST_FOCUS_STORAGE_KEY = 'qnc_admin_order_list_focus_v1';
|
||||||
|
|
||||||
|
export const ORDER_LIST_FOCUS_EVENT = 'qnc:order-list-focus';
|
||||||
|
|
||||||
|
export type OrderListFocusPayload = {
|
||||||
|
order_no?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function consumeOrderListFocusFromStorage(): OrderListFocusPayload | null {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = sessionStorage.getItem(ORDER_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(ORDER_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(raw) as OrderListFocusPayload;
|
||||||
|
if (!v || typeof v !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到订单列表并按商户订单号筛选。
|
||||||
|
* 不写 order_no 到 URL,避免点击「重置」后仍被地址栏参数合并进请求。
|
||||||
|
* 已在订单列表页内跳转时通过 CustomEvent 通知当前页应用筛选。
|
||||||
|
*/
|
||||||
|
export function navigateToOrderList(
|
||||||
|
router: Router,
|
||||||
|
opts: { orderNo?: string | null },
|
||||||
|
) {
|
||||||
|
const orderNo = opts.orderNo != null ? String(opts.orderNo).trim() : '';
|
||||||
|
if (!orderNo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: OrderListFocusPayload = { order_no: orderNo };
|
||||||
|
const r = router.currentRoute.value;
|
||||||
|
const onOrderList =
|
||||||
|
r.name === 'Order' || r.path === '/order' || r.path.startsWith('/order?');
|
||||||
|
|
||||||
|
if (onOrderList) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(ORDER_LIST_FOCUS_EVENT, { detail: payload }),
|
||||||
|
);
|
||||||
|
void router.replace({ path: '/order', query: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(
|
||||||
|
ORDER_LIST_FOCUS_STORAGE_KEY,
|
||||||
|
JSON.stringify(payload),
|
||||||
|
);
|
||||||
|
router.push({ path: '/order', query: {} });
|
||||||
|
}
|
||||||
130
apps/web-antd/src/composables/use-platform-user-list-navigate.ts
Normal file
130
apps/web-antd/src/composables/use-platform-user-list-navigate.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
/** 跨页进入平台用户列表时的一次性筛选(不写 URL query,避免点「重置」后仍合并进请求) */
|
||||||
|
export const PLATFORM_USER_LIST_FOCUS_STORAGE_KEY =
|
||||||
|
'qnc_admin_platform_user_list_focus_v1';
|
||||||
|
|
||||||
|
export const PLATFORM_USER_LIST_FOCUS_EVENT =
|
||||||
|
'qnc:platform-user-list-focus';
|
||||||
|
|
||||||
|
export type PlatformUserListFocusPayload = {
|
||||||
|
user_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normPath(p: string) {
|
||||||
|
return p.replace(/\/+/g, '/').replace(/\/+$/, '') || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeScore(
|
||||||
|
r: Pick<RouteLocationNormalizedLoaded, 'path' | 'name'>,
|
||||||
|
): number {
|
||||||
|
const p = normPath(r.path || '');
|
||||||
|
const n = String(r.name ?? '');
|
||||||
|
if (p.includes('platform-user') && p.includes('list')) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (/platformuserlist/i.test(n)) {
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
if (/platform[-_]?user/i.test(p) && /list$/i.test(p)) {
|
||||||
|
return 80;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlatformUserListTarget(router: Router): {
|
||||||
|
name?: string | symbol;
|
||||||
|
path?: string;
|
||||||
|
} | null {
|
||||||
|
const routes = router.getRoutes();
|
||||||
|
let best: (typeof routes)[0] | null = null;
|
||||||
|
let bestScore = 0;
|
||||||
|
for (const r of routes) {
|
||||||
|
const s = routeScore(r);
|
||||||
|
if (s > bestScore) {
|
||||||
|
bestScore = s;
|
||||||
|
best = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best && bestScore >= 80) {
|
||||||
|
if (best.name != null && String(best.name) !== '') {
|
||||||
|
return { name: best.name };
|
||||||
|
}
|
||||||
|
return { path: best.path };
|
||||||
|
}
|
||||||
|
const fallbackPath = '/platform-user';
|
||||||
|
const fb = routes.find((r) => normPath(r.path || '') === normPath(fallbackPath));
|
||||||
|
if (fb?.name != null && String(fb.name) !== '') {
|
||||||
|
return { name: fb.name };
|
||||||
|
}
|
||||||
|
if (fb?.path) {
|
||||||
|
return { path: fb.path };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumePlatformUserListFocusFromStorage(): PlatformUserListFocusPayload | null {
|
||||||
|
if (typeof sessionStorage === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = sessionStorage.getItem(PLATFORM_USER_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(PLATFORM_USER_LIST_FOCUS_STORAGE_KEY);
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(raw) as PlatformUserListFocusPayload;
|
||||||
|
if (!v || typeof v !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到平台用户列表并按用户主键 ID 精确筛选。
|
||||||
|
*/
|
||||||
|
export function navigateToPlatformUserList(
|
||||||
|
router: Router,
|
||||||
|
opts: { userId: string | number | null | undefined },
|
||||||
|
) {
|
||||||
|
const userId = opts.userId != null ? String(opts.userId).trim() : '';
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: PlatformUserListFocusPayload = { user_id: userId };
|
||||||
|
const r = router.currentRoute.value;
|
||||||
|
const onPlatformUserList = routeScore(r) >= 80;
|
||||||
|
|
||||||
|
if (onPlatformUserList) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(PLATFORM_USER_LIST_FOCUS_EVENT, { detail: payload }),
|
||||||
|
);
|
||||||
|
void router.replace({ path: r.path, query: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolvePlatformUserListTarget(router);
|
||||||
|
if (!target) {
|
||||||
|
message.warning(
|
||||||
|
'未找到「平台用户」菜单路由,请确认后台菜单组件为 platform-user',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(
|
||||||
|
PLATFORM_USER_LIST_FOCUS_STORAGE_KEY,
|
||||||
|
JSON.stringify(payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (target.name != null && String(target.name) !== '') {
|
||||||
|
router.push({ name: target.name as string, query: {} });
|
||||||
|
} else if (target.path) {
|
||||||
|
router.push({ path: target.path, query: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,8 @@
|
|||||||
"setPermissions": "Set Permissions",
|
"setPermissions": "Set Permissions",
|
||||||
"createTime": "Create Time",
|
"createTime": "Create Time",
|
||||||
"operation": "Operation",
|
"operation": "Operation",
|
||||||
"resetPassword": "Reset Password",
|
"changePassword": "Change Password",
|
||||||
|
"changePasswordSuccess": "Password changed successfully",
|
||||||
"newPassword": "New Password",
|
"newPassword": "New Password",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Confirm Password",
|
||||||
"confirmPasswordRequired": "Please confirm password",
|
"confirmPasswordRequired": "Please confirm password",
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
"setPermissions": "设置权限",
|
"setPermissions": "设置权限",
|
||||||
"createTime": "创建时间",
|
"createTime": "创建时间",
|
||||||
"operation": "操作",
|
"operation": "操作",
|
||||||
"resetPassword": "重置密码",
|
"changePassword": "修改密码",
|
||||||
|
"changePasswordSuccess": "密码修改成功",
|
||||||
"newPassword": "新密码",
|
"newPassword": "新密码",
|
||||||
"confirmPassword": "确认密码",
|
"confirmPassword": "确认密码",
|
||||||
"confirmPasswordRequired": "请确认密码",
|
"confirmPasswordRequired": "请确认密码",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/agent/commission',
|
path: '/agent/commission',
|
||||||
name: 'AgentCommission',
|
name: 'AgentCommission',
|
||||||
meta: {
|
meta: {
|
||||||
|
hideInMenu: true,
|
||||||
icon: 'mdi:cash-multiple',
|
icon: 'mdi:cash-multiple',
|
||||||
title: '佣金记录',
|
title: '佣金记录',
|
||||||
},
|
},
|
||||||
@@ -41,6 +42,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/agent/rebate',
|
path: '/agent/rebate',
|
||||||
name: 'AgentRebate',
|
name: 'AgentRebate',
|
||||||
meta: {
|
meta: {
|
||||||
|
hideInMenu: true,
|
||||||
icon: 'mdi:currency-usd',
|
icon: 'mdi:currency-usd',
|
||||||
title: '返佣记录',
|
title: '返佣记录',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,15 +22,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.dashboard.analytics'),
|
title: $t('page.dashboard.analytics'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Workspace',
|
|
||||||
path: '/workspace',
|
|
||||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
|
||||||
meta: {
|
|
||||||
icon: 'carbon:workspace',
|
|
||||||
title: $t('page.dashboard.workspace'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function getOrderProcessStatusName(status: number): string {
|
|||||||
*/
|
*/
|
||||||
export function getUpgradeStatusName(status: number): string {
|
export function getUpgradeStatusName(status: number): string {
|
||||||
const map: Record<number, string> = {
|
const map: Record<number, string> = {
|
||||||
1: '待处理',
|
1: '待支付',
|
||||||
2: '已完成',
|
2: '已完成',
|
||||||
3: '已失败',
|
3: '已失败',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { About } from '@vben/common-ui';
|
|
||||||
|
|
||||||
defineOptions({ name: 'About' });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<About />
|
|
||||||
</template>
|
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
|
type OrderNoClickFn = (row: AgentApi.AgentCommissionListItem) => void;
|
||||||
|
|
||||||
// 佣金记录列表列配置
|
// 佣金记录列表列配置
|
||||||
export function useCommissionColumns(): VxeTableGridOptions['columns'] {
|
export function useCommissionColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentApi.AgentCommissionListItem) => void,
|
||||||
|
onOrderNoClick?: OrderNoClickFn,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_code',
|
||||||
title: '代理ID',
|
title: '代理编号',
|
||||||
width: 100,
|
width: 110,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'order_id',
|
field: 'order_no',
|
||||||
title: '订单ID',
|
title: '商户订单号',
|
||||||
width: 100,
|
minWidth: 180,
|
||||||
|
cellRender: onOrderNoClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onOrderNoClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'amount',
|
field: 'amount',
|
||||||
@@ -54,8 +72,21 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'product_name',
|
fieldName: 'agent_code',
|
||||||
label: '产品名称',
|
label: '代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入代理编号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'order_no',
|
||||||
|
label: '商户订单号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入商户订单号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentCommissionList } from '#/api/agent';
|
import { getAgentCommissionList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
import { navigateToOrderList } from '#/composables/use-order-list-navigate';
|
||||||
|
|
||||||
import { useCommissionColumns, useCommissionFormSchema } from './data';
|
import { useCommissionColumns, useCommissionFormSchema } from './data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
|
/** 嵌套在弹窗内时,跳转到订单/代理列表后调用(用于关闭弹窗) */
|
||||||
|
navigateAway?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
@@ -20,29 +29,49 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onCommissionAgentCodeClick(row: AgentApi.AgentCommissionListItem) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOrderNoClick(row: AgentApi.AgentCommissionListItem) {
|
||||||
|
if (row.order_no) {
|
||||||
|
navigateToOrderList(router, { orderNo: row.order_no });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useCommissionFormSchema(),
|
schema: useCommissionFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useCommissionColumns(),
|
columns: useCommissionColumns(onCommissionAgentCodeClick, onOrderNoClick),
|
||||||
|
toolbarConfig: {
|
||||||
|
custom: true,
|
||||||
|
export: false,
|
||||||
|
refresh: false,
|
||||||
|
search: true,
|
||||||
|
zoom: true,
|
||||||
|
},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentCommissionList({
|
return await getAgentCommissionList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
@@ -53,7 +82,7 @@ const [Grid] = useVbenVxeGrid({
|
|||||||
total: 'total',
|
total: 'total',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<AgentApi.AgentCommissionListItem>,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AgentApi } from '#/api/agent';
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { h, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button, Card, Col, Form, InputNumber, Row, Space, message } from 'ant-design-vue';
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
InputNumber,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { getAgentConfig, updateAgentConfig } from '#/api/agent';
|
import { getAgentConfig, updateAgentConfig } from '#/api/agent';
|
||||||
|
|
||||||
|
import AgentConfigPreview from './modules/agent-config-preview.vue';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const config = ref<AgentApi.AgentConfig | null>(null);
|
const config = ref<AgentApi.AgentConfig | null>(null);
|
||||||
|
|
||||||
// 使用 reactive 管理表单数据(价格配置已移除,改为产品配置表管理)
|
|
||||||
const formData = reactive<AgentApi.AgentConfig>({
|
const formData = reactive<AgentApi.AgentConfig>({
|
||||||
level_bonus: {
|
level_bonus: {
|
||||||
normal: 0,
|
normal: 0,
|
||||||
@@ -45,7 +58,6 @@ const formData = reactive<AgentApi.AgentConfig>({
|
|||||||
diamond_max_uplift_amount: 0,
|
diamond_max_uplift_amount: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载配置
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -59,8 +71,33 @@ async function loadConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置
|
function collectSaveWarnings(): string[] {
|
||||||
async function handleSave() {
|
const w: string[] = [];
|
||||||
|
const g = Number(formData.upgrade_fee.normal_to_gold);
|
||||||
|
const d = Number(formData.upgrade_fee.normal_to_diamond);
|
||||||
|
const rg = Number(formData.upgrade_rebate.normal_to_gold_rebate);
|
||||||
|
const rd = Number(formData.upgrade_rebate.to_diamond_rebate);
|
||||||
|
if (rg > g) {
|
||||||
|
w.push('「普通→黄金」升级返佣大于升级费用。');
|
||||||
|
}
|
||||||
|
if (rd > d) {
|
||||||
|
w.push('「升至钻石」升级返佣大于「普通→钻石」升级费用。');
|
||||||
|
}
|
||||||
|
if (d < g) {
|
||||||
|
w.push('「普通→钻石」费用小于「普通→黄金」费用,黄金升钻石应付差价可能异常。');
|
||||||
|
}
|
||||||
|
const r = Number(formData.commission_freeze.ratio);
|
||||||
|
if (r < 0 || r > 1) {
|
||||||
|
w.push('佣金冻结比例应在 0~1 之间。');
|
||||||
|
}
|
||||||
|
const tr = Number(formData.tax_rate);
|
||||||
|
if (tr < 0 || tr > 1) {
|
||||||
|
w.push('税率应在 0~1 之间。');
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSave() {
|
||||||
try {
|
try {
|
||||||
const params: AgentApi.UpdateAgentConfigParams = {
|
const params: AgentApi.UpdateAgentConfigParams = {
|
||||||
level_bonus: {
|
level_bonus: {
|
||||||
@@ -100,7 +137,30 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置配置
|
async function handleSave() {
|
||||||
|
const warnings = collectSaveWarnings();
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '保存前请确认',
|
||||||
|
width: 480,
|
||||||
|
content: () =>
|
||||||
|
h('div', { class: 'text-sm' }, [
|
||||||
|
h('p', { class: 'mb-2 text-gray-600' }, '以下情况可能影响业务逻辑,若确认无误可继续保存:'),
|
||||||
|
h(
|
||||||
|
'ul',
|
||||||
|
{ class: 'mb-0 list-disc pl-4 text-gray-800' },
|
||||||
|
warnings.map((t) => h('li', { class: 'mb-1' }, t)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
okText: '仍要保存',
|
||||||
|
cancelText: '返回修改',
|
||||||
|
onOk: () => doSave(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await doSave();
|
||||||
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
if (config.value) {
|
if (config.value) {
|
||||||
Object.assign(formData, config.value);
|
Object.assign(formData, config.value);
|
||||||
@@ -114,161 +174,355 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<Card title="系统配置" :loading="loading">
|
<Card size="small" title="代理系统配置" class="agent-config-card" :loading="loading">
|
||||||
<Form layout="vertical">
|
<template #extra>
|
||||||
<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>
|
<Space>
|
||||||
<Button type="primary" @click="handleSave">保存</Button>
|
<Button type="primary" size="small" @click="handleSave">
|
||||||
<Button @click="handleReset">重置</Button>
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button size="small" @click="handleReset">重置</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="agent-config-layout">
|
||||||
|
<div class="agent-config-main">
|
||||||
|
<Form layout="vertical" size="small" class="agent-config-form">
|
||||||
|
<!-- 等级加成 -->·
|
||||||
|
<div class="config-panel config-panel--a">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">等级加成</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
按推广代理等级,在产品底价上叠加的金额(用于计算代理订单「实际底价」),单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item :name="['level_bonus', 'normal']" label="普通代理" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.level_bonus.normal" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['level_bonus', 'gold']" label="黄金代理" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.level_bonus.gold" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['level_bonus', 'diamond']" label="钻石代理" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.level_bonus.diamond" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最高价上调 -->
|
||||||
|
<div class="config-panel config-panel--b">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">等级最高价上调上限</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
黄金、钻石代理在定价时可上浮的售价上限额度,单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item name="gold_max_uplift_amount" label="黄金代理" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.gold_max_uplift_amount" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="diamond_max_uplift_amount" label="钻石代理" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.diamond_max_uplift_amount" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 升级费用 -->
|
||||||
|
<div class="config-panel config-panel--c">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">升级费用</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
用户自主升级时需支付的金额,单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item :name="['upgrade_fee', 'normal_to_gold']" label="普通 → 黄金" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.upgrade_fee.normal_to_gold" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['upgrade_fee', 'normal_to_diamond']" label="普通 → 钻石" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.upgrade_fee.normal_to_diamond" :min="0" :precision="2"
|
||||||
|
:step="0.01" addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 升级返佣 -->
|
||||||
|
<div class="config-panel config-panel--d">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">升级返佣</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
下级完成升级付费后,返给直接上级的金额,单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item :name="['upgrade_rebate', 'normal_to_gold_rebate']" label="普通升黄金" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.upgrade_rebate.normal_to_gold_rebate" :min="0" :precision="2"
|
||||||
|
:step="0.01" addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['upgrade_rebate', 'to_diamond_rebate']" label="升至钻石(含普通或黄金路径)" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.upgrade_rebate.to_diamond_rebate" :min="0" :precision="2"
|
||||||
|
:step="0.01" addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 直接上级返佣 -->
|
||||||
|
<div class="config-panel config-panel--e">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">直接上级返佣</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
推广订单产生等级加成时,按直接上级的等级拆分给上级的金额,单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item :name="['direct_parent_rebate', 'diamond']" label="上级为钻石" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.direct_parent_rebate.diamond" :min="0" :precision="2"
|
||||||
|
:step="0.01" addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['direct_parent_rebate', 'gold']" label="上级为黄金" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.direct_parent_rebate.gold" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['direct_parent_rebate', 'normal']" label="上级为普通" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.direct_parent_rebate.normal" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 黄金返佣上限 -->
|
||||||
|
<div class="config-panel config-panel--f">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">黄金代理返佣上限</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
规则内黄金上级所获返佣的上限金额,单位为元。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item name="max_gold_rebate_amount" label="上限金额" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.max_gold_rebate_amount" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 佣金冻结 -->
|
||||||
|
<div class="config-panel config-panel--g">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">佣金冻结</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
订单单价较高时,将部分佣金先放入冻结余额,到期再解冻。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item :name="['commission_freeze', 'ratio']" label="冻结比例" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.commission_freeze.ratio" :min="0" :max="1" :precision="4"
|
||||||
|
:step="0.0001" class="w-full" />
|
||||||
|
<div class="field-hint">
|
||||||
|
0~1 的小数。触发冻结后,冻结金额 = min(订单单价×本比例,
|
||||||
|
该笔佣金),与右侧示意一致。
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['commission_freeze', 'threshold']" label="触发阈值(按订单单价)" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.commission_freeze.threshold" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item :name="['commission_freeze', 'days']" label="冻结天数" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.commission_freeze.days" :min="0" :precision="0" :step="1"
|
||||||
|
addon-after="天" class="w-full" />
|
||||||
|
<div class="field-hint">填 0 表示不经过「天」延迟(仍受是否触发冻结影响),保存后会写入后台。</div>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 税费 -->
|
||||||
|
<div class="config-panel config-panel--h">
|
||||||
|
<div class="config-panel-head">
|
||||||
|
<Text strong class="config-panel-title">税费</Text>
|
||||||
|
<div class="config-panel-desc">
|
||||||
|
提现等环节使用的税率与免税额度。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-grid config-grid--3">
|
||||||
|
<Form.Item name="tax_rate" label="税率" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.tax_rate" :min="0" :max="1" :precision="4" :step="0.0001"
|
||||||
|
class="w-full" />
|
||||||
|
<div class="field-hint">零到一之间的小数,例如填 0.06 表示百分之六</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="tax_exemption_amount" label="免税额度" class="config-cell">
|
||||||
|
<InputNumber v-model:value="formData.tax_exemption_amount" :min="0" :precision="2" :step="0.01"
|
||||||
|
addon-after="元" class="w-full" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
</div>
|
||||||
|
<aside class="agent-config-aside">
|
||||||
|
<AgentConfigPreview :config="formData" />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.agent-config-card :deep(.ant-card-body) {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-config-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-config-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-config-aside {
|
||||||
|
width: 380px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-config-form {
|
||||||
|
max-width: 1080px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.agent-config-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-config-aside {
|
||||||
|
width: 100%;
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
border-left-width: 3px;
|
||||||
|
border-left-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--a {
|
||||||
|
background: #f2f6ff;
|
||||||
|
border-left-color: #8ca8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--b {
|
||||||
|
background: #f4faf4;
|
||||||
|
border-left-color: #7cb87c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--c {
|
||||||
|
background: #fffbf0;
|
||||||
|
border-left-color: #e8b86d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--d {
|
||||||
|
background: #f0fbfa;
|
||||||
|
border-left-color: #5dbeb3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--e {
|
||||||
|
background: #fdf4fa;
|
||||||
|
border-left-color: #d68bb8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--f {
|
||||||
|
background: #f7f3fc;
|
||||||
|
border-left-color: #a78bdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--g {
|
||||||
|
background: #fff8f0;
|
||||||
|
border-left-color: #e89e6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel--h {
|
||||||
|
background: #f8faf0;
|
||||||
|
border-left-color: #a8b86a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
column-gap: 16px;
|
||||||
|
row-gap: 2px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid--3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.config-grid--3 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.config-grid--3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-cell {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-cell :deep(.ant-form-item-label) {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-cell :deep(label) {
|
||||||
|
font-size: 12px;
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.48);
|
||||||
|
line-height: 1.35;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Alert, Divider, InputNumber, Typography } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 与左侧表单同一引用,随输入实时变化 */
|
||||||
|
config: AgentApi.AgentConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const demoOrderPrice = ref(500);
|
||||||
|
const demoCommission = ref(80);
|
||||||
|
const demoWithdraw = ref(10_000);
|
||||||
|
const demoMonthWithdrawn = ref(2000);
|
||||||
|
|
||||||
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
||||||
|
|
||||||
|
const diamondUpgradeFee = computed(() =>
|
||||||
|
round2(Number(props.config.upgrade_fee?.normal_to_diamond ?? 0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const commissionFreezePreview = computed(() => {
|
||||||
|
const threshold = Number(props.config.commission_freeze?.threshold ?? 0);
|
||||||
|
const ratio = Number(props.config.commission_freeze?.ratio ?? 0);
|
||||||
|
const days = Number(props.config.commission_freeze?.days ?? 0);
|
||||||
|
const op = Number(demoOrderPrice.value || 0);
|
||||||
|
const comm = Number(demoCommission.value || 0);
|
||||||
|
if (op < threshold) {
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
freezeAmount: 0,
|
||||||
|
threshold,
|
||||||
|
ratio,
|
||||||
|
days,
|
||||||
|
op,
|
||||||
|
comm,
|
||||||
|
raw: 0 as number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const raw = round2(op * ratio);
|
||||||
|
const freezeAmount = round2(Math.min(raw, comm));
|
||||||
|
return {
|
||||||
|
active: freezeAmount > 0,
|
||||||
|
freezeAmount,
|
||||||
|
threshold,
|
||||||
|
ratio,
|
||||||
|
days,
|
||||||
|
op,
|
||||||
|
comm,
|
||||||
|
raw,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxPreview = computed(() => {
|
||||||
|
const amount = Number(demoWithdraw.value || 0);
|
||||||
|
const monthly = Number(demoMonthWithdrawn.value || 0);
|
||||||
|
const exemption = Number(props.config.tax_exemption_amount ?? 0);
|
||||||
|
const rate = Number(props.config.tax_rate ?? 0);
|
||||||
|
let taxable = amount;
|
||||||
|
if (exemption > 0) {
|
||||||
|
const remainingExemption = round2(exemption - monthly);
|
||||||
|
if (remainingExemption > 0) {
|
||||||
|
if (amount <= remainingExemption) taxable = 0;
|
||||||
|
else taxable = round2(amount - remainingExemption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tax = round2(taxable * rate);
|
||||||
|
const actual = round2(amount - tax);
|
||||||
|
return { amount, monthly, exemption, rate, taxable, tax, actual };
|
||||||
|
});
|
||||||
|
|
||||||
|
const upgradeWarnings = computed(() => {
|
||||||
|
const w: string[] = [];
|
||||||
|
const g = Number(props.config.upgrade_fee?.normal_to_gold ?? 0);
|
||||||
|
const d = Number(props.config.upgrade_fee?.normal_to_diamond ?? 0);
|
||||||
|
const rg = Number(props.config.upgrade_rebate?.normal_to_gold_rebate ?? 0);
|
||||||
|
const rd = Number(props.config.upgrade_rebate?.to_diamond_rebate ?? 0);
|
||||||
|
if (rg > g) w.push('「普通→黄金」升级返佣大于升级费用,请确认是否符合运营预期。');
|
||||||
|
if (rd > d) w.push('「升至钻石」升级返佣大于「普通→钻石」升级费用,请确认是否符合运营预期。');
|
||||||
|
if (d < g) w.push('「普通→钻石」费用小于「普通→黄金」费用,请检查。');
|
||||||
|
return w;
|
||||||
|
});
|
||||||
|
|
||||||
|
function row(label: string, value: string) {
|
||||||
|
return { label, value };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="agent-config-preview rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
||||||
|
<div class="mb-3 text-base font-semibold text-gray-800">配置效果示意</div>
|
||||||
|
<Paragraph type="secondary" class="!mb-3 !text-xs">
|
||||||
|
下方数字随左侧表单实时变化;「演示」类数字仅用于模拟单笔订单/提现,不会保存。
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
v-if="upgradeWarnings.length"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
class="mb-3"
|
||||||
|
message="升级费用与返佣"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<ul class="mb-0 list-disc pl-4 text-xs">
|
||||||
|
<li v-for="(t, i) in upgradeWarnings" :key="i">{{ t }}</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div class="preview-section-title">升级:费用与返佣</div>
|
||||||
|
<Text type="secondary" class="mb-2 block text-xs">
|
||||||
|
黄金代理升为钻石时,应付升级费与「普通→钻石」相同,均取配置「普通→钻石」金额。
|
||||||
|
</Text>
|
||||||
|
<div class="preview-table">
|
||||||
|
<div
|
||||||
|
v-for="(r, idx) in [
|
||||||
|
row(
|
||||||
|
'普通 → 黄金',
|
||||||
|
`用户付 ¥${round2(config.upgrade_fee?.normal_to_gold ?? 0).toFixed(2)},上级返佣 ¥${round2(config.upgrade_rebate?.normal_to_gold_rebate ?? 0).toFixed(2)}`,
|
||||||
|
),
|
||||||
|
row(
|
||||||
|
'普通 → 钻石',
|
||||||
|
`用户付 ¥${diamondUpgradeFee.toFixed(2)},上级返佣 ¥${round2(config.upgrade_rebate?.to_diamond_rebate ?? 0).toFixed(2)}`,
|
||||||
|
),
|
||||||
|
row(
|
||||||
|
'黄金 → 钻石',
|
||||||
|
`用户付 ¥${diamondUpgradeFee.toFixed(2)}(与普通→钻石相同),上级返佣仍按「升至钻石」档:¥${round2(config.upgrade_rebate?.to_diamond_rebate ?? 0).toFixed(2)}`,
|
||||||
|
),
|
||||||
|
]"
|
||||||
|
:key="idx"
|
||||||
|
class="preview-table-row"
|
||||||
|
>
|
||||||
|
<span class="preview-table-label">{{ r.label }}</span>
|
||||||
|
<span class="preview-table-value">{{ r.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider class="!my-3" />
|
||||||
|
|
||||||
|
<div class="preview-section-title">定价空间(示意)</div>
|
||||||
|
<Text type="secondary" class="mb-2 block text-xs">
|
||||||
|
假设某产品底价为 100 元:可设售价上限约为「底价 + 上调上限」;等级加成参与的是「实际底价」计算,不计入此处售价上限示意。
|
||||||
|
</Text>
|
||||||
|
<div class="preview-table">
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">黄金代理</span>
|
||||||
|
<span class="preview-table-value">
|
||||||
|
≤ {{ round2(100 + (config.gold_max_uplift_amount ?? 0)).toFixed(2) }} 元(100 + 上调上限
|
||||||
|
{{ round2(config.gold_max_uplift_amount ?? 0).toFixed(2) }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">钻石代理</span>
|
||||||
|
<span class="preview-table-value">
|
||||||
|
≤ {{ round2(100 + (config.diamond_max_uplift_amount ?? 0)).toFixed(2) }} 元(100 + 上调上限
|
||||||
|
{{ round2(config.diamond_max_uplift_amount ?? 0).toFixed(2) }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider class="!my-3" />
|
||||||
|
|
||||||
|
<div class="preview-section-title">佣金冻结(与线上一致)</div>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
class="mb-2"
|
||||||
|
message="规则说明"
|
||||||
|
description="当订单单价 ≥ 触发阈值时:冻结金额 = min(订单单价 × 冻结比例, 该笔佣金金额);达到冻结天数后解冻。比例为 0~1 的小数。"
|
||||||
|
/>
|
||||||
|
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span>演示订单单价</span>
|
||||||
|
<InputNumber v-model:value="demoOrderPrice" :min="0" :precision="2" size="small" class="w-28" />
|
||||||
|
<span>演示佣金</span>
|
||||||
|
<InputNumber v-model:value="demoCommission" :min="0" :precision="2" size="small" class="w-28" />
|
||||||
|
</div>
|
||||||
|
<div class="preview-table">
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">是否触发</span>
|
||||||
|
<span class="preview-table-value">
|
||||||
|
{{
|
||||||
|
commissionFreezePreview.op >= commissionFreezePreview.threshold
|
||||||
|
? '是(单价 ≥ 阈值)'
|
||||||
|
: '否'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">冻结金额(示意)</span>
|
||||||
|
<span class="preview-table-value">
|
||||||
|
<Text strong>¥{{ commissionFreezePreview.freezeAmount.toFixed(2) }}</Text>
|
||||||
|
<span v-if="commissionFreezePreview.op >= commissionFreezePreview.threshold" class="text-gray-500">
|
||||||
|
(单价×比例 = ¥{{ commissionFreezePreview.raw.toFixed(2) }},与佣金取小)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">解冻天数</span>
|
||||||
|
<span class="preview-table-value">{{ commissionFreezePreview.days }} 天</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider class="!my-3" />
|
||||||
|
|
||||||
|
<div class="preview-section-title">提现税费(与线上一致)</div>
|
||||||
|
<Text type="secondary" class="mb-2 block text-xs">
|
||||||
|
免税额度按自然月累计提现额抵扣:本月已提现 + 本次提现 未超过免税部分则不计税。
|
||||||
|
</Text>
|
||||||
|
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span>演示本次提现</span>
|
||||||
|
<InputNumber v-model:value="demoWithdraw" :min="0" :precision="2" size="small" class="w-28" />
|
||||||
|
<span>本月已提现</span>
|
||||||
|
<InputNumber v-model:value="demoMonthWithdrawn" :min="0" :precision="2" size="small" class="w-28" />
|
||||||
|
</div>
|
||||||
|
<div class="preview-table">
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">税率</span>
|
||||||
|
<span class="preview-table-value">{{ ((taxPreview.rate || 0) * 100).toFixed(2) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">免税额度</span>
|
||||||
|
<span class="preview-table-value">¥{{ round2(taxPreview.exemption).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">本次应税金额</span>
|
||||||
|
<span class="preview-table-value">¥{{ taxPreview.taxable.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">本次税费</span>
|
||||||
|
<span class="preview-table-value">¥{{ taxPreview.tax.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-table-row">
|
||||||
|
<span class="preview-table-label">本次到账(示意)</span>
|
||||||
|
<span class="preview-table-value">
|
||||||
|
<Text strong>¥{{ taxPreview.actual.toFixed(2) }}</Text>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.agent-config-preview {
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-table {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-table-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-table-label {
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-table-value {
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
line-height: 1.45;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,22 +3,31 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||||||
|
|
||||||
import { getLevelName } from '#/utils/agent';
|
import { getLevelName } from '#/utils/agent';
|
||||||
|
|
||||||
export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
|
export type InviteCodeRow = Record<string, any> & {
|
||||||
|
agent_code?: number;
|
||||||
|
used_agent_code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useInviteCodeColumns(
|
||||||
|
onIssuerAgentCodeClick?: (row: InviteCodeRow) => void,
|
||||||
|
onUsedAgentCodeClick?: (row: InviteCodeRow) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'code',
|
field: 'code',
|
||||||
title: '邀请码',
|
title: '邀请码',
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_code',
|
||||||
title: '发放代理ID',
|
title: '发放代理编号',
|
||||||
width: 120,
|
width: 120,
|
||||||
|
cellRender: onIssuerAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onIssuerAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_mobile',
|
field: 'agent_mobile',
|
||||||
@@ -52,9 +61,15 @@ export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
|
|||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'used_agent_id',
|
field: 'used_agent_code',
|
||||||
title: '使用代理ID',
|
title: '使用代理编号',
|
||||||
width: 120,
|
width: 120,
|
||||||
|
cellRender: onUsedAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onUsedAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'used_time',
|
field: 'used_time',
|
||||||
@@ -77,7 +92,7 @@ export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
|
|||||||
width: 160,
|
width: 160,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
] as const;
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInviteCodeFormSchema(): VbenFormSchema[] {
|
export function useInviteCodeFormSchema(): VbenFormSchema[] {
|
||||||
@@ -86,11 +101,10 @@ export function useInviteCodeFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
label: '邀请码',
|
label: '邀请码',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '',
|
||||||
|
allowClear: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
fieldName: 'agent_id',
|
|
||||||
label: '发放代理ID',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AgentApi } from '#/api/agent';
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button, Input, InputNumber, Modal, Space, message } from 'ant-design-vue';
|
import { Button, Input, InputNumber, Modal, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
generateDiamondInviteCode,
|
generateDiamondInviteCode,
|
||||||
getInviteCodeList,
|
getInviteCodeList,
|
||||||
} from '#/api/agent';
|
} from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
|
||||||
import { useInviteCodeColumns, useInviteCodeFormSchema } from './data';
|
import {
|
||||||
|
type InviteCodeRow,
|
||||||
|
useInviteCodeColumns,
|
||||||
|
useInviteCodeFormSchema,
|
||||||
|
} from './data';
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
@@ -21,6 +25,8 @@ interface QueryParams {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const generateModalVisible = ref(false);
|
const generateModalVisible = ref(false);
|
||||||
const generatedCodes = ref<string[]>([]);
|
const generatedCodes = ref<string[]>([]);
|
||||||
const generateForm = ref({
|
const generateForm = ref({
|
||||||
@@ -29,24 +35,30 @@ const generateForm = ref({
|
|||||||
remark: '',
|
remark: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onInviteIssuerCode(row: InviteCodeRow) {
|
||||||
|
if (row.agent_code != null && row.agent_code > 0) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInviteUsedCode(row: InviteCodeRow) {
|
||||||
|
if (row.used_agent_code != null && row.used_agent_code > 0) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.used_agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useInviteCodeFormSchema(),
|
schema: useInviteCodeFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useInviteCodeColumns(),
|
columns: useInviteCodeColumns(onInviteIssuerCode, onInviteUsedCode),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||||
page,
|
|
||||||
form,
|
|
||||||
}: {
|
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getInviteCodeList({
|
return await getInviteCodeList({
|
||||||
...form,
|
...formValues,
|
||||||
target_level: 3,
|
target_level: 3,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
export type AgentLinkRow = Record<string, any> & {
|
||||||
|
agent_id: string;
|
||||||
|
agent_code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// 推广链接列表列配置
|
// 推广链接列表列配置
|
||||||
export function useLinkColumns(): VxeTableGridOptions['columns'] {
|
export function useLinkColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentLinkRow) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_code',
|
||||||
title: '代理ID',
|
title: '代理编号',
|
||||||
width: 100,
|
width: 110,
|
||||||
},
|
cellRender: onAgentCodeClick
|
||||||
{
|
? {
|
||||||
field: 'product_id',
|
name: 'CellLink',
|
||||||
title: '产品ID',
|
props: { onClick: onAgentCodeClick },
|
||||||
width: 100,
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'product_name',
|
field: 'product_name',
|
||||||
@@ -48,20 +56,14 @@ export function useLinkColumns(): VxeTableGridOptions['columns'] {
|
|||||||
// 推广链接搜索表单配置
|
// 推广链接搜索表单配置
|
||||||
export function useLinkFormSchema(): VbenFormSchema[] {
|
export function useLinkFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
fieldName: 'product_id',
|
|
||||||
label: '产品ID',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'product_name',
|
fieldName: 'agent_code',
|
||||||
label: '产品名称',
|
label: '代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入代理编号',
|
||||||
|
allowClear: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'link_identifier',
|
|
||||||
label: '推广码',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentLinkList } from '#/api/agent';
|
import { getAgentLinkList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
|
||||||
import { useLinkColumns, useLinkFormSchema } from './data';
|
import { type AgentLinkRow, useLinkColumns, useLinkFormSchema } from './data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
@@ -20,29 +22,31 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onLinkAgentCode(row: AgentLinkRow) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useLinkFormSchema(),
|
schema: useLinkFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useLinkColumns(),
|
columns: useLinkColumns(onLinkAgentCode),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||||
page,
|
|
||||||
form,
|
|
||||||
}: {
|
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentLinkList({
|
return await getAgentLinkList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
import { getLevelName } from '#/utils/agent';
|
import { getLevelName } from '#/utils/agent';
|
||||||
|
|
||||||
@@ -48,6 +49,20 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'mobile',
|
fieldName: 'mobile',
|
||||||
label: '手机号',
|
label: '手机号',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'real_name',
|
||||||
|
label: '姓名',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '实名姓名,模糊匹配',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'agent_code',
|
||||||
|
label: '代理编号',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'region',
|
fieldName: 'region',
|
||||||
@@ -66,38 +81,53 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
fieldName: 'team_leader_id',
|
|
||||||
label: '团队首领ID',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'RangePicker',
|
component: 'RangePicker',
|
||||||
fieldName: 'create_time',
|
fieldName: 'create_time',
|
||||||
label: '创建时间',
|
label: '成为代理时间',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
showTime: true,
|
showTime: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
function fmtMoney(cellValue: unknown) {
|
||||||
|
const n = Number(cellValue);
|
||||||
|
const v = Number.isFinite(n) ? n : 0;
|
||||||
|
return `¥${v.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
export function useColumns(): VxeTableGridOptions['columns'] {
|
export function useColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentApi.AgentListItem) => void,
|
||||||
|
onUserIdClick?: (row: AgentApi.AgentListItem) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'user_id',
|
field: 'user_id',
|
||||||
title: '用户ID',
|
title: '用户ID',
|
||||||
width: 100,
|
width: 110,
|
||||||
|
cellRender: onUserIdClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: {
|
||||||
|
onClick: onUserIdClick,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_code',
|
field: 'agent_code',
|
||||||
title: '代理编码',
|
title: '代理编号',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: {
|
||||||
|
onClick: onAgentCodeClick,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'level',
|
field: 'level',
|
||||||
@@ -129,6 +159,23 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
|||||||
title: '实名认证状态',
|
title: '实名认证状态',
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'real_name',
|
||||||
|
title: '姓名',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({
|
||||||
|
row,
|
||||||
|
cellValue,
|
||||||
|
}: {
|
||||||
|
row: AgentApi.AgentListItem;
|
||||||
|
cellValue?: string;
|
||||||
|
}) => {
|
||||||
|
if (row.is_real_name && (cellValue || row.real_name)) {
|
||||||
|
return String(cellValue ?? row.real_name ?? '');
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'wechat_id',
|
field: 'wechat_id',
|
||||||
title: '微信号',
|
title: '微信号',
|
||||||
@@ -145,29 +192,51 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'balance',
|
field: 'balance',
|
||||||
title: '钱包余额',
|
title: '钱包余额',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
`¥${cellValue.toFixed(2)}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'total_earnings',
|
field: 'total_earnings',
|
||||||
title: '累计收益',
|
title: '累计收益',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
`¥${cellValue.toFixed(2)}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'frozen_balance',
|
field: 'frozen_balance',
|
||||||
title: '冻结余额',
|
title: '冻结余额',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
`¥${cellValue.toFixed(2)}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'withdrawn_amount',
|
field: 'withdrawn_amount',
|
||||||
title: '提现总额',
|
title: '提现总额',
|
||||||
width: 120,
|
width: 120,
|
||||||
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'total_orders',
|
||||||
|
title: '订单总数',
|
||||||
|
width: 100,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
cellValue || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'total_order_amount',
|
||||||
|
title: '订单总金额',
|
||||||
|
width: 130,
|
||||||
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'total_agent_profit',
|
||||||
|
title: '代理总收益',
|
||||||
|
width: 130,
|
||||||
|
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'subordinate_count',
|
||||||
|
title: '下级数量',
|
||||||
|
width: 100,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
|
cellValue || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'create_time',
|
field: 'create_time',
|
||||||
@@ -182,7 +251,7 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'operation',
|
field: 'operation',
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 280,
|
width: 240,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return columns;
|
return columns;
|
||||||
@@ -234,11 +303,6 @@ export function useLinkFormSchema(): VbenFormSchema[] {
|
|||||||
// 佣金记录列表列配置
|
// 佣金记录列表列配置
|
||||||
export function useCommissionColumns(): VxeTableGridOptions['columns'] {
|
export function useCommissionColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_id',
|
||||||
title: '代理ID',
|
title: '代理ID',
|
||||||
@@ -310,11 +374,6 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
|
|||||||
// 奖励记录列表列配置
|
// 奖励记录列表列配置
|
||||||
export function useRewardColumns(): VxeTableGridOptions['columns'] {
|
export function useRewardColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_id',
|
||||||
title: '代理ID',
|
title: '代理ID',
|
||||||
@@ -365,11 +424,6 @@ export function useRewardFormSchema(): VbenFormSchema[] {
|
|||||||
// 提现记录列表列配置
|
// 提现记录列表列配置
|
||||||
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
|
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_id',
|
||||||
title: '代理ID',
|
title: '代理ID',
|
||||||
|
|||||||
@@ -6,29 +6,193 @@ import type {
|
|||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
import type { AgentApi } from '#/api/agent';
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button, Card, Dropdown, Menu } from 'ant-design-vue';
|
import { Breadcrumb, Button, Card, Dropdown, Menu } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentList } from '#/api/agent';
|
import { getAgentList } from '#/api/agent';
|
||||||
|
import {
|
||||||
|
AGENT_LIST_FOCUS_EVENT,
|
||||||
|
clearStaleAgentListNavStackStorage,
|
||||||
|
consumeAgentListFocusFromStorage,
|
||||||
|
navigateToAgentList,
|
||||||
|
navEntryFromFocusPayload,
|
||||||
|
type AgentListFocusPayload,
|
||||||
|
type AgentListNavEntry,
|
||||||
|
} from '#/composables/use-agent-list-navigate';
|
||||||
|
import { navigateToPlatformUserList } from '#/composables/use-platform-user-list-navigate';
|
||||||
|
|
||||||
import { useColumns, useGridFormSchema } from './data';
|
import { useColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
|
/** 金额类列在表格级 cellStyle 着色(列配置里的 cellStyle 不会透传到 VxeGrid) */
|
||||||
|
const agentListMoneyCellStyleMap: Record<string, { color: string; fontWeight: string }> =
|
||||||
|
{
|
||||||
|
balance: { color: '#1677ff', fontWeight: '600' },
|
||||||
|
total_earnings: { color: '#389e0d', fontWeight: '600' },
|
||||||
|
frozen_balance: { color: '#d46b08', fontWeight: '600' },
|
||||||
|
withdrawn_amount: { color: '#531dab', fontWeight: '600' },
|
||||||
|
total_order_amount: { color: '#08979c', fontWeight: '600' },
|
||||||
|
total_agent_profit: { color: '#c41d7f', fontWeight: '600' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function agentListCellStyle({
|
||||||
|
column,
|
||||||
|
}: {
|
||||||
|
column?: { field?: string };
|
||||||
|
}) {
|
||||||
|
const field = column?.field;
|
||||||
|
if (!field) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return agentListMoneyCellStyleMap[field];
|
||||||
|
}
|
||||||
import CommissionModal from './modules/commission-modal.vue';
|
import CommissionModal from './modules/commission-modal.vue';
|
||||||
import Form from './modules/form.vue';
|
import Form from './modules/form.vue';
|
||||||
import LinkModal from './modules/link-modal.vue';
|
import LinkModal from './modules/link-modal.vue';
|
||||||
import OrderModal from './modules/order-modal.vue';
|
import OrderModal from './modules/order-modal.vue';
|
||||||
import RebateModal from './modules/rebate-modal.vue';
|
import RebateModal from './modules/rebate-modal.vue';
|
||||||
import PlatformUpgradeModal from './modules/platform-upgrade-modal.vue';
|
import PlatformUpgradeModal from './modules/platform-upgrade-modal.vue';
|
||||||
|
import TeamTreeModal from './modules/team-tree-modal.vue';
|
||||||
import UpgradeModal from './modules/upgrade-modal.vue';
|
import UpgradeModal from './modules/upgrade-modal.vue';
|
||||||
import WithdrawalModal from './modules/withdrawal-modal.vue';
|
import WithdrawalModal from './modules/withdrawal-modal.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 下一次列表请求合并的定位参数(来自 sessionStorage 或同页 CustomEvent),请求后清除 */
|
||||||
|
const ephemeralAgentFocus = ref<AgentListFocusPayload | null>(null);
|
||||||
|
|
||||||
|
/** 列表内跳转轨迹:全部列表 → 某代理直属下级 → 精确查看某代理,用于面包屑与返回上一级 */
|
||||||
|
const navStack = ref<AgentListNavEntry[]>([{ kind: 'full' }]);
|
||||||
|
|
||||||
|
function navEntryLabel(entry: AgentListNavEntry): string {
|
||||||
|
if (entry.kind === 'full') {
|
||||||
|
return '全部代理';
|
||||||
|
}
|
||||||
|
if (entry.kind === 'subs') {
|
||||||
|
return entry.leaderCode != null
|
||||||
|
? `编号 ${entry.leaderCode} 的直属下级`
|
||||||
|
: `直属下级(${entry.leaderId.slice(0, 8)}…)`;
|
||||||
|
}
|
||||||
|
if (entry.kind === 'focus') {
|
||||||
|
if (entry.agentCode != null) {
|
||||||
|
return `查看代理 · 编号 ${entry.agentCode}`;
|
||||||
|
}
|
||||||
|
return `查看代理 · ${entry.agentId?.slice(0, 8) ?? ''}…`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusPayloadFromNavEntry(
|
||||||
|
entry: Extract<AgentListNavEntry, { kind: 'focus' }>,
|
||||||
|
): AgentListFocusPayload {
|
||||||
|
return {
|
||||||
|
agent_id: entry.agentId,
|
||||||
|
agent_code:
|
||||||
|
entry.agentCode != null ? String(entry.agentCode) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 避免连续重复写入相同「精确查看」节点 */
|
||||||
|
function appendFocusNavFromPayload(p: AgentListFocusPayload) {
|
||||||
|
const entry = navEntryFromFocusPayload(p);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const last = navStack.value[navStack.value.length - 1];
|
||||||
|
if (last && last.kind === 'focus') {
|
||||||
|
if (last.agentId === entry.agentId && last.agentCode === entry.agentCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navStack.value = [...navStack.value, entry];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alignRouteToStackTop() {
|
||||||
|
const top = navStack.value[navStack.value.length - 1];
|
||||||
|
if (!top) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tl = route.query.team_leader_id
|
||||||
|
? String(route.query.team_leader_id)
|
||||||
|
: '';
|
||||||
|
if (top.kind === 'subs') {
|
||||||
|
if (tl !== top.leaderId) {
|
||||||
|
await router.replace({
|
||||||
|
name: 'AgentList',
|
||||||
|
query: { team_leader_id: top.leaderId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tl) {
|
||||||
|
await router.replace({ name: 'AgentList', query: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据栈顶同步路由与一次性定位(栈仅在内存中;刷新后仅能通过 URL 的 team_leader_id 或一次性 focus 恢复) */
|
||||||
|
async function applyNavStackTop() {
|
||||||
|
const top = navStack.value[navStack.value.length - 1];
|
||||||
|
if (!top) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (top.kind === 'full') {
|
||||||
|
ephemeralAgentFocus.value = null;
|
||||||
|
await router.replace({ name: 'AgentList', query: {} });
|
||||||
|
} else if (top.kind === 'subs') {
|
||||||
|
ephemeralAgentFocus.value = null;
|
||||||
|
await router.replace({
|
||||||
|
name: 'AgentList',
|
||||||
|
query: { team_leader_id: top.leaderId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ephemeralAgentFocus.value = focusPayloadFromNavEntry(top);
|
||||||
|
await router.replace({ name: 'AgentList', query: {} });
|
||||||
|
}
|
||||||
|
await syncRouteQueryToSearchForm();
|
||||||
|
loadTeamLeaderSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goNavIndex(index: number) {
|
||||||
|
if (index < 0 || index >= navStack.value.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navStack.value = navStack.value.slice(0, index + 1);
|
||||||
|
await applyNavStackTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navBackOne() {
|
||||||
|
if (navStack.value.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navStack.value = navStack.value.slice(0, -1);
|
||||||
|
await applyNavStackTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导航栈不持久化:避免下次从菜单进入时代 URL 仍带 team_leader_id。仅当前 URL 含 team_leader_id 时重建「下级视图」栈。 */
|
||||||
|
function initNavStackFromRoute() {
|
||||||
|
clearStaleAgentListNavStackStorage();
|
||||||
|
const tl = route.query.team_leader_id
|
||||||
|
? String(route.query.team_leader_id)
|
||||||
|
: '';
|
||||||
|
if (tl) {
|
||||||
|
navStack.value = [{ kind: 'full' }, { kind: 'subs', leaderId: tl }];
|
||||||
|
} else {
|
||||||
|
navStack.value = [{ kind: 'full' }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 表单抽屉
|
// 表单抽屉
|
||||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||||
connectedComponent: Form,
|
connectedComponent: Form,
|
||||||
@@ -77,6 +241,12 @@ const [WithdrawalModalComponent, withdrawalModalApi] = useVbenModal({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 团队树弹窗
|
||||||
|
const [TeamTreeModalComponent, teamTreeModalApi] = useVbenModal({
|
||||||
|
connectedComponent: TeamTreeModal,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 表格配置
|
// 表格配置
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
@@ -92,7 +262,20 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
},
|
},
|
||||||
} as VxeGridListeners<AgentApi.AgentListItem>,
|
} as VxeGridListeners<AgentApi.AgentListItem>,
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useColumns(),
|
cellStyle: agentListCellStyle,
|
||||||
|
columns: useColumns(
|
||||||
|
(row) => {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(row) => {
|
||||||
|
const uid = row.user_id;
|
||||||
|
if (uid != null && String(uid).trim() !== '') {
|
||||||
|
navigateToPlatformUserList(router, { userId: uid });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
sortConfig: {
|
sortConfig: {
|
||||||
@@ -112,16 +295,36 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
// 兼容旧链接:URL 上的 agent_code / agent_id;新跳转走 sessionStorage / 同页事件,不写 URL
|
||||||
|
const urlAgentCode = route.query.agent_code
|
||||||
|
? String(route.query.agent_code)
|
||||||
|
: undefined;
|
||||||
|
const urlAgentId = route.query.agent_id
|
||||||
|
? String(route.query.agent_id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const shot = ephemeralAgentFocus.value;
|
||||||
|
|
||||||
const res = await getAgentList({
|
const res = await getAgentList({
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
...formValues,
|
...formValues,
|
||||||
...sortParams,
|
...sortParams,
|
||||||
team_leader_id: route.query.team_leader_id
|
team_leader_id:
|
||||||
? Number(route.query.team_leader_id)
|
shot?.agent_id || urlAgentId || !route.query.team_leader_id
|
||||||
: undefined,
|
? undefined
|
||||||
|
: String(route.query.team_leader_id),
|
||||||
|
agent_code:
|
||||||
|
formValues?.agent_code ||
|
||||||
|
shot?.agent_code ||
|
||||||
|
urlAgentCode,
|
||||||
|
agent_id: shot?.agent_id || urlAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (shot) {
|
||||||
|
ephemeralAgentFocus.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
sort: sort || null,
|
sort: sort || null,
|
||||||
@@ -132,7 +335,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
result: 'items',
|
result: 'items',
|
||||||
total: 'total',
|
total: 'total',
|
||||||
},
|
},
|
||||||
autoLoad: true,
|
autoLoad: false,
|
||||||
},
|
},
|
||||||
rowConfig: {
|
rowConfig: {
|
||||||
keyField: 'id',
|
keyField: 'id',
|
||||||
@@ -147,6 +350,102 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
} as VxeTableGridOptions<AgentApi.AgentListItem>,
|
} as VxeTableGridOptions<AgentApi.AgentListItem>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 团队首领一行摘要(下级视图顶栏) */
|
||||||
|
const teamLeaderRow = ref<AgentApi.AgentListItem | null>(null);
|
||||||
|
|
||||||
|
async function loadTeamLeaderSummary() {
|
||||||
|
teamLeaderRow.value = null;
|
||||||
|
const tl = route.query.team_leader_id;
|
||||||
|
if (!tl || String(tl) === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getAgentList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
agent_id: String(tl),
|
||||||
|
});
|
||||||
|
teamLeaderRow.value = (res.items && res.items[0]) || null;
|
||||||
|
} catch {
|
||||||
|
teamLeaderRow.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncRouteQueryToSearchForm() {
|
||||||
|
await nextTick();
|
||||||
|
if (!gridApi.formApi?.setValues) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shot = ephemeralAgentFocus.value;
|
||||||
|
await gridApi.formApi.setValues({
|
||||||
|
agent_code:
|
||||||
|
(shot?.agent_code?.trim() ? shot.agent_code : '') ||
|
||||||
|
(route.query.agent_code ? String(route.query.agent_code) : '') ||
|
||||||
|
'',
|
||||||
|
});
|
||||||
|
await gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAgentListExternalFocus(e: Event) {
|
||||||
|
const ce = e as CustomEvent<AgentListFocusPayload>;
|
||||||
|
const d = ce.detail;
|
||||||
|
if (!d?.agent_code && !d?.agent_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ephemeralAgentFocus.value = d;
|
||||||
|
appendFocusNavFromPayload(d);
|
||||||
|
void syncRouteQueryToSearchForm();
|
||||||
|
loadTeamLeaderSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.team_leader_id,
|
||||||
|
() => {
|
||||||
|
loadTeamLeaderSummary();
|
||||||
|
void syncRouteQueryToSearchForm();
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [route.query.agent_code, route.query.agent_id],
|
||||||
|
() => {
|
||||||
|
void syncRouteQueryToSearchForm();
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener(
|
||||||
|
AGENT_LIST_FOCUS_EVENT,
|
||||||
|
onAgentListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
initNavStackFromRoute();
|
||||||
|
|
||||||
|
const fromStorage = consumeAgentListFocusFromStorage();
|
||||||
|
if (fromStorage?.agent_code || fromStorage?.agent_id) {
|
||||||
|
ephemeralAgentFocus.value = fromStorage;
|
||||||
|
appendFocusNavFromPayload(fromStorage);
|
||||||
|
} else {
|
||||||
|
const top = navStack.value[navStack.value.length - 1];
|
||||||
|
if (top?.kind === 'focus') {
|
||||||
|
ephemeralAgentFocus.value = focusPayloadFromNavEntry(top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await alignRouteToStackTop();
|
||||||
|
|
||||||
|
await syncRouteQueryToSearchForm();
|
||||||
|
loadTeamLeaderSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener(
|
||||||
|
AGENT_LIST_FOCUS_EVENT,
|
||||||
|
onAgentListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// 更多操作菜单项
|
// 更多操作菜单项
|
||||||
const moreMenuItems = [
|
const moreMenuItems = [
|
||||||
{
|
{
|
||||||
@@ -157,6 +456,10 @@ const moreMenuItems = [
|
|||||||
key: 'links',
|
key: 'links',
|
||||||
label: '推广链接',
|
label: '推广链接',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'commission',
|
||||||
|
label: '佣金记录',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'rebate',
|
key: 'rebate',
|
||||||
label: '返佣记录',
|
label: '返佣记录',
|
||||||
@@ -178,14 +481,16 @@ const moreMenuItems = [
|
|||||||
// 团队首领信息
|
// 团队首领信息
|
||||||
const teamLeaderId = computed(() => route.query.team_leader_id);
|
const teamLeaderId = computed(() => route.query.team_leader_id);
|
||||||
|
|
||||||
// 返回团队首领列表
|
function onOpenTeamTree(row: AgentApi.AgentListItem) {
|
||||||
function onBackToParent() {
|
teamTreeModalApi
|
||||||
router.replace({
|
.setData({
|
||||||
query: {
|
anchorAgentId: String(row.id),
|
||||||
...route.query,
|
onLocate: ({ agentId }: { agentId: string }) => {
|
||||||
team_leader_id: undefined,
|
navigateToAgentList(router, { agentId });
|
||||||
|
teamTreeModalApi.close();
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 操作处理函数
|
// 操作处理函数
|
||||||
@@ -223,15 +528,6 @@ function onActionClick(
|
|||||||
onViewOrder(e.row);
|
onViewOrder(e.row);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'view-sub-agent': {
|
|
||||||
router.replace({
|
|
||||||
query: {
|
|
||||||
...route.query,
|
|
||||||
team_leader_id: e.row.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'withdrawal': {
|
case 'withdrawal': {
|
||||||
onViewWithdrawal(e.row);
|
onViewWithdrawal(e.row);
|
||||||
break;
|
break;
|
||||||
@@ -297,12 +593,53 @@ function onRefresh() {
|
|||||||
<UpgradeModalComponent />
|
<UpgradeModalComponent />
|
||||||
<OrderModalComponent />
|
<OrderModalComponent />
|
||||||
<WithdrawalModalComponent />
|
<WithdrawalModalComponent />
|
||||||
|
<TeamTreeModalComponent />
|
||||||
|
|
||||||
|
<div class="mb-3 flex flex-wrap items-center gap-3">
|
||||||
|
<Button
|
||||||
|
v-if="navStack.length > 1"
|
||||||
|
type="default"
|
||||||
|
@click="navBackOne"
|
||||||
|
>
|
||||||
|
返回上一级
|
||||||
|
</Button>
|
||||||
|
<Breadcrumb class="agent-list-nav-bc flex-1 min-w-[200px]">
|
||||||
|
<Breadcrumb.Item v-for="(entry, index) in navStack" :key="index">
|
||||||
|
<a
|
||||||
|
v-if="index < navStack.length - 1"
|
||||||
|
class="cursor-pointer text-primary"
|
||||||
|
@click.prevent="goNavIndex(index)"
|
||||||
|
>
|
||||||
|
{{ navEntryLabel(entry) }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ navEntryLabel(entry) }}</span>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 团队首领信息卡片 -->
|
<!-- 团队首领信息卡片 -->
|
||||||
<Card v-if="teamLeaderId" class="mb-4">
|
<Card v-if="teamLeaderId" class="mb-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<Button @click="onBackToParent">返回上级列表</Button>
|
<Button :disabled="navStack.length <= 1" @click="navBackOne">
|
||||||
<div>团队首领ID:{{ teamLeaderId }}</div>
|
返回上一级
|
||||||
|
</Button>
|
||||||
|
<template v-if="teamLeaderRow">
|
||||||
|
<span>团队首领:编号 {{ teamLeaderRow.agent_code }}</span>
|
||||||
|
<span>{{ teamLeaderRow.level_name }}</span>
|
||||||
|
<span>{{ teamLeaderRow.mobile }}</span>
|
||||||
|
<Button
|
||||||
|
v-if="teamLeaderRow.agent_code != null"
|
||||||
|
type="link"
|
||||||
|
@click="
|
||||||
|
navigateToAgentList(router, {
|
||||||
|
agentCode: teamLeaderRow.agent_code,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
定位到该代理
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-gray-500">首领ID:{{ teamLeaderId }}</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -312,8 +649,8 @@ function onRefresh() {
|
|||||||
<Button type="link" @click="onActionClick({ code: 'edit', row })">
|
<Button type="link" @click="onActionClick({ code: 'edit', row })">
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" @click="onActionClick({ code: 'view-sub-agent', row })">
|
<Button type="link" @click="onOpenTeamTree(row)">
|
||||||
查看下级
|
查看团队
|
||||||
</Button>
|
</Button>
|
||||||
<!-- <Button
|
<!-- <Button
|
||||||
type="link"
|
type="link"
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
|||||||
<template>
|
<template>
|
||||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||||
<div class="agent-commission-modal">
|
<div class="agent-commission-modal">
|
||||||
<CommissionList :agent-id="modalData?.agentId" />
|
<CommissionList
|
||||||
|
:agent-id="modalData?.agentId"
|
||||||
|
:navigate-away="() => modalApi.close()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
const values = await formApi.getValues();
|
void (await formApi.getValues());
|
||||||
drawerApi.lock();
|
drawerApi.lock();
|
||||||
// TODO: 实现更新代理信息的接口
|
// TODO: 实现更新代理信息的接口
|
||||||
// updateAgent(id.value, values as AgentApi.UpdateAgentRequest)
|
// updateAgent(id.value, values as AgentApi.UpdateAgentRequest)
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
|||||||
<template>
|
<template>
|
||||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||||
<div class="agent-order-modal">
|
<div class="agent-order-modal">
|
||||||
<OrderList :agent-id="modalData?.agentId" />
|
<OrderList
|
||||||
|
:agent-id="modalData?.agentId"
|
||||||
|
:navigate-away="() => modalApi.close()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ const canUpgrade = computed(() => targetLevelOptions.value.length > 0);
|
|||||||
|
|
||||||
// 当可选目标等级变化时(如打开弹窗、切换代理),重置选中为第一项
|
// 当可选目标等级变化时(如打开弹窗、切换代理),重置选中为第一项
|
||||||
watch(targetLevelOptions, (opts) => {
|
watch(targetLevelOptions, (opts) => {
|
||||||
if (opts.length) {
|
const first = opts[0];
|
||||||
selectedToLevel.value = opts[0].value;
|
if (first) {
|
||||||
|
selectedToLevel.value = first.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
|||||||
<template>
|
<template>
|
||||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||||
<div class="agent-rebate-modal">
|
<div class="agent-rebate-modal">
|
||||||
<RebateList :agent-id="modalData?.agentId" />
|
<RebateList
|
||||||
|
:agent-id="modalData?.agentId"
|
||||||
|
:navigate-away="() => modalApi.close()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import RewardList from '../../agent-reward/list.vue';
|
||||||
|
|
||||||
|
interface ModalData {
|
||||||
|
agentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: '奖励记录列表',
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalData = computed(() => modalApi.getData<ModalData>());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||||
|
<div class="agent-reward-modal p-4">
|
||||||
|
<RewardList :agent-id="modalData?.agentId" />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
|
import { ApartmentOutlined, UserOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Empty, Spin, Tree } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getAgentTeamTree } from '#/api/agent';
|
||||||
|
|
||||||
|
interface ModalData {
|
||||||
|
anchorAgentId: string;
|
||||||
|
onLocate: (payload: { agentId: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ant-design-vue Tree 节点;自定义字段在 dataRef 中带给 #title 插槽 */
|
||||||
|
interface TeamTreeDataNode {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
children?: TeamTreeDataNode[];
|
||||||
|
depth: number;
|
||||||
|
layerCaption: string;
|
||||||
|
siblingCaption: string;
|
||||||
|
agentCode: number;
|
||||||
|
levelName: string;
|
||||||
|
mobile: string;
|
||||||
|
isAnchor: boolean;
|
||||||
|
directBranchCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = ref<TeamTreeDataNode[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const loadError = ref('');
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: '查看团队',
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
treeData.value = [];
|
||||||
|
loadError.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadTree();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function mapNode(
|
||||||
|
node: AgentApi.AgentTeamTreeNode,
|
||||||
|
depth: number,
|
||||||
|
siblingIndex: number,
|
||||||
|
siblingTotal: number,
|
||||||
|
): TeamTreeDataNode {
|
||||||
|
const kids = node.children?.length
|
||||||
|
? node.children.map((c, i) =>
|
||||||
|
mapNode(c, depth + 1, i, node.children!.length),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const layerCaption =
|
||||||
|
depth === 0 ? '团队根 · 首领' : `邀请第 ${depth} 层`;
|
||||||
|
|
||||||
|
const siblingCaption =
|
||||||
|
siblingTotal <= 1
|
||||||
|
? '同级唯一分支'
|
||||||
|
: `同级分支 ${siblingIndex + 1} / ${siblingTotal}`;
|
||||||
|
|
||||||
|
const anchorTag = node.is_anchor ? '(本次查看)' : '';
|
||||||
|
const summaryTitle = `编号${node.agent_code} ${node.level_name}${anchorTag}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: node.id,
|
||||||
|
title: summaryTitle,
|
||||||
|
depth,
|
||||||
|
layerCaption,
|
||||||
|
siblingCaption,
|
||||||
|
agentCode: node.agent_code,
|
||||||
|
levelName: node.level_name,
|
||||||
|
mobile: node.mobile,
|
||||||
|
isAnchor: node.is_anchor,
|
||||||
|
directBranchCount: kids?.length ?? 0,
|
||||||
|
children: kids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTree() {
|
||||||
|
const data = modalApi.getData<ModalData>();
|
||||||
|
if (!data?.anchorAgentId) {
|
||||||
|
loadError.value = '缺少代理信息';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
loadError.value = '';
|
||||||
|
treeData.value = [];
|
||||||
|
try {
|
||||||
|
const res = await getAgentTeamTree(data.anchorAgentId);
|
||||||
|
const root = res.root;
|
||||||
|
if (root?.id) {
|
||||||
|
treeData.value = [mapNode(root, 0, 0, 1)];
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : '加载失败';
|
||||||
|
loadError.value = msg;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Tree #title 插槽解析节点(优先 dataRef) */
|
||||||
|
function nodeFromTitleSlot(slotProps: unknown): TeamTreeDataNode | null {
|
||||||
|
if (!slotProps || typeof slotProps !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const sp = slotProps as Record<string, unknown>;
|
||||||
|
const raw = (sp.dataRef ?? sp) as Record<string, unknown>;
|
||||||
|
if (typeof raw.depth !== 'number' || raw.key === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return raw as unknown as TeamTreeDataNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asSingleNode(n: TeamTreeDataNode | null): TeamTreeDataNode[] {
|
||||||
|
return n ? [n] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTreeSelect(selectedKeys: (string | number)[]) {
|
||||||
|
const raw = selectedKeys[0];
|
||||||
|
if (raw === undefined || raw === null || raw === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const agentId = String(raw);
|
||||||
|
const data = modalApi.getData<ModalData>();
|
||||||
|
data?.onLocate?.({ agentId });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="team-tree-modal w-[min(92vw,780px)]" :footer="false">
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<div class="team-tree-legend mb-3 rounded border border-gray-200 bg-gray-50 px-3 py-2 text-gray-600 text-xs leading-relaxed">
|
||||||
|
<p class="font-medium text-gray-700 mb-1">
|
||||||
|
树状图怎么读
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4 space-y-0.5 m-0">
|
||||||
|
<li>
|
||||||
|
<strong>自上而下</strong>为邀请链路:上级 → 直属下级(一根竖线上的父子)。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>同一父节点下展开的多行</strong>,表示该上级发展的<strong>多条并列分支</strong>(多个直属下级并排)。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
左侧缩进线与 <strong>show-line</strong> 折线对应层级;标签里的「同级分支 i/n」标明是第几条并列支。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p class="mb-3 text-gray-500 text-sm">
|
||||||
|
点击任意成员,将在下方代理列表中筛选并定位到该代理。
|
||||||
|
</p>
|
||||||
|
<div v-if="loadError" class="text-red-500 mb-2">
|
||||||
|
{{ loadError }}
|
||||||
|
</div>
|
||||||
|
<Empty
|
||||||
|
v-if="!loading && !treeData.length && !loadError"
|
||||||
|
description="暂无团队数据"
|
||||||
|
/>
|
||||||
|
<Tree
|
||||||
|
v-if="treeData.length"
|
||||||
|
class="team-tree-modal-tree"
|
||||||
|
block-node
|
||||||
|
show-line
|
||||||
|
default-expand-all
|
||||||
|
:tree-data="treeData"
|
||||||
|
@select="onTreeSelect"
|
||||||
|
>
|
||||||
|
<template #title="slotProps">
|
||||||
|
<template
|
||||||
|
v-for="n in asSingleNode(nodeFromTitleSlot(slotProps))"
|
||||||
|
:key="n.key"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="team-tree-node-row"
|
||||||
|
:class="[
|
||||||
|
`team-tree-node-row--depth-${Math.min(n.depth, 6)}`,
|
||||||
|
{ 'team-tree-node-row--anchor': n.isAnchor },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="team-tree-node-row__icons">
|
||||||
|
<ApartmentOutlined
|
||||||
|
v-if="n.directBranchCount > 0"
|
||||||
|
class="text-primary"
|
||||||
|
title="存在直属下级(多条分支从此分出)"
|
||||||
|
/>
|
||||||
|
<UserOutlined
|
||||||
|
v-else
|
||||||
|
class="text-gray-400"
|
||||||
|
title="叶子节点(当前无下级)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div class="team-tree-node-row__meta">
|
||||||
|
<div class="team-tree-node-row__branch">
|
||||||
|
<span class="team-tree-badge team-tree-badge--layer">
|
||||||
|
{{ n.layerCaption }}
|
||||||
|
</span>
|
||||||
|
<span class="team-tree-badge team-tree-badge--sibling">
|
||||||
|
{{ n.siblingCaption }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="n.directBranchCount > 0"
|
||||||
|
class="team-tree-badge team-tree-badge--count"
|
||||||
|
>
|
||||||
|
{{ n.directBranchCount }}
|
||||||
|
条直属分支
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="team-tree-node-row__main">
|
||||||
|
<span class="font-medium">编号 {{ n.agentCode }}</span>
|
||||||
|
<span class="mx-1 text-gray-400">·</span>
|
||||||
|
<span>{{ n.levelName }}</span>
|
||||||
|
<span class="mx-1 text-gray-400">·</span>
|
||||||
|
<span class="text-gray-600">{{ n.mobile }}</span>
|
||||||
|
<span v-if="n.isAnchor" class="ml-2 text-primary text-xs">
|
||||||
|
【本次查看】
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-if="!nodeFromTitleSlot(slotProps)">{{
|
||||||
|
(slotProps as { title?: string }).title
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</Tree>
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.team-tree-modal-tree {
|
||||||
|
max-height: min(58vh, 520px);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px 4px 12px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
:deep(.ant-tree-show-line .ant-tree-indent-unit) {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-tree-treenode) {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-tree-node-content-wrapper) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(24, 144, 255, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-tree-switcher) {
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-tree-title) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强化纵向连线视觉 */
|
||||||
|
:deep(.ant-tree-show-line .ant-tree-switcher-line-icon) {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-tree-node-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
border-left: 3px solid #d9d9d9;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-left: 2px;
|
||||||
|
|
||||||
|
&--depth-0 {
|
||||||
|
border-left-color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--depth-1 {
|
||||||
|
border-left-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--depth-2 {
|
||||||
|
border-left-color: #13c2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--depth-3 {
|
||||||
|
border-left-color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--depth-4 {
|
||||||
|
border-left-color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--depth-5,
|
||||||
|
&--depth-6 {
|
||||||
|
border-left-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--anchor {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(24, 144, 255, 0.08),
|
||||||
|
transparent 72%
|
||||||
|
);
|
||||||
|
margin: -6px -8px;
|
||||||
|
padding: 6px 8px 6px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-color: #1890ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icons {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__branch {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-tree-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--layer {
|
||||||
|
background: #f0f5ff;
|
||||||
|
color: #2f54eb;
|
||||||
|
border: 1px solid #d6e4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--sibling {
|
||||||
|
background: #e6fffb;
|
||||||
|
color: #08979c;
|
||||||
|
border: 1px solid #87e8de;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--count {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #d46b08;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,29 +1,51 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
|
||||||
import { getOrderProcessStatusName } from '#/utils/agent';
|
export type AgentOrderRow = AgentApi.AgentOrderListItem;
|
||||||
|
|
||||||
export function useOrderColumns(): VxeTableGridOptions['columns'] {
|
export function useOrderColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentOrderRow) => void,
|
||||||
|
onOrderNoClick?: (row: AgentOrderRow) => void,
|
||||||
|
onOperationClick?: (e: { code: string; row: AgentOrderRow }) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'agent_code',
|
||||||
title: 'ID',
|
title: '推广代理编号',
|
||||||
width: 80,
|
width: 130,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'order_no',
|
||||||
title: '代理ID',
|
title: '商户订单号',
|
||||||
width: 100,
|
minWidth: 180,
|
||||||
|
cellRender: onOrderNoClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onOrderNoClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'order_id',
|
field: 'order_status',
|
||||||
title: '订单ID',
|
title: '订单状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellTag',
|
||||||
|
options: [
|
||||||
|
{ value: 'pending', color: 'warning', label: '待支付' },
|
||||||
|
{ value: 'paid', color: 'success', label: '已支付' },
|
||||||
|
{ value: 'failed', color: 'error', label: '支付失败' },
|
||||||
|
{ value: 'refunded', color: 'default', label: '已退款' },
|
||||||
|
{ value: 'closed', color: 'default', label: '已关闭' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'product_id',
|
|
||||||
title: '产品ID',
|
|
||||||
width: 100,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'product_name',
|
field: 'product_name',
|
||||||
@@ -35,35 +57,35 @@ export function useOrderColumns(): VxeTableGridOptions['columns'] {
|
|||||||
title: '订单金额',
|
title: '订单金额',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${Number(cellValue).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'set_price',
|
field: 'set_price',
|
||||||
title: '设定价格',
|
title: '设定价格',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${Number(cellValue).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actual_base_price',
|
field: 'actual_base_price',
|
||||||
title: '实际底价',
|
title: '实际底价',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${Number(cellValue).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'price_cost',
|
field: 'price_cost',
|
||||||
title: '提价成本',
|
title: '提价成本',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${Number(cellValue).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_profit',
|
field: 'agent_profit',
|
||||||
title: '代理收益',
|
title: '代理收益',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${Number(cellValue).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'process_status',
|
field: 'process_status',
|
||||||
@@ -84,20 +106,54 @@ export function useOrderColumns(): VxeTableGridOptions['columns'] {
|
|||||||
width: 160,
|
width: 160,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
] as const;
|
...(onOperationClick
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
align: 'center' as const,
|
||||||
|
cellRender: {
|
||||||
|
attrs: {
|
||||||
|
nameField: 'order_no',
|
||||||
|
nameTitle: '商户订单号',
|
||||||
|
onClick: onOperationClick,
|
||||||
|
},
|
||||||
|
name: 'CellOperation',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
code: 'settlement',
|
||||||
|
text: '分账链路',
|
||||||
|
disabled: (row: AgentOrderRow) => !row.order_no,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
field: 'operation',
|
||||||
|
fixed: 'right' as const,
|
||||||
|
title: '操作',
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOrderFormSchema(): VbenFormSchema[] {
|
export function useOrderFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'agent_id',
|
fieldName: 'agent_code',
|
||||||
label: '代理ID',
|
label: '推广代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入推广代理编号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'order_id',
|
fieldName: 'order_no',
|
||||||
label: '订单ID',
|
label: '商户订单号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入商户订单号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
@@ -112,6 +168,20 @@ export function useOrderFormSchema(): VbenFormSchema[] {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'order_status',
|
||||||
|
label: '订单状态',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '待支付', value: 'pending' },
|
||||||
|
{ label: '已支付', value: 'paid' },
|
||||||
|
{ label: '支付失败', value: 'failed' },
|
||||||
|
{ label: '已退款', value: 'refunded' },
|
||||||
|
{ label: '已关闭', value: 'closed' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentOrderList } from '#/api/agent';
|
import { getAgentOrderList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
import { navigateToOrderList } from '#/composables/use-order-list-navigate';
|
||||||
|
|
||||||
import { useOrderColumns, useOrderFormSchema } from './data';
|
import {
|
||||||
|
type AgentOrderRow,
|
||||||
|
useOrderColumns,
|
||||||
|
useOrderFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import OrderSettlementModal from './modules/order-settlement-modal.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
|
/** 嵌套在弹窗内时,跳转到订单/代理列表后关闭外层弹窗 */
|
||||||
|
navigateAway?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
@@ -20,29 +32,70 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onOrderAgentCode(row: AgentOrderRow) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOrderNoClick(row: AgentOrderRow) {
|
||||||
|
if (row.order_no) {
|
||||||
|
navigateToOrderList(router, { orderNo: row.order_no });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [SettlementModalComponent, settlementModalApi] = useVbenModal({
|
||||||
|
connectedComponent: OrderSettlementModal,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOrderOperationClick(e: { code: string; row: AgentOrderRow }) {
|
||||||
|
if (e.code === 'settlement' && e.row.order_no) {
|
||||||
|
settlementModalApi
|
||||||
|
.setData({
|
||||||
|
order_no: e.row.order_no,
|
||||||
|
record_agent_code:
|
||||||
|
e.row.agent_code != null ? Number(e.row.agent_code) : undefined,
|
||||||
|
})
|
||||||
|
.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useOrderFormSchema(),
|
schema: useOrderFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useOrderColumns(),
|
columns: useOrderColumns(
|
||||||
|
onOrderAgentCode,
|
||||||
|
onOrderNoClick,
|
||||||
|
onOrderOperationClick,
|
||||||
|
),
|
||||||
|
toolbarConfig: {
|
||||||
|
custom: true,
|
||||||
|
export: false,
|
||||||
|
refresh: false,
|
||||||
|
search: true,
|
||||||
|
zoom: true,
|
||||||
|
},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentOrderList({
|
return await getAgentOrderList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
@@ -53,12 +106,13 @@ const [Grid] = useVbenVxeGrid({
|
|||||||
total: 'total',
|
total: 'total',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<AgentOrderRow>,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="!agentId">
|
<Page :auto-content-height="!agentId">
|
||||||
|
<SettlementModalComponent />
|
||||||
<Grid :table-title="agentId ? '订单记录列表' : '所有订单记录'" />
|
<Grid :table-title="agentId ? '订单记录列表' : '所有订单记录'" />
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
import type { AgentRebateRow } from '../../agent-rebate/data';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Alert, Descriptions, Spin, Table, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAgentOrderSettlement,
|
||||||
|
} from '#/api/agent';
|
||||||
|
import { getRebateTypeName } from '#/utils/agent';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
orderNo: string;
|
||||||
|
/** 当前列表行所在代理编号,用于高亮「本行视角」 */
|
||||||
|
highlightAgentCode?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
const commissions = ref<AgentApi.AgentCommissionListItem[]>([]);
|
||||||
|
const rebates = ref<AgentRebateRow[]>([]);
|
||||||
|
|
||||||
|
function commissionStatusLabel(s: number) {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
0: '待结算',
|
||||||
|
1: '已结算',
|
||||||
|
2: '已取消',
|
||||||
|
};
|
||||||
|
return m[s] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebateStatusLabel(s: number) {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
1: '已发放',
|
||||||
|
2: '已冻结',
|
||||||
|
3: '已取消',
|
||||||
|
};
|
||||||
|
return m[s] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!props.orderNo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
try {
|
||||||
|
const res = await getAgentOrderSettlement({ order_no: props.orderNo });
|
||||||
|
commissions.value = res.commissions ?? [];
|
||||||
|
rebates.value = (res.rebates ?? []) as AgentRebateRow[];
|
||||||
|
} catch (e: any) {
|
||||||
|
errorMsg.value = e?.message || '加载失败';
|
||||||
|
commissions.value = [];
|
||||||
|
rebates.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.orderNo,
|
||||||
|
() => {
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const involvedCodes = computed(() => {
|
||||||
|
const set = new Set<number>();
|
||||||
|
for (const r of commissions.value) {
|
||||||
|
if (r.agent_code != null) {
|
||||||
|
set.add(Number(r.agent_code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of rebates.value) {
|
||||||
|
if (r.agent_code != null) {
|
||||||
|
set.add(Number(r.agent_code));
|
||||||
|
}
|
||||||
|
if (r.source_agent_code != null) {
|
||||||
|
set.add(Number(r.source_agent_code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...set].sort((a, b) => a - b);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commissionColumns = [
|
||||||
|
{ title: '代理编号', dataIndex: 'agent_code', key: 'agent_code', width: 100 },
|
||||||
|
{ title: '佣金', dataIndex: 'amount', key: 'amount', width: 110 },
|
||||||
|
{
|
||||||
|
title: '产品',
|
||||||
|
dataIndex: 'product_name',
|
||||||
|
key: 'product_name',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 90 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rebateColumns = [
|
||||||
|
{
|
||||||
|
title: '返佣接收方(代理编号)',
|
||||||
|
dataIndex: 'agent_code',
|
||||||
|
key: 'agent_code',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '来源代理编号',
|
||||||
|
dataIndex: 'source_agent_code',
|
||||||
|
key: 'source_agent_code',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{ title: '类型', dataIndex: 'rebate_type', key: 'rebate_type', width: 130 },
|
||||||
|
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 100 },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 90 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function tagColor(code: number) {
|
||||||
|
if (props.highlightAgentCode != null && code === props.highlightAgentCode) {
|
||||||
|
return 'blue';
|
||||||
|
}
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="order-settlement-body">
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<Alert v-if="errorMsg" class="mb-3" type="error" :message="errorMsg" show-icon />
|
||||||
|
<Descriptions bordered size="small" :column="1" class="mb-4">
|
||||||
|
<Descriptions.Item label="商户订单号">{{ orderNo }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="涉及代理编号(去重)">
|
||||||
|
<template v-if="involvedCodes.length">
|
||||||
|
<Tag v-for="code in involvedCodes" :key="code" :color="tagColor(code)">
|
||||||
|
{{ code }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-gray-400">暂无</span>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
<div class="mb-2 text-sm font-medium text-gray-700">链路示意</div>
|
||||||
|
<div class="flow-hint mb-4 rounded border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
|
||||||
|
<div class="flow-line">
|
||||||
|
<span class="flow-node">订单支付成功</span>
|
||||||
|
<span class="flow-arrow">→</span>
|
||||||
|
<span class="flow-node">产生销售代理佣金</span>
|
||||||
|
<span class="flow-arrow">→</span>
|
||||||
|
<span class="flow-node">按规则向上级代理发放返佣</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 mb-0 text-xs text-gray-500">
|
||||||
|
下表为接口实际数据:佣金表为「谁卖了这一单拿多少」;返佣表为「谁因下级/来源代理的订单而获得返佣」。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 text-sm font-medium text-gray-700">销售佣金(本单)</div>
|
||||||
|
<Table size="small" :columns="commissionColumns" :data-source="commissions" :pagination="false" row-key="id"
|
||||||
|
class="mb-4">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'amount'">
|
||||||
|
¥{{ Number(record.amount).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
{{ commissionStatusLabel(Number(record.status)) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div class="mb-2 text-sm font-medium text-gray-700">返佣(本单)</div>
|
||||||
|
<Table size="small" :columns="rebateColumns" :data-source="rebates" :pagination="false" row-key="id">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'rebate_type'">
|
||||||
|
{{ getRebateTypeName(Number(record.rebate_type)) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'amount'">
|
||||||
|
¥{{ Number(record.amount).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
{{ rebateStatusLabel(Number(record.status)) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flow-line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-node {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import OrderSettlementBody from './order-settlement-body.vue';
|
||||||
|
|
||||||
|
interface ModalData {
|
||||||
|
order_no: string;
|
||||||
|
record_agent_code?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: '本单分账与代理链路',
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalData = computed(() => modalApi.getData<ModalData>());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[920px] max-w-[calc(100vw-48px)]" :footer="false">
|
||||||
|
<OrderSettlementBody
|
||||||
|
v-if="modalData?.order_no"
|
||||||
|
:key="modalData.order_no"
|
||||||
|
:order-no="modalData.order_no"
|
||||||
|
:highlight-agent-code="modalData.record_agent_code"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -8,11 +8,6 @@ import { z } from '#/adapter/form';
|
|||||||
// 代理产品配置列表列配置
|
// 代理产品配置列表列配置
|
||||||
export function useColumns(): VxeTableGridOptions['columns'] {
|
export function useColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'product_name',
|
field: 'product_name',
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { maskIdCard, maskMobile, getRealNameStatusName } from '#/utils/agent';
|
import { maskIdCard, maskMobile } from '#/utils/agent';
|
||||||
|
|
||||||
export function useRealNameColumns(): VxeTableGridOptions['columns'] {
|
export type AgentRealNameRow = Record<string, any> & {
|
||||||
|
agent_id: string;
|
||||||
|
agent_code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useRealNameColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentRealNameRow) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'agent_code',
|
||||||
title: 'ID',
|
title: '代理编号',
|
||||||
width: 80,
|
width: 110,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'agent_id',
|
||||||
@@ -59,15 +72,19 @@ export function useRealNameColumns(): VxeTableGridOptions['columns'] {
|
|||||||
width: 160,
|
width: 160,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
] as const;
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRealNameFormSchema(): VbenFormSchema[] {
|
export function useRealNameFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'agent_id',
|
fieldName: 'agent_id',
|
||||||
label: '代理ID',
|
label: '代理ID',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入代理ID',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentRealNameList } from '#/api/agent';
|
import { getAgentRealNameList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
|
||||||
import { useRealNameColumns, useRealNameFormSchema } from './data';
|
import {
|
||||||
|
type AgentRealNameRow,
|
||||||
|
useRealNameColumns,
|
||||||
|
useRealNameFormSchema,
|
||||||
|
} from './data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
@@ -20,29 +26,34 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onRealNameAgentCode(row: AgentRealNameRow) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useRealNameFormSchema(),
|
schema: useRealNameFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useRealNameColumns(),
|
columns: useRealNameColumns(onRealNameAgentCode),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentRealNameList({
|
return await getAgentRealNameList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,27 +3,60 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||||||
|
|
||||||
import { getRebateTypeName } from '#/utils/agent';
|
import { getRebateTypeName } from '#/utils/agent';
|
||||||
|
|
||||||
export function useRebateColumns(): VxeTableGridOptions['columns'] {
|
export type AgentRebateRow = {
|
||||||
|
id: string;
|
||||||
|
agent_id: string;
|
||||||
|
agent_code?: number;
|
||||||
|
source_agent_id: string;
|
||||||
|
source_agent_code?: number;
|
||||||
|
order_id: string;
|
||||||
|
order_no: string;
|
||||||
|
rebate_type: number;
|
||||||
|
amount: number;
|
||||||
|
status: number;
|
||||||
|
create_time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrderNoClickFn = (row: AgentRebateRow) => void;
|
||||||
|
|
||||||
|
export function useRebateColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentRebateRow) => void,
|
||||||
|
onSourceAgentCodeClick?: (row: AgentRebateRow) => void,
|
||||||
|
onOrderNoClick?: OrderNoClickFn,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'agent_code',
|
||||||
title: 'ID',
|
title: '代理编号',
|
||||||
width: 80,
|
width: 110,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'agent_id',
|
field: 'source_agent_code',
|
||||||
title: '代理ID',
|
title: '来源代理编号',
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'source_agent_id',
|
|
||||||
title: '来源代理ID',
|
|
||||||
width: 120,
|
width: 120,
|
||||||
|
cellRender: onSourceAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onSourceAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'order_id',
|
field: 'order_no',
|
||||||
title: '订单ID',
|
title: '商户订单号',
|
||||||
width: 100,
|
minWidth: 180,
|
||||||
|
cellRender: onOrderNoClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onOrderNoClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'rebate_type',
|
field: 'rebate_type',
|
||||||
@@ -40,26 +73,56 @@ export function useRebateColumns(): VxeTableGridOptions['columns'] {
|
|||||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||||
`¥${cellValue.toFixed(2)}`,
|
`¥${cellValue.toFixed(2)}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) => {
|
||||||
|
const m: Record<number, string> = {
|
||||||
|
1: '已发放',
|
||||||
|
2: '已冻结',
|
||||||
|
3: '已取消',
|
||||||
|
};
|
||||||
|
return m[cellValue] ?? '未知';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'create_time',
|
field: 'create_time',
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
width: 160,
|
width: 160,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
] as const;
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRebateFormSchema(): VbenFormSchema[] {
|
export function useRebateFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'agent_id',
|
fieldName: 'agent_code',
|
||||||
label: '代理ID',
|
label: '代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入代理编号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'source_agent_id',
|
fieldName: 'source_agent_code',
|
||||||
label: '来源代理ID',
|
label: '来源代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入来源代理编号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'order_no',
|
||||||
|
label: '商户订单号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入商户订单号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentRebateList } from '#/api/agent';
|
import { getAgentRebateList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
import { navigateToOrderList } from '#/composables/use-order-list-navigate';
|
||||||
|
|
||||||
import { useRebateColumns, useRebateFormSchema } from './data';
|
import {
|
||||||
|
type AgentRebateRow,
|
||||||
|
useRebateColumns,
|
||||||
|
useRebateFormSchema,
|
||||||
|
} from './data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
|
/** 嵌套在弹窗内时,跳转到订单/代理列表后调用(用于关闭弹窗) */
|
||||||
|
navigateAway?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryParams {
|
interface QueryParams {
|
||||||
@@ -20,29 +31,56 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onRebateAgentCode(row: AgentRebateRow) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRebateSourceAgentCode(row: AgentRebateRow) {
|
||||||
|
if (row.source_agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.source_agent_code });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOrderNoClick(row: AgentRebateRow) {
|
||||||
|
if (row.order_no) {
|
||||||
|
navigateToOrderList(router, { orderNo: row.order_no });
|
||||||
|
props.navigateAway?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useRebateFormSchema(),
|
schema: useRebateFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useRebateColumns(),
|
columns: useRebateColumns(onRebateAgentCode, onRebateSourceAgentCode, onOrderNoClick),
|
||||||
|
toolbarConfig: {
|
||||||
|
custom: true,
|
||||||
|
export: false,
|
||||||
|
refresh: false,
|
||||||
|
search: true,
|
||||||
|
zoom: true,
|
||||||
|
},
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentRebateList({
|
return await getAgentRebateList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
@@ -53,7 +91,7 @@ const [Grid] = useVbenVxeGrid({
|
|||||||
total: 'total',
|
total: 'total',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<AgentRebateRow>,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,13 @@ const [Grid] = useVbenVxeGrid({
|
|||||||
columns: useRewardColumns(),
|
columns: useRewardColumns(),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentRewardList({
|
return await getAgentRewardList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import {
|
import { getLevelName, getUpgradeTypeName } from '#/utils/agent';
|
||||||
getLevelName,
|
|
||||||
getUpgradeStatusName,
|
|
||||||
getUpgradeTypeName,
|
|
||||||
} from '#/utils/agent';
|
|
||||||
|
|
||||||
export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
|
export type AgentUpgradeRow = Record<string, any> & {
|
||||||
|
agent_id: string;
|
||||||
|
agent_code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useUpgradeColumns(
|
||||||
|
onAgentCodeClick?: (row: AgentUpgradeRow) => void,
|
||||||
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'agent_code',
|
||||||
title: 'ID',
|
title: '代理编号',
|
||||||
width: 80,
|
width: 110,
|
||||||
},
|
cellRender: onAgentCodeClick
|
||||||
{
|
? {
|
||||||
field: 'agent_id',
|
name: 'CellLink',
|
||||||
title: '代理ID',
|
props: { onClick: onAgentCodeClick },
|
||||||
width: 100,
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'from_level',
|
field: 'from_level',
|
||||||
@@ -64,7 +68,7 @@ export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
|
|||||||
cellRender: {
|
cellRender: {
|
||||||
name: 'CellTag',
|
name: 'CellTag',
|
||||||
options: [
|
options: [
|
||||||
{ value: 1, color: 'warning', label: '待处理' },
|
{ value: 1, color: 'warning', label: '待支付' },
|
||||||
{ value: 2, color: 'success', label: '已完成' },
|
{ value: 2, color: 'success', label: '已完成' },
|
||||||
{ value: 3, color: 'error', label: '已失败' },
|
{ value: 3, color: 'error', label: '已失败' },
|
||||||
],
|
],
|
||||||
@@ -76,15 +80,19 @@ export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
|
|||||||
width: 160,
|
width: 160,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
] as const;
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpgradeFormSchema(): VbenFormSchema[] {
|
export function useUpgradeFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'Input',
|
||||||
fieldName: 'agent_id',
|
fieldName: 'agent_code',
|
||||||
label: '代理ID',
|
label: '代理编号',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入代理编号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
@@ -105,7 +113,7 @@ export function useUpgradeFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
options: [
|
options: [
|
||||||
{ label: '待处理', value: 1 },
|
{ label: '待支付', value: 1 },
|
||||||
{ label: '已完成', value: 2 },
|
{ label: '已完成', value: 2 },
|
||||||
{ label: '已失败', value: 3 },
|
{ label: '已失败', value: 3 },
|
||||||
],
|
],
|
||||||
@@ -113,4 +121,3 @@ export function useUpgradeFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentUpgradeList } from '#/api/agent';
|
import { getAgentUpgradeList } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
|
||||||
import { useUpgradeColumns, useUpgradeFormSchema } from './data';
|
import {
|
||||||
|
type AgentUpgradeRow,
|
||||||
|
useUpgradeColumns,
|
||||||
|
useUpgradeFormSchema,
|
||||||
|
} from './data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agentId?: number;
|
agentId?: number;
|
||||||
@@ -20,29 +26,34 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onUpgradeAgentCode(row: AgentUpgradeRow) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
schema: useUpgradeFormSchema(),
|
schema: useUpgradeFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useUpgradeColumns(),
|
columns: useUpgradeColumns(onUpgradeAgentCode),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async (
|
||||||
page,
|
{ page }: { page: QueryParams },
|
||||||
form,
|
formValues: Record<string, any>,
|
||||||
}: {
|
) => {
|
||||||
form: Record<string, any>;
|
|
||||||
page: QueryParams;
|
|
||||||
}) => {
|
|
||||||
return await getAgentUpgradeList({
|
return await getAgentUpgradeList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AgentApi } from '#/api/agent';
|
|||||||
|
|
||||||
export function useWithdrawalColumns(
|
export function useWithdrawalColumns(
|
||||||
onActionClick?: OnActionClickFn<AgentApi.AgentWithdrawalListItem>,
|
onActionClick?: OnActionClickFn<AgentApi.AgentWithdrawalListItem>,
|
||||||
|
onAgentCodeClick?: (row: AgentApi.AgentWithdrawalListItem) => void,
|
||||||
): VxeTableGridOptions['columns'] {
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -14,9 +15,15 @@ export function useWithdrawalColumns(
|
|||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '代理ID',
|
title: '代理编号',
|
||||||
field: 'agent_id',
|
field: 'agent_code',
|
||||||
width: 100,
|
width: 110,
|
||||||
|
cellRender: onAgentCodeClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: { onClick: onAgentCodeClick },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '提现金额',
|
title: '提现金额',
|
||||||
@@ -40,12 +47,12 @@ export function useWithdrawalColumns(
|
|||||||
title: '提现方式',
|
title: '提现方式',
|
||||||
field: 'withdrawal_type',
|
field: 'withdrawal_type',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: ({ cellValue }: { cellValue: number }) => {
|
cellRender: {
|
||||||
const methodMap: Record<number, string> = {
|
name: 'CellTag',
|
||||||
1: '支付宝',
|
options: [
|
||||||
2: '银行卡',
|
{ value: 1, color: 'processing', label: '支付宝' },
|
||||||
};
|
{ value: 2, color: 'success', label: '银行卡' },
|
||||||
return methodMap[cellValue] || '未知';
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -126,16 +133,16 @@ export function useWithdrawalFormSchema(): VbenFormSchema[] {
|
|||||||
label: '提现单号',
|
label: '提现单号',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入提现单号',
|
placeholder: '支持模糊匹配',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'agent_id',
|
fieldName: 'agent_code',
|
||||||
label: '代理ID',
|
label: '代理编号',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入代理ID',
|
placeholder: '请输入代理编号',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -144,10 +151,28 @@ export function useWithdrawalFormSchema(): VbenFormSchema[] {
|
|||||||
label: '提现方式',
|
label: '提现方式',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
options: [
|
options: [
|
||||||
{ label: '支付宝', value: 1 },
|
{ label: '支付宝', value: 1 },
|
||||||
{ label: '银行卡', value: 2 },
|
{ label: '银行卡', value: 2 },
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'payee_account',
|
||||||
|
label: '收款账户',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '支持模糊匹配',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'payee_name',
|
||||||
|
label: '收款人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '支持模糊匹配',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getAgentWithdrawalList } from '#/api/agent';
|
import { getAgentWithdrawalList } from '#/api/agent';
|
||||||
import type { AgentApi } from '#/api/agent';
|
import type { AgentApi } from '#/api/agent';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
|
||||||
import AuditModal from './modules/audit-modal.vue';
|
import AuditModal from './modules/audit-modal.vue';
|
||||||
import { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
|
import { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
|
||||||
@@ -22,10 +24,18 @@ interface QueryParams {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const queryParams = computed(() => ({
|
const queryParams = computed(() => ({
|
||||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onWithdrawalAgentCode(row: AgentApi.AgentWithdrawalListItem) {
|
||||||
|
if (row.agent_code != null) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 审核弹窗
|
// 审核弹窗
|
||||||
const [AuditModalComponent, auditModalApi] = useVbenModal({
|
const [AuditModalComponent, auditModalApi] = useVbenModal({
|
||||||
connectedComponent: AuditModal,
|
connectedComponent: AuditModal,
|
||||||
@@ -59,19 +69,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useWithdrawalColumns(handleActionClick),
|
columns: useWithdrawalColumns(handleActionClick, onWithdrawalAgentCode),
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({
|
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||||
page,
|
|
||||||
form,
|
|
||||||
}: {
|
|
||||||
page: QueryParams;
|
|
||||||
form: Record<string, any>;
|
|
||||||
}) => {
|
|
||||||
return await getAgentWithdrawalList({
|
return await getAgentWithdrawalList({
|
||||||
...queryParams.value,
|
...queryParams.value,
|
||||||
...form,
|
...formValues,
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type {
|
|
||||||
WorkbenchProjectItem,
|
|
||||||
WorkbenchQuickNavItem,
|
|
||||||
WorkbenchTodoItem,
|
|
||||||
WorkbenchTrendItem,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AnalysisChartCard,
|
|
||||||
WorkbenchHeader,
|
|
||||||
WorkbenchProject,
|
|
||||||
WorkbenchQuickNav,
|
|
||||||
WorkbenchTodo,
|
|
||||||
WorkbenchTrends,
|
|
||||||
} from '@vben/common-ui';
|
|
||||||
import { preferences } from '@vben/preferences';
|
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
import { openWindow } from '@vben/utils';
|
|
||||||
|
|
||||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
|
||||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
|
||||||
// 例如:url: /dashboard/workspace
|
|
||||||
const projectItems: WorkbenchProjectItem[] = [
|
|
||||||
{
|
|
||||||
color: '',
|
|
||||||
content: '不要等待机会,而要创造机会。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: '开源组',
|
|
||||||
icon: 'carbon:logo-github',
|
|
||||||
title: 'Github',
|
|
||||||
url: 'https://github.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#3fb27f',
|
|
||||||
content: '现在的你决定将来的你。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: '算法组',
|
|
||||||
icon: 'ion:logo-vue',
|
|
||||||
title: 'Vue',
|
|
||||||
url: 'https://vuejs.org',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#e18525',
|
|
||||||
content: '没有什么才能比努力更重要。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: '上班摸鱼',
|
|
||||||
icon: 'ion:logo-html5',
|
|
||||||
title: 'Html5',
|
|
||||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#bf0c2c',
|
|
||||||
content: '热情和欲望可以突破一切难关。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: 'UI',
|
|
||||||
icon: 'ion:logo-angular',
|
|
||||||
title: 'Angular',
|
|
||||||
url: 'https://angular.io',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#00d8ff',
|
|
||||||
content: '健康的身体是实现目标的基石。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: '技术牛',
|
|
||||||
icon: 'bx:bxl-react',
|
|
||||||
title: 'React',
|
|
||||||
url: 'https://reactjs.org',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#EBD94E',
|
|
||||||
content: '路是走出来的,而不是空想出来的。',
|
|
||||||
date: '2021-04-01',
|
|
||||||
group: '架构组',
|
|
||||||
icon: 'ion:logo-javascript',
|
|
||||||
title: 'Js',
|
|
||||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
|
||||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
|
||||||
{
|
|
||||||
color: '#1fdaca',
|
|
||||||
icon: 'ion:home-outline',
|
|
||||||
title: '首页',
|
|
||||||
url: '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#bf0c2c',
|
|
||||||
icon: 'ion:grid-outline',
|
|
||||||
title: '仪表盘',
|
|
||||||
url: '/dashboard',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#e18525',
|
|
||||||
icon: 'ion:layers-outline',
|
|
||||||
title: '组件',
|
|
||||||
url: '/demos/features/icons',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#3fb27f',
|
|
||||||
icon: 'ion:settings-outline',
|
|
||||||
title: '系统管理',
|
|
||||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#4daf1bc9',
|
|
||||||
icon: 'ion:key-outline',
|
|
||||||
title: '权限管理',
|
|
||||||
url: '/demos/access/page-control',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: '#00d8ff',
|
|
||||||
icon: 'ion:bar-chart-outline',
|
|
||||||
title: '图表',
|
|
||||||
url: '/analytics',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
|
||||||
{
|
|
||||||
completed: false,
|
|
||||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
|
||||||
date: '2024-07-30 11:00:00',
|
|
||||||
title: '审查前端代码提交',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: true,
|
|
||||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
|
||||||
date: '2024-07-30 11:00:00',
|
|
||||||
title: '系统性能优化',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: false,
|
|
||||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
|
||||||
date: '2024-07-30 11:00:00',
|
|
||||||
title: '安全检查',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: false,
|
|
||||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
|
||||||
date: '2024-07-30 11:00:00',
|
|
||||||
title: '更新项目依赖',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completed: false,
|
|
||||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
|
||||||
date: '2024-07-30 11:00:00',
|
|
||||||
title: '修复UI显示问题',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const trendItems: WorkbenchTrendItem[] = [
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-1',
|
|
||||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
|
||||||
date: '刚刚',
|
|
||||||
title: '威廉',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-2',
|
|
||||||
content: `关注了 <a>威廉</a> `,
|
|
||||||
date: '1个小时前',
|
|
||||||
title: '艾文',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-3',
|
|
||||||
content: `发布了 <a>个人动态</a> `,
|
|
||||||
date: '1天前',
|
|
||||||
title: '克里斯',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-4',
|
|
||||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
|
||||||
date: '2天前',
|
|
||||||
title: 'Vben',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-1',
|
|
||||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
|
||||||
date: '3天前',
|
|
||||||
title: '皮特',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-2',
|
|
||||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
|
||||||
date: '1周前',
|
|
||||||
title: '杰克',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-3',
|
|
||||||
content: `发布了 <a>个人动态</a> `,
|
|
||||||
date: '1周前',
|
|
||||||
title: '威廉',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-4',
|
|
||||||
content: `推送了代码到 <a>Github</a>`,
|
|
||||||
date: '2021-04-01 20:00',
|
|
||||||
title: '威廉',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'svg:avatar-4',
|
|
||||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
|
||||||
date: '2021-03-01 20:00',
|
|
||||||
title: 'Vben',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
|
||||||
// This is a sample method, adjust according to the actual project requirements
|
|
||||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|
||||||
if (nav.url?.startsWith('http')) {
|
|
||||||
openWindow(nav.url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (nav.url?.startsWith('/')) {
|
|
||||||
router.push(nav.url).catch((error) => {
|
|
||||||
console.error('Navigation failed:', error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-5">
|
|
||||||
<WorkbenchHeader
|
|
||||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
|
||||||
</template>
|
|
||||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
|
||||||
</WorkbenchHeader>
|
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col lg:flex-row">
|
|
||||||
<div class="mr-4 w-full lg:w-3/5">
|
|
||||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
|
||||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full lg:w-2/5">
|
|
||||||
<WorkbenchQuickNav
|
|
||||||
:items="quickNavItems"
|
|
||||||
class="mt-5 lg:mt-0"
|
|
||||||
title="快捷导航"
|
|
||||||
@click="navTo"
|
|
||||||
/>
|
|
||||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
|
||||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
|
||||||
<AnalyticsVisitsSource />
|
|
||||||
</AnalysisChartCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -58,18 +58,18 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'title',
|
fieldName: 'title',
|
||||||
label: '通知标题',
|
label: '通知标题',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '模糊匹配标题',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Input',
|
||||||
fieldName: 'notification_page',
|
fieldName: 'notification_page',
|
||||||
label: '通知页面',
|
label: '通知页面',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
options: [
|
placeholder: '模糊匹配路径',
|
||||||
{ label: '首页', value: 'home' },
|
|
||||||
{ label: '个人中心', value: 'profile' },
|
|
||||||
{ label: '订单页', value: 'order' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,8 +79,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
options: [
|
options: [
|
||||||
{ label: '启用', value: 'active' },
|
{ label: '启用', value: 1 },
|
||||||
{ label: '禁用', value: 'inactive' },
|
{ label: '禁用', value: 0 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,7 @@ export function useColumns<T = NotificationApi.NotificationItem>(
|
|||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'show_date',
|
field: 'start_date',
|
||||||
title: '展示日期',
|
title: '展示日期',
|
||||||
width: 300,
|
width: 300,
|
||||||
formatter: ({ row }) => {
|
formatter: ({ row }) => {
|
||||||
@@ -125,7 +125,7 @@ export function useColumns<T = NotificationApi.NotificationItem>(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'show_time',
|
field: 'start_time',
|
||||||
title: '展示时间',
|
title: '展示时间',
|
||||||
width: 300,
|
width: 300,
|
||||||
formatter: ({ row }) => {
|
formatter: ({ row }) => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Button, message, Modal } from 'ant-design-vue';
|
|||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
|
getNotificationDetail,
|
||||||
getNotificationList,
|
getNotificationList,
|
||||||
updateNotification,
|
updateNotification,
|
||||||
} from '#/api/notification';
|
} from '#/api/notification';
|
||||||
@@ -36,23 +37,56 @@ const columns = useColumns<NotificationApi.NotificationItem>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function buildNotificationListParams(formValues: Record<string, any>) {
|
||||||
|
const params: Record<string, any> = { ...formValues };
|
||||||
|
delete params.date_range;
|
||||||
|
|
||||||
|
for (const key of ['title', 'notification_page']) {
|
||||||
|
const v = params[key];
|
||||||
|
if (v === '' || v === undefined || v === null) {
|
||||||
|
delete params[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusRaw = params.status;
|
||||||
|
if (statusRaw === '' || statusRaw === undefined || statusRaw === null) {
|
||||||
|
delete params.status;
|
||||||
|
} else {
|
||||||
|
const n = Number(String(statusRaw).trim());
|
||||||
|
if (n === 0 || n === 1) {
|
||||||
|
params.status = n;
|
||||||
|
} else {
|
||||||
|
delete params.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
fieldMappingTime: [['date', ['startDate', 'endDate']]],
|
fieldMappingTime: [['date_range', ['start_date', 'end_date']]],
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns,
|
columns,
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: false,
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async ({ page }, formValues) => {
|
query: async (
|
||||||
|
{ page, form }: { page: { currentPage: number; pageSize: number }; form?: Record<string, any> },
|
||||||
|
formValues: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const filters =
|
||||||
|
formValues && Object.keys(formValues).length > 0
|
||||||
|
? formValues
|
||||||
|
: (form ?? {});
|
||||||
return await getNotificationList({
|
return await getNotificationList({
|
||||||
page: page.currentPage,
|
page: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
...formValues,
|
...buildNotificationListParams(filters),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -125,17 +159,8 @@ async function onStatusChange(
|
|||||||
`你要将通知"${row.title}"的状态切换为【${status[newStatus]}】吗?`,
|
`你要将通知"${row.title}"的状态切换为【${status[newStatus]}】吗?`,
|
||||||
'切换状态',
|
'切换状态',
|
||||||
);
|
);
|
||||||
// 获取完整的通知数据
|
// 列表接口不支持按 id 筛选,必须用详情接口取完整字段再更新
|
||||||
const notification = await getNotificationList({
|
const fullData = await getNotificationDetail(row.id);
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
id: row.id,
|
|
||||||
});
|
|
||||||
const fullData = notification.items[0];
|
|
||||||
if (!fullData) {
|
|
||||||
message.error('获取通知数据失败');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await updateNotification(row.id, {
|
await updateNotification(row.id, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
status: newStatus,
|
status: newStatus,
|
||||||
@@ -147,6 +172,7 @@ async function onStatusChange(
|
|||||||
end_date: fullData.end_date,
|
end_date: fullData.end_date,
|
||||||
end_time: fullData.end_time,
|
end_time: fullData.end_time,
|
||||||
});
|
});
|
||||||
|
onRefresh();
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
));
|
));
|
||||||
emit('success');
|
emit('success');
|
||||||
drawerApi.close();
|
drawerApi.close();
|
||||||
} catch {
|
} finally {
|
||||||
drawerApi.unlock();
|
drawerApi.unlock();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { OrderApi } from '#/api/order';
|
import type { OrderApi } from '#/api/order';
|
||||||
|
|
||||||
|
type AgentClickFn = (row: OrderApi.Order) => void;
|
||||||
|
|
||||||
export function useColumns<T = OrderApi.Order>(
|
export function useColumns<T = OrderApi.Order>(
|
||||||
onActionClick: OnActionClickFn<T>,
|
onActionClick: OnActionClickFn<T>,
|
||||||
|
onAgentClick?: AgentClickFn,
|
||||||
): VxeTableGridOptions['columns'] {
|
): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -61,6 +64,45 @@ export function useColumns<T = OrderApi.Order>(
|
|||||||
return `¥${row.amount.toFixed(2)}`;
|
return `¥${row.amount.toFixed(2)}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellTag',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: true, color: 'success' },
|
||||||
|
{ label: '否', value: false, color: 'default' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
field: 'is_agent_order',
|
||||||
|
title: '是否代理订单',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'agent_code',
|
||||||
|
title: '代理编号',
|
||||||
|
width: 120,
|
||||||
|
cellRender: onAgentClick
|
||||||
|
? {
|
||||||
|
name: 'CellLink',
|
||||||
|
props: {
|
||||||
|
onClick: onAgentClick,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellTag',
|
||||||
|
options: [
|
||||||
|
{ value: 'not_agent', color: 'default', label: '非代理订单' },
|
||||||
|
{ value: 'pending', color: 'warning', label: '待处理' },
|
||||||
|
{ value: 'success', color: 'success', label: '处理成功' },
|
||||||
|
{ value: 'failed', color: 'error', label: '处理失败' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
field: 'agent_process_status',
|
||||||
|
title: '代理处理状态',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cellRender: {
|
cellRender: {
|
||||||
name: 'CellTag',
|
name: 'CellTag',
|
||||||
@@ -130,7 +172,7 @@ export function useColumns<T = OrderApi.Order>(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'query',
|
code: 'openOrderQueryResultPage',
|
||||||
text: '查询结果',
|
text: '查询结果',
|
||||||
disabled: (row: OrderApi.Order) => {
|
disabled: (row: OrderApi.Order) => {
|
||||||
return row.query_state !== 'success';
|
return row.query_state !== 'success';
|
||||||
@@ -205,6 +247,23 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: '支付状态',
|
label: '支付状态',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fieldName: 'is_agent_order',
|
||||||
|
label: '是否代理订单',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'agent_code',
|
||||||
|
label: '代理编号',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'RangePicker',
|
component: 'RangePicker',
|
||||||
fieldName: 'create_time',
|
fieldName: 'create_time',
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import type {
|
|||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
import type { OrderApi } from '#/api/order';
|
import type { OrderApi } from '#/api/order';
|
||||||
|
|
||||||
import { useRouter } from 'vue-router';
|
import { nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { navigateToAgentList } from '#/composables/use-agent-list-navigate';
|
||||||
|
import {
|
||||||
|
ORDER_LIST_FOCUS_EVENT,
|
||||||
|
consumeOrderListFocusFromStorage,
|
||||||
|
} from '#/composables/use-order-list-navigate';
|
||||||
import { getOrderList } from '#/api/order';
|
import { getOrderList } from '#/api/order';
|
||||||
|
|
||||||
import { useColumns, useGridFormSchema } from './data';
|
import { useColumns, useGridFormSchema } from './data';
|
||||||
@@ -26,7 +32,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useColumns(onActionClick),
|
columns: useColumns(onActionClick, onAgentClick),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
@@ -46,7 +52,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
custom: true,
|
custom: true,
|
||||||
export: true,
|
export: true,
|
||||||
refresh: { code: 'query' },
|
// 关闭工具栏刷新,避免与代理 query / 误触操作列逻辑产生冲突;可用搜索区「搜索」刷新列表
|
||||||
|
refresh: false,
|
||||||
search: true,
|
search: true,
|
||||||
zoom: true,
|
zoom: true,
|
||||||
},
|
},
|
||||||
@@ -58,15 +65,25 @@ const [RefundDrawer, refundDrawerApi] = useVbenDrawer({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
|
function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
|
// 历史/误触:勿与表格代理 code 混用
|
||||||
case 'query': {
|
case 'query': {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openOrderQueryResultPage': {
|
||||||
|
const id = e.row?.id;
|
||||||
|
const idStr = id == null ? '' : String(id).trim();
|
||||||
|
if (!idStr || idStr === ':id') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.push({
|
router.push({
|
||||||
name: 'OrderQueryDetail',
|
name: 'OrderQueryDetail',
|
||||||
params: {
|
params: {
|
||||||
id: e.row.id,
|
id: idStr,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -75,6 +92,9 @@ function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
|
|||||||
onRefund(e.row);
|
onRefund(e.row);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +109,89 @@ function onRefundSuccess() {
|
|||||||
function onRefresh() {
|
function onRefresh() {
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击代理编号跳转到代理列表并筛选
|
||||||
|
function onAgentClick(row: OrderApi.Order) {
|
||||||
|
if (row.agent_code) {
|
||||||
|
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格挂载后 formApi 才可用,外部带单号进入时需短暂等待 */
|
||||||
|
async function waitForGridFormReady(maxAttempts = 30) {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const setValues = (gridApi.formApi as unknown as Record<string, unknown>)
|
||||||
|
?.setValues;
|
||||||
|
if (typeof setValues === 'function') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await new Promise<void>((r) => setTimeout(r, 16));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从外部跳转过来时,读取 sessionStorage 中的 order_no 并填入表单并筛选列表(不进入任何详情页)
|
||||||
|
async function applyOrderListFocus(orderNo: string) {
|
||||||
|
const ok = await waitForGridFormReady();
|
||||||
|
const setValues = (gridApi.formApi as unknown as Record<string, unknown>)
|
||||||
|
?.setValues;
|
||||||
|
if (!ok || typeof setValues !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await gridApi.formApi.setValues({ order_no: orderNo });
|
||||||
|
await gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOrderListExternalFocus(e: Event) {
|
||||||
|
const ce = e as CustomEvent<{ order_no?: string }>;
|
||||||
|
const d = ce.detail;
|
||||||
|
if (d?.order_no) {
|
||||||
|
void applyOrderListFocus(d.order_no);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyOrderNoFromRouteIfPresent() {
|
||||||
|
const q = route.query.order_no;
|
||||||
|
const orderNo =
|
||||||
|
typeof q === 'string' ? q.trim() : Array.isArray(q) ? String(q[0] ?? '').trim() : '';
|
||||||
|
if (!orderNo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyOrderListFocus(orderNo);
|
||||||
|
if (route.query.order_no != null) {
|
||||||
|
await router.replace({ path: '/order', query: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener(
|
||||||
|
ORDER_LIST_FOCUS_EVENT,
|
||||||
|
onOrderListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
await applyOrderNoFromRouteIfPresent();
|
||||||
|
const fromStorage = consumeOrderListFocusFromStorage();
|
||||||
|
if (fromStorage?.order_no) {
|
||||||
|
await applyOrderListFocus(fromStorage.order_no);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.order_no,
|
||||||
|
() => {
|
||||||
|
void applyOrderNoFromRouteIfPresent();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener(
|
||||||
|
ORDER_LIST_FOCUS_EVENT,
|
||||||
|
onOrderListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -22,7 +22,25 @@ import { getOrderQueryDetail } from '#/api/order/query';
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const orderId = route.params.id as string;
|
const rawId = route.params.id;
|
||||||
|
const orderId =
|
||||||
|
typeof rawId === 'string'
|
||||||
|
? rawId
|
||||||
|
: Array.isArray(rawId)
|
||||||
|
? String(rawId[0] ?? '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
function isValidOrderIdForQuery(id: string) {
|
||||||
|
const t = id.trim();
|
||||||
|
if (!t || t === ':id' || t === 'undefined' || t === 'null') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 未替换的路由占位(如字面量 :xxx)
|
||||||
|
if (t.startsWith(':')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const queryDetail = ref<OrderQueryApi.QueryDetail>();
|
const queryDetail = ref<OrderQueryApi.QueryDetail>();
|
||||||
|
|
||||||
@@ -84,7 +102,10 @@ function handleBack() {
|
|||||||
|
|
||||||
// 获取查询详情
|
// 获取查询详情
|
||||||
async function fetchQueryDetail() {
|
async function fetchQueryDetail() {
|
||||||
if (!orderId) return;
|
if (!isValidOrderIdForQuery(orderId)) {
|
||||||
|
await router.replace({ path: '/order', query: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
// 搜索表单配置
|
// 搜索表单配置
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'user_id',
|
||||||
|
label: '用户ID',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '平台用户主键,精确查询',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'mobile',
|
fieldName: 'mobile',
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ import type {
|
|||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
import type { PlatformUserApi } from '#/api/platform-user';
|
import type { PlatformUserApi } from '#/api/platform-user';
|
||||||
|
|
||||||
|
import { nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
consumePlatformUserListFocusFromStorage,
|
||||||
|
PLATFORM_USER_LIST_FOCUS_EVENT,
|
||||||
|
} from '#/composables/use-platform-user-list-navigate';
|
||||||
import { getPlatformUserList } from '#/api/platform-user';
|
import { getPlatformUserList } from '#/api/platform-user';
|
||||||
|
|
||||||
import { useColumns, useGridFormSchema } from './data';
|
import { useColumns, useGridFormSchema } from './data';
|
||||||
import Form from './modules/form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 表单抽屉
|
// 表单抽屉
|
||||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||||
connectedComponent: Form,
|
connectedComponent: Form,
|
||||||
@@ -108,6 +118,88 @@ function onEdit(row: PlatformUserApi.PlatformUserItem) {
|
|||||||
function onRefresh() {
|
function onRefresh() {
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForGridFormReady(maxAttempts = 30) {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const setValues = (gridApi.formApi as unknown as Record<string, unknown>)
|
||||||
|
?.setValues;
|
||||||
|
if (typeof setValues === 'function') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await new Promise<void>((r) => setTimeout(r, 16));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPlatformUserListFocus(userId: string) {
|
||||||
|
const id = userId.trim();
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await waitForGridFormReady();
|
||||||
|
const setValues = (gridApi.formApi as unknown as Record<string, unknown>)
|
||||||
|
?.setValues;
|
||||||
|
if (!ok || typeof setValues !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await gridApi.formApi.setValues({ user_id: id });
|
||||||
|
await gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlatformUserListExternalFocus(e: Event) {
|
||||||
|
const ce = e as CustomEvent<{ user_id?: string }>;
|
||||||
|
const d = ce.detail;
|
||||||
|
if (d?.user_id) {
|
||||||
|
void applyPlatformUserListFocus(d.user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyUserIdFromRouteIfPresent() {
|
||||||
|
const q = route.query.user_id;
|
||||||
|
const userId =
|
||||||
|
typeof q === 'string'
|
||||||
|
? q.trim()
|
||||||
|
: Array.isArray(q)
|
||||||
|
? String(q[0] ?? '').trim()
|
||||||
|
: '';
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyPlatformUserListFocus(userId);
|
||||||
|
if (route.query.user_id != null) {
|
||||||
|
await router.replace({ path: route.path, query: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener(
|
||||||
|
PLATFORM_USER_LIST_FOCUS_EVENT,
|
||||||
|
onPlatformUserListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
await applyUserIdFromRouteIfPresent();
|
||||||
|
const fromStorage = consumePlatformUserListFocusFromStorage();
|
||||||
|
if (fromStorage?.user_id) {
|
||||||
|
await applyPlatformUserListFocus(fromStorage.user_id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.user_id,
|
||||||
|
() => {
|
||||||
|
void applyUserIdFromRouteIfPresent();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener(
|
||||||
|
PLATFORM_USER_LIST_FOCUS_EVENT,
|
||||||
|
onPlatformUserListExternalFocus as EventListener,
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -28,16 +28,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'notes',
|
fieldName: 'notes',
|
||||||
label: '备注',
|
label: '备注',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'InputNumber',
|
|
||||||
componentProps: {
|
|
||||||
min: 0,
|
|
||||||
precision: 2,
|
|
||||||
},
|
|
||||||
fieldName: 'cost_price',
|
|
||||||
label: '成本价',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -88,9 +78,9 @@ export function useColumns<T = ProductApi.ProductItem>(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'cost_price',
|
field: 'cost_price',
|
||||||
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
|
formatter: ({ cellValue }) => `¥${(cellValue || 0).toFixed(2)}`,
|
||||||
title: '成本价',
|
title: '成本价(模块合计)',
|
||||||
width: 120,
|
width: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'sell_price',
|
field: 'sell_price',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
import type { FeatureApi } from '#/api/product-manage/feature';
|
import type { FeatureApi } from '#/api/product-manage/feature';
|
||||||
import type { ProductApi } from '#/api/product-manage/product';
|
import type { ProductApi } from '#/api/product-manage/product';
|
||||||
|
|
||||||
import { h, ref } from 'vue';
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
@@ -85,6 +85,14 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const tempFeatureList = ref<TempFeatureItem[]>([]);
|
const tempFeatureList = ref<TempFeatureItem[]>([]);
|
||||||
|
|
||||||
|
/** 已关联模块成本合计(与后台汇总规则一致:各模块 cost_price 相加) */
|
||||||
|
const totalLinkedCost = computed(() =>
|
||||||
|
tempFeatureList.value.reduce(
|
||||||
|
(sum, row) => sum + Number(row.cost_price ?? 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 表格配置
|
// 表格配置
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
@@ -166,6 +174,15 @@ const columns: TableColumnsType<TempFeatureItem> = [
|
|||||||
title: '模块描述',
|
title: '模块描述',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '模块成本(元)',
|
||||||
|
dataIndex: 'cost_price',
|
||||||
|
width: 130,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const v = Number(record.cost_price ?? 0);
|
||||||
|
return `¥${v.toFixed(2)}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '是否启用',
|
title: '是否启用',
|
||||||
dataIndex: 'enable',
|
dataIndex: 'enable',
|
||||||
@@ -303,6 +320,7 @@ function handleAddFeature(feature: FeatureApi.FeatureItem) {
|
|||||||
feature_id: feature.id,
|
feature_id: feature.id,
|
||||||
api_id: feature.api_id,
|
api_id: feature.api_id,
|
||||||
name: feature.name,
|
name: feature.name,
|
||||||
|
cost_price: feature.cost_price ?? 0,
|
||||||
sort: maxSort + 1,
|
sort: maxSort + 1,
|
||||||
enable: 1,
|
enable: 1,
|
||||||
is_important: 0,
|
is_important: 0,
|
||||||
@@ -342,7 +360,15 @@ function handleRemoveFeature(record: TempFeatureItem) {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 右侧:已关联模块列表 -->
|
<!-- 右侧:已关联模块列表 -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="mb-2 text-base font-medium">已关联模块</div>
|
<div class="mb-2 flex flex-wrap items-center justify-between gap-2 text-base font-medium">
|
||||||
|
<span>已关联模块</span>
|
||||||
|
<span class="text-sm font-normal text-gray-600">
|
||||||
|
总成本(元):
|
||||||
|
<span class="font-semibold text-primary">
|
||||||
|
¥{{ totalLinkedCost.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="mb-4 text-sm text-gray-500">
|
<div class="mb-4 text-sm text-gray-500">
|
||||||
提示:可以通过拖拽行来调整模块顺序,通过开关控制模块的启用状态和重要程度
|
提示:可以通过拖拽行来调整模块顺序,通过开关控制模块的启用状态和重要程度
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,11 +70,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'api_name',
|
fieldName: 'api_name',
|
||||||
label: $t('system.api.apiName'),
|
label: $t('system.api.apiName'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'api_code',
|
|
||||||
label: $t('system.api.apiCode'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -103,11 +98,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: $t('system.api.status'),
|
label: $t('system.api.status'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'RangePicker',
|
|
||||||
fieldName: 'create_time',
|
|
||||||
label: $t('system.api.createTime'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
|||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,10 +51,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
fieldName: 'role_name',
|
fieldName: 'name',
|
||||||
label: $t('system.role.roleName'),
|
label: $t('system.role.roleName'),
|
||||||
},
|
},
|
||||||
{ component: 'Input', fieldName: 'id', label: $t('system.role.id') },
|
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -67,16 +66,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: $t('system.role.status'),
|
label: $t('system.role.status'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'description',
|
|
||||||
label: $t('system.role.remark'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RangePicker',
|
|
||||||
fieldName: 'create_time',
|
|
||||||
label: $t('system.role.createTime'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const [ApiPermissionsDrawer, apiPermissionsDrawerApi] = useVbenDrawer({
|
|||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,11 +76,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: $t('system.user.status'),
|
label: $t('system.user.status'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'RangePicker',
|
|
||||||
fieldName: 'create_time',
|
|
||||||
label: $t('system.user.createTime'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +118,7 @@ export function useColumns<T = SystemUserApi.SystemUser>(
|
|||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
{ code: 'edit', text: '编辑' },
|
{ code: 'edit', text: '编辑' },
|
||||||
{ code: 'resetPassword', text: '重置密码' },
|
{ code: 'changePassword', text: $t('system.user.changePassword') },
|
||||||
{ code: 'delete', text: '删除' },
|
{ code: 'delete', text: '删除' },
|
||||||
],
|
],
|
||||||
name: 'CellOperation',
|
name: 'CellOperation',
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ResetPasswordDrawer, resetPasswordDrawerApi] = useVbenDrawer({
|
const [ChangePasswordDrawer, changePasswordDrawerApi] = useVbenDrawer({
|
||||||
connectedComponent: ResetPasswordForm,
|
connectedComponent: ResetPasswordForm,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
|
||||||
schema: useGridFormSchema(),
|
schema: useGridFormSchema(),
|
||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
@@ -75,8 +74,8 @@ function onActionClick(e: OnActionClickParams<SystemUserApi.SystemUser>) {
|
|||||||
onEdit(e.row);
|
onEdit(e.row);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'resetPassword': {
|
case 'changePassword': {
|
||||||
onResetPassword(e.row);
|
onChangePassword(e.row);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,8 +131,8 @@ function onEdit(row: SystemUserApi.SystemUser) {
|
|||||||
formDrawerApi.setData(row).open();
|
formDrawerApi.setData(row).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResetPassword(row: SystemUserApi.SystemUser) {
|
function onChangePassword(row: SystemUserApi.SystemUser) {
|
||||||
resetPasswordDrawerApi.setData(row).open();
|
changePasswordDrawerApi.setData(row).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDelete(row: SystemUserApi.SystemUser) {
|
function onDelete(row: SystemUserApi.SystemUser) {
|
||||||
@@ -166,7 +165,7 @@ function onCreate() {
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<FormDrawer />
|
<FormDrawer />
|
||||||
<ResetPasswordDrawer @success="onRefresh" />
|
<ChangePasswordDrawer @success="onRefresh" />
|
||||||
<Grid :table-title="$t('system.user.list')">
|
<Grid :table-title="$t('system.user.list')">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<Button type="primary" @click="onCreate">
|
<Button type="primary" @click="onCreate">
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import type { SystemUserApi } from '#/api/system/user';
|
|||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenDrawer, z } from '@vben/common-ui';
|
import { useVbenDrawer, z } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { resetPassword } from '#/api/system/user';
|
import { resetPassword } from '#/api/system/user';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const emits = defineEmits(['success']);
|
const emits = defineEmits(['success']);
|
||||||
|
|
||||||
@@ -51,6 +54,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
drawerApi.lock();
|
drawerApi.lock();
|
||||||
resetPassword(id.value, { password: values.password })
|
resetPassword(id.value, { password: values.password })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
message.success($t('system.user.changePasswordSuccess'));
|
||||||
|
drawerApi.unlock();
|
||||||
emits('success');
|
emits('success');
|
||||||
drawerApi.close();
|
drawerApi.close();
|
||||||
})
|
})
|
||||||
@@ -72,9 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDrawerTitle = computed(() => {
|
const getDrawerTitle = computed(() => $t('system.user.changePassword'));
|
||||||
return '重置密码';
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Drawer :title="getDrawerTitle">
|
<Drawer :title="getDrawerTitle">
|
||||||
|
|||||||
Reference in New Issue
Block a user