f
This commit is contained in:
@@ -62,14 +62,27 @@ setupVbenVxeTable({
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' };未传 text 时默认展示当前列字段值
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
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(
|
||||
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 },
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -160,10 +173,10 @@ setupVbenVxeTable({
|
||||
return presets[opt]
|
||||
? { code: opt, ...presets[opt], ...defaultProps }
|
||||
: {
|
||||
code: opt,
|
||||
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
|
||||
...defaultProps,
|
||||
};
|
||||
code: opt,
|
||||
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
|
||||
...defaultProps,
|
||||
};
|
||||
} else {
|
||||
return { ...defaultProps, ...presets[opt.code], ...opt };
|
||||
}
|
||||
@@ -177,6 +190,17 @@ setupVbenVxeTable({
|
||||
})
|
||||
.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) {
|
||||
return h(
|
||||
Button,
|
||||
@@ -185,11 +209,15 @@ setupVbenVxeTable({
|
||||
...opt,
|
||||
icon: undefined,
|
||||
onClick: listen
|
||||
? () =>
|
||||
? () => {
|
||||
if (isOperationDisabled(opt, row)) {
|
||||
return;
|
||||
}
|
||||
attrs?.onClick?.({
|
||||
code: opt.code,
|
||||
row,
|
||||
})
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
@@ -225,6 +253,9 @@ setupVbenVxeTable({
|
||||
...opt,
|
||||
icon: undefined,
|
||||
onConfirm: () => {
|
||||
if (isOperationDisabled(opt, row)) {
|
||||
return;
|
||||
}
|
||||
attrs?.onClick?.({
|
||||
code: opt.code,
|
||||
row,
|
||||
|
||||
@@ -2,8 +2,11 @@ import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AgentApi {
|
||||
export interface AgentListItem {
|
||||
id: number;
|
||||
user_id: number;
|
||||
id: string | number;
|
||||
user_id: string | number;
|
||||
level?: number;
|
||||
agent_code?: number;
|
||||
team_leader_id?: string;
|
||||
level_name: string;
|
||||
region: string;
|
||||
mobile: string;
|
||||
@@ -13,10 +16,17 @@ export namespace AgentApi {
|
||||
frozen_balance: number;
|
||||
withdrawn_amount: number;
|
||||
create_time: string;
|
||||
is_real_name_verified: boolean;
|
||||
is_real_name_verified?: boolean;
|
||||
/** 与后端 json 字段 is_real_name 一致 */
|
||||
is_real_name?: boolean;
|
||||
real_name: string;
|
||||
id_card: string;
|
||||
real_name_status: 'approved' | 'pending' | 'rejected';
|
||||
// 订单统计相关字段
|
||||
total_orders?: number; // 订单总数
|
||||
total_order_amount?: number; // 订单总金额
|
||||
total_agent_profit?: number; // 代理总收益
|
||||
subordinate_count?: number; // 下级数量
|
||||
}
|
||||
|
||||
export interface AgentList {
|
||||
@@ -24,17 +34,34 @@ export namespace AgentApi {
|
||||
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 {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
mobile?: string;
|
||||
agent_code?: string;
|
||||
agent_id?: string;
|
||||
region?: string;
|
||||
parent_agent_id?: number;
|
||||
level?: number;
|
||||
team_leader_id?: string;
|
||||
id?: number;
|
||||
create_time_start?: string;
|
||||
create_time_end?: string;
|
||||
order_by?: string;
|
||||
order_type?: 'asc' | 'desc';
|
||||
/** 实名姓名模糊筛选(须已三要素核验) */
|
||||
real_name?: string;
|
||||
}
|
||||
|
||||
export interface AgentLinkListItem {
|
||||
@@ -54,15 +81,18 @@ export namespace AgentApi {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
agent_id?: number;
|
||||
agent_code?: string;
|
||||
product_name?: string;
|
||||
link_identifier?: string;
|
||||
}
|
||||
|
||||
// 代理佣金相关接口
|
||||
export interface AgentCommissionListItem {
|
||||
id: number;
|
||||
agent_id: number;
|
||||
order_id: number;
|
||||
id: string | number;
|
||||
agent_id: string | number;
|
||||
agent_code?: number;
|
||||
order_id: string | number;
|
||||
order_no: string;
|
||||
amount: number;
|
||||
product_name: string;
|
||||
status: number;
|
||||
@@ -78,7 +108,8 @@ export namespace AgentApi {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
agent_id?: number;
|
||||
product_name?: string;
|
||||
agent_code?: string;
|
||||
order_no?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -109,6 +140,7 @@ export namespace AgentApi {
|
||||
export interface AgentWithdrawalListItem {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
agent_code?: number;
|
||||
withdraw_no: string;
|
||||
amount: number;
|
||||
tax_amount: number;
|
||||
@@ -132,9 +164,12 @@ export namespace AgentApi {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
agent_id?: number | string;
|
||||
status?: number;
|
||||
agent_code?: string;
|
||||
withdraw_no?: string;
|
||||
withdrawal_type?: number;
|
||||
status?: number | string;
|
||||
withdrawal_type?: number | string;
|
||||
payee_account?: string;
|
||||
payee_name?: string;
|
||||
}
|
||||
|
||||
export interface AuditWithdrawalParams {
|
||||
@@ -313,11 +348,55 @@ export namespace AgentApi {
|
||||
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 {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
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 {
|
||||
@@ -331,21 +410,28 @@ export namespace AgentApi {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
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 {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
agent_id?: number | string;
|
||||
[key: string]: any;
|
||||
agent_code?: string;
|
||||
upgrade_type?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface GetInviteCodeListParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
code?: string;
|
||||
status?: number | string;
|
||||
target_level?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
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) {
|
||||
return requestClient.get<AgentApi.AdminListResp<Record<string, any>>>(
|
||||
return requestClient.get<AgentApi.AdminListResp<AgentApi.AgentOrderListItem>>(
|
||||
'/agent/order/list',
|
||||
{ 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) {
|
||||
return requestClient.get<AgentApi.AdminListResp<Record<string, any>>>(
|
||||
return requestClient.get<AgentApi.AdminListResp<AgentApi.AgentRebateListItem>>(
|
||||
'/agent/rebate/list',
|
||||
{ params },
|
||||
);
|
||||
@@ -659,8 +763,10 @@ export {
|
||||
getAgentConfig,
|
||||
getAgentLinkList,
|
||||
getAgentList,
|
||||
getAgentTeamTree,
|
||||
getAgentMembershipConfigList,
|
||||
getAgentOrderList,
|
||||
getAgentOrderSettlement,
|
||||
getAgentPlatformDeductionList,
|
||||
getAgentProductionConfigList,
|
||||
getAgentRealNameList,
|
||||
|
||||
@@ -58,11 +58,27 @@ async function getNotificationList(params: Recordable<any>) {
|
||||
return requestClient.get<NotificationApi.NotificationList>(
|
||||
'/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 {
|
||||
createNotification,
|
||||
deleteNotification,
|
||||
getNotificationDetail,
|
||||
getNotificationList,
|
||||
updateNotification,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,10 @@ export namespace OrderApi {
|
||||
pay_time: null | string;
|
||||
refund_time: null | string;
|
||||
update_time: string;
|
||||
// 代理相关字段
|
||||
is_agent_order: boolean; // 是否是代理订单
|
||||
agent_code?: string; // 代理编号
|
||||
agent_process_status?: string; // 代理处理状态
|
||||
}
|
||||
|
||||
export interface OrderList {
|
||||
|
||||
@@ -25,7 +25,6 @@ export namespace ProductApi {
|
||||
product_en: string;
|
||||
description: string;
|
||||
notes?: string;
|
||||
cost_price: number;
|
||||
sell_price: number;
|
||||
}
|
||||
|
||||
@@ -34,7 +33,6 @@ export namespace ProductApi {
|
||||
product_en?: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
cost_price?: number;
|
||||
sell_price?: number;
|
||||
}
|
||||
|
||||
@@ -44,6 +42,8 @@ export namespace ProductApi {
|
||||
feature_id: number;
|
||||
api_id: string;
|
||||
name: string;
|
||||
/** 模块成本(元),来自 feature,用于产品侧展示与汇总 */
|
||||
cost_price: number;
|
||||
sort: number;
|
||||
enable: number;
|
||||
is_important: number;
|
||||
|
||||
@@ -58,7 +58,7 @@ async function deleteUser(id: string) {
|
||||
* @param data.password 新密码
|
||||
*/
|
||||
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 };
|
||||
|
||||
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",
|
||||
"createTime": "Create Time",
|
||||
"operation": "Operation",
|
||||
"resetPassword": "Reset Password",
|
||||
"changePassword": "Change Password",
|
||||
"changePasswordSuccess": "Password changed successfully",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"confirmPasswordRequired": "Please confirm password",
|
||||
|
||||
@@ -92,7 +92,8 @@
|
||||
"setPermissions": "设置权限",
|
||||
"createTime": "创建时间",
|
||||
"operation": "操作",
|
||||
"resetPassword": "重置密码",
|
||||
"changePassword": "修改密码",
|
||||
"changePasswordSuccess": "密码修改成功",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"confirmPasswordRequired": "请确认密码",
|
||||
|
||||
@@ -32,6 +32,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/agent/commission',
|
||||
name: 'AgentCommission',
|
||||
meta: {
|
||||
hideInMenu: true,
|
||||
icon: 'mdi:cash-multiple',
|
||||
title: '佣金记录',
|
||||
},
|
||||
@@ -41,6 +42,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/agent/rebate',
|
||||
name: 'AgentRebate',
|
||||
meta: {
|
||||
hideInMenu: true,
|
||||
icon: 'mdi:currency-usd',
|
||||
title: '返佣记录',
|
||||
},
|
||||
|
||||
@@ -22,15 +22,6 @@ const routes: RouteRecordRaw[] = [
|
||||
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 {
|
||||
const map: Record<number, string> = {
|
||||
1: '待处理',
|
||||
1: '待支付',
|
||||
2: '已完成',
|
||||
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 { 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 [
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
width: 100,
|
||||
field: 'agent_code',
|
||||
title: '代理编号',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'order_id',
|
||||
title: '订单ID',
|
||||
width: 100,
|
||||
field: 'order_no',
|
||||
title: '商户订单号',
|
||||
minWidth: 180,
|
||||
cellRender: onOrderNoClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onOrderNoClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'amount',
|
||||
@@ -54,8 +72,21 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'product_name',
|
||||
label: '产品名称',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'order_no',
|
||||
label: '商户订单号',
|
||||
componentProps: {
|
||||
placeholder: '请输入商户订单号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AgentApi } from '#/api/agent';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
agentId?: number;
|
||||
/** 嵌套在弹窗内时,跳转到订单/代理列表后调用(用于关闭弹窗) */
|
||||
navigateAway?: () => void;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
@@ -20,29 +29,49 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(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({
|
||||
formOptions: {
|
||||
schema: useCommissionFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useCommissionColumns(),
|
||||
columns: useCommissionColumns(onCommissionAgentCodeClick, onOrderNoClick),
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: false,
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentCommissionList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
@@ -53,7 +82,7 @@ const [Grid] = useVbenVxeGrid({
|
||||
total: 'total',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<AgentApi.AgentCommissionListItem>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
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 { 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 AgentConfigPreview from './modules/agent-config-preview.vue';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const loading = ref(false);
|
||||
const config = ref<AgentApi.AgentConfig | null>(null);
|
||||
|
||||
// 使用 reactive 管理表单数据(价格配置已移除,改为产品配置表管理)
|
||||
const formData = reactive<AgentApi.AgentConfig>({
|
||||
level_bonus: {
|
||||
normal: 0,
|
||||
@@ -45,7 +58,6 @@ const formData = reactive<AgentApi.AgentConfig>({
|
||||
diamond_max_uplift_amount: 0,
|
||||
});
|
||||
|
||||
// 加载配置
|
||||
async function loadConfig() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -59,8 +71,33 @@ async function loadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function handleSave() {
|
||||
function collectSaveWarnings(): string[] {
|
||||
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 {
|
||||
const params: AgentApi.UpdateAgentConfigParams = {
|
||||
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() {
|
||||
if (config.value) {
|
||||
Object.assign(formData, config.value);
|
||||
@@ -114,161 +174,355 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Card title="系统配置" :loading="loading">
|
||||
<Form layout="vertical">
|
||||
<Card title="等级奖金" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['level_bonus', 'normal']" label="普通代理奖金">
|
||||
<InputNumber v-model:value="formData.level_bonus.normal" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['level_bonus', 'gold']" label="黄金代理奖金">
|
||||
<InputNumber v-model:value="formData.level_bonus.gold" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['level_bonus', 'diamond']" label="钻石代理奖金">
|
||||
<InputNumber v-model:value="formData.level_bonus.diamond" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="等级最高价上调金额" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item name="gold_max_uplift_amount" label="黄金代理最高价上调金额">
|
||||
<InputNumber v-model:value="formData.gold_max_uplift_amount" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item name="diamond_max_uplift_amount" label="钻石代理最高价上调金额">
|
||||
<InputNumber v-model:value="formData.diamond_max_uplift_amount" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="升级费用" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['upgrade_fee', 'normal_to_gold']" label="普通→黄金">
|
||||
<InputNumber v-model:value="formData.upgrade_fee.normal_to_gold" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['upgrade_fee', 'normal_to_diamond']" label="普通→钻石">
|
||||
<InputNumber v-model:value="formData.upgrade_fee.normal_to_diamond" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="升级返佣" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item :name="['upgrade_rebate', 'normal_to_gold_rebate']" label="普通→黄金返佣">
|
||||
<InputNumber v-model:value="formData.upgrade_rebate.normal_to_gold_rebate" :min="0" :precision="2"
|
||||
:step="0.01" style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item :name="['upgrade_rebate', 'to_diamond_rebate']" label="→钻石返佣">
|
||||
<InputNumber v-model:value="formData.upgrade_rebate.to_diamond_rebate" :min="0" :precision="2"
|
||||
:step="0.01" style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="直接上级返佣配置" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['direct_parent_rebate', 'diamond']" label="直接上级是钻石的返佣金额">
|
||||
<InputNumber v-model:value="formData.direct_parent_rebate.diamond" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['direct_parent_rebate', 'gold']" label="直接上级是黄金的返佣金额">
|
||||
<InputNumber v-model:value="formData.direct_parent_rebate.gold" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['direct_parent_rebate', 'normal']" label="直接上级是普通的返佣金额">
|
||||
<InputNumber v-model:value="formData.direct_parent_rebate.normal" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="返佣限额配置" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item name="max_gold_rebate_amount" label="黄金代理最大返佣金额">
|
||||
<InputNumber v-model:value="formData.max_gold_rebate_amount" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="佣金冻结配置" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['commission_freeze', 'ratio']" label="佣金冻结比例(例如:0.1表示10%)">
|
||||
<InputNumber v-model:value="formData.commission_freeze.ratio" :min="0" :max="1" :precision="4"
|
||||
:step="0.0001" style="width: 100%" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['commission_freeze', 'threshold']" label="佣金冻结阈值">
|
||||
<InputNumber v-model:value="formData.commission_freeze.threshold" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
|
||||
<Form.Item :name="['commission_freeze', 'days']" label="佣金冻结解冻天数">
|
||||
<InputNumber v-model:value="formData.commission_freeze.days" :min="0" :precision="0" :step="1"
|
||||
style="width: 100%" addon-after="天" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card title="税费配置" size="small" class="mb-4">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item name="tax_rate" label="税率(例如:0.06表示6%)">
|
||||
<InputNumber v-model:value="formData.tax_rate" :min="0" :max="1" :precision="4" :step="0.0001"
|
||||
style="width: 100%" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||
<Form.Item name="tax_exemption_amount" label="免税额度">
|
||||
<InputNumber v-model:value="formData.tax_exemption_amount" :min="0" :precision="2" :step="0.01"
|
||||
style="width: 100%" addon-after="元" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="代理系统配置" class="agent-config-card" :loading="loading">
|
||||
<template #extra>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleSave">保存</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
<Button type="primary" size="small" @click="handleSave">
|
||||
保存
|
||||
</Button>
|
||||
<Button size="small" @click="handleReset">重置</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</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>
|
||||
</div>
|
||||
<aside class="agent-config-aside">
|
||||
<AgentConfigPreview :config="formData" />
|
||||
</aside>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</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';
|
||||
|
||||
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 [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '邀请码',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '发放代理ID',
|
||||
field: 'agent_code',
|
||||
title: '发放代理编号',
|
||||
width: 120,
|
||||
cellRender: onIssuerAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onIssuerAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'agent_mobile',
|
||||
@@ -52,9 +61,15 @@ export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'used_agent_id',
|
||||
title: '使用代理ID',
|
||||
field: 'used_agent_code',
|
||||
title: '使用代理编号',
|
||||
width: 120,
|
||||
cellRender: onUsedAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onUsedAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'used_time',
|
||||
@@ -77,7 +92,7 @@ export function useInviteCodeColumns(): VxeTableGridOptions['columns'] {
|
||||
width: 160,
|
||||
sortable: true,
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
}
|
||||
|
||||
export function useInviteCodeFormSchema(): VbenFormSchema[] {
|
||||
@@ -86,11 +101,10 @@ export function useInviteCodeFormSchema(): VbenFormSchema[] {
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: '邀请码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'agent_id',
|
||||
label: '发放代理ID',
|
||||
componentProps: {
|
||||
placeholder: '',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AgentApi } from '#/api/agent';
|
||||
|
||||
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 {
|
||||
generateDiamondInviteCode,
|
||||
getInviteCodeList,
|
||||
} 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 {
|
||||
currentPage: number;
|
||||
@@ -21,6 +25,8 @@ interface QueryParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const generateModalVisible = ref(false);
|
||||
const generatedCodes = ref<string[]>([]);
|
||||
const generateForm = ref({
|
||||
@@ -29,24 +35,30 @@ const generateForm = ref({
|
||||
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({
|
||||
formOptions: {
|
||||
schema: useInviteCodeFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useInviteCodeColumns(),
|
||||
columns: useInviteCodeColumns(onInviteIssuerCode, onInviteUsedCode),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||
return await getInviteCodeList({
|
||||
...form,
|
||||
...formValues,
|
||||
target_level: 3,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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 [
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'product_id',
|
||||
title: '产品ID',
|
||||
width: 100,
|
||||
field: 'agent_code',
|
||||
title: '代理编号',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'product_name',
|
||||
@@ -48,20 +56,14 @@ export function useLinkColumns(): VxeTableGridOptions['columns'] {
|
||||
// 推广链接搜索表单配置
|
||||
export function useLinkFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'product_id',
|
||||
label: '产品ID',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'product_name',
|
||||
label: '产品名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'link_identifier',
|
||||
label: '推广码',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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 {
|
||||
agentId?: number;
|
||||
@@ -20,29 +22,31 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||
}));
|
||||
|
||||
function onLinkAgentCode(row: AgentLinkRow) {
|
||||
if (row.agent_code != null) {
|
||||
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useLinkFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useLinkColumns(),
|
||||
columns: useLinkColumns(onLinkAgentCode),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||
return await getAgentLinkList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { AgentApi } from '#/api/agent';
|
||||
|
||||
import { getLevelName } from '#/utils/agent';
|
||||
|
||||
@@ -48,6 +49,20 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'real_name',
|
||||
label: '姓名',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '实名姓名,模糊匹配',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'region',
|
||||
@@ -66,38 +81,53 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'team_leader_id',
|
||||
label: '团队首领ID',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'create_time',
|
||||
label: '创建时间',
|
||||
label: '成为代理时间',
|
||||
componentProps: {
|
||||
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 = [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'user_id',
|
||||
title: '用户ID',
|
||||
width: 100,
|
||||
width: 110,
|
||||
cellRender: onUserIdClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: {
|
||||
onClick: onUserIdClick,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'agent_code',
|
||||
title: '代理编码',
|
||||
title: '代理编号',
|
||||
width: 100,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: {
|
||||
onClick: onAgentCodeClick,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
@@ -129,6 +159,23 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '实名认证状态',
|
||||
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',
|
||||
title: '微信号',
|
||||
@@ -145,29 +192,51 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'balance',
|
||||
title: '钱包余额',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'total_earnings',
|
||||
title: '累计收益',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'frozen_balance',
|
||||
title: '冻结余额',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'withdrawn_amount',
|
||||
title: '提现总额',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: unknown }) => fmtMoney(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'total_orders',
|
||||
title: '订单总数',
|
||||
width: 100,
|
||||
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',
|
||||
@@ -182,7 +251,7 @@ export function useColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'operation',
|
||||
fixed: 'right' as const,
|
||||
title: '操作',
|
||||
width: 280,
|
||||
width: 240,
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
@@ -234,11 +303,6 @@ export function useLinkFormSchema(): VbenFormSchema[] {
|
||||
// 佣金记录列表列配置
|
||||
export function useCommissionColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
@@ -310,11 +374,6 @@ export function useCommissionFormSchema(): VbenFormSchema[] {
|
||||
// 奖励记录列表列配置
|
||||
export function useRewardColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
@@ -365,11 +424,6 @@ export function useRewardFormSchema(): VbenFormSchema[] {
|
||||
// 提现记录列表列配置
|
||||
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
|
||||
@@ -6,29 +6,193 @@ import type {
|
||||
} from '#/adapter/vxe-table';
|
||||
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 { 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 { 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';
|
||||
|
||||
/** 金额类列在表格级 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 Form from './modules/form.vue';
|
||||
import LinkModal from './modules/link-modal.vue';
|
||||
import OrderModal from './modules/order-modal.vue';
|
||||
import RebateModal from './modules/rebate-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 WithdrawalModal from './modules/withdrawal-modal.vue';
|
||||
|
||||
const route = useRoute();
|
||||
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({
|
||||
connectedComponent: Form,
|
||||
@@ -77,6 +241,12 @@ const [WithdrawalModalComponent, withdrawalModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 团队树弹窗
|
||||
const [TeamTreeModalComponent, teamTreeModalApi] = useVbenModal({
|
||||
connectedComponent: TeamTreeModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 表格配置
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
@@ -92,7 +262,20 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeGridListeners<AgentApi.AgentListItem>,
|
||||
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',
|
||||
keepSource: true,
|
||||
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({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
...sortParams,
|
||||
team_leader_id: route.query.team_leader_id
|
||||
? Number(route.query.team_leader_id)
|
||||
: undefined,
|
||||
team_leader_id:
|
||||
shot?.agent_id || urlAgentId || !route.query.team_leader_id
|
||||
? 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 {
|
||||
...res,
|
||||
sort: sort || null,
|
||||
@@ -132,7 +335,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
},
|
||||
autoLoad: true,
|
||||
autoLoad: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
@@ -147,6 +350,102 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
} 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 = [
|
||||
{
|
||||
@@ -157,6 +456,10 @@ const moreMenuItems = [
|
||||
key: 'links',
|
||||
label: '推广链接',
|
||||
},
|
||||
{
|
||||
key: 'commission',
|
||||
label: '佣金记录',
|
||||
},
|
||||
{
|
||||
key: 'rebate',
|
||||
label: '返佣记录',
|
||||
@@ -178,14 +481,16 @@ const moreMenuItems = [
|
||||
// 团队首领信息
|
||||
const teamLeaderId = computed(() => route.query.team_leader_id);
|
||||
|
||||
// 返回团队首领列表
|
||||
function onBackToParent() {
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
team_leader_id: undefined,
|
||||
},
|
||||
});
|
||||
function onOpenTeamTree(row: AgentApi.AgentListItem) {
|
||||
teamTreeModalApi
|
||||
.setData({
|
||||
anchorAgentId: String(row.id),
|
||||
onLocate: ({ agentId }: { agentId: string }) => {
|
||||
navigateToAgentList(router, { agentId });
|
||||
teamTreeModalApi.close();
|
||||
},
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
// 操作处理函数
|
||||
@@ -223,15 +528,6 @@ function onActionClick(
|
||||
onViewOrder(e.row);
|
||||
break;
|
||||
}
|
||||
case 'view-sub-agent': {
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
team_leader_id: e.row.id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'withdrawal': {
|
||||
onViewWithdrawal(e.row);
|
||||
break;
|
||||
@@ -297,12 +593,53 @@ function onRefresh() {
|
||||
<UpgradeModalComponent />
|
||||
<OrderModalComponent />
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button @click="onBackToParent">返回上级列表</Button>
|
||||
<div>团队首领ID:{{ teamLeaderId }}</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Button :disabled="navStack.length <= 1" @click="navBackOne">
|
||||
返回上一级
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
@@ -312,8 +649,8 @@ function onRefresh() {
|
||||
<Button type="link" @click="onActionClick({ code: 'edit', row })">
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" @click="onActionClick({ code: 'view-sub-agent', row })">
|
||||
查看下级
|
||||
<Button type="link" @click="onOpenTeamTree(row)">
|
||||
查看团队
|
||||
</Button>
|
||||
<!-- <Button
|
||||
type="link"
|
||||
|
||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
||||
<template>
|
||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||
<div class="agent-commission-modal">
|
||||
<CommissionList :agent-id="modalData?.agentId" />
|
||||
<CommissionList
|
||||
:agent-id="modalData?.agentId"
|
||||
:navigate-away="() => modalApi.close()"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
void (await formApi.getValues());
|
||||
drawerApi.lock();
|
||||
// TODO: 实现更新代理信息的接口
|
||||
// updateAgent(id.value, values as AgentApi.UpdateAgentRequest)
|
||||
|
||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
||||
<template>
|
||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||
<div class="agent-order-modal">
|
||||
<OrderList :agent-id="modalData?.agentId" />
|
||||
<OrderList
|
||||
:agent-id="modalData?.agentId"
|
||||
:navigate-away="() => modalApi.close()"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -46,8 +46,9 @@ const canUpgrade = computed(() => targetLevelOptions.value.length > 0);
|
||||
|
||||
// 当可选目标等级变化时(如打开弹窗、切换代理),重置选中为第一项
|
||||
watch(targetLevelOptions, (opts) => {
|
||||
if (opts.length) {
|
||||
selectedToLevel.value = opts[0].value;
|
||||
const first = opts[0];
|
||||
if (first) {
|
||||
selectedToLevel.value = first.value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ const modalData = computed(() => modalApi.getData<ModalData>());
|
||||
<template>
|
||||
<Modal class="w-[calc(100vw-200px)]" :footer="false">
|
||||
<div class="agent-rebate-modal">
|
||||
<RebateList :agent-id="modalData?.agentId" />
|
||||
<RebateList
|
||||
:agent-id="modalData?.agentId"
|
||||
:navigate-away="() => modalApi.close()"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</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 { 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 [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
field: 'agent_code',
|
||||
title: '推广代理编号',
|
||||
width: 130,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
width: 100,
|
||||
field: 'order_no',
|
||||
title: '商户订单号',
|
||||
minWidth: 180,
|
||||
cellRender: onOrderNoClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onOrderNoClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'order_id',
|
||||
title: '订单ID',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'product_id',
|
||||
title: '产品ID',
|
||||
field: 'order_status',
|
||||
title: '订单状态',
|
||||
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_name',
|
||||
@@ -35,35 +57,35 @@ export function useOrderColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '订单金额',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
`¥${Number(cellValue).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'set_price',
|
||||
title: '设定价格',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
`¥${Number(cellValue).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'actual_base_price',
|
||||
title: '实际底价',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
`¥${Number(cellValue).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'price_cost',
|
||||
title: '提价成本',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
`¥${Number(cellValue).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'agent_profit',
|
||||
title: '代理收益',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${cellValue.toFixed(2)}`,
|
||||
`¥${Number(cellValue).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'process_status',
|
||||
@@ -84,20 +106,54 @@ export function useOrderColumns(): VxeTableGridOptions['columns'] {
|
||||
width: 160,
|
||||
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[] {
|
||||
return [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'agent_id',
|
||||
label: '代理ID',
|
||||
component: 'Input',
|
||||
fieldName: 'agent_code',
|
||||
label: '推广代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入推广代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'order_id',
|
||||
label: '订单ID',
|
||||
component: 'Input',
|
||||
fieldName: 'order_no',
|
||||
label: '商户订单号',
|
||||
componentProps: {
|
||||
placeholder: '请输入商户订单号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
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 { 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 {
|
||||
agentId?: number;
|
||||
/** 嵌套在弹窗内时,跳转到订单/代理列表后关闭外层弹窗 */
|
||||
navigateAway?: () => void;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
@@ -20,29 +32,70 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(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({
|
||||
formOptions: {
|
||||
schema: useOrderFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useOrderColumns(),
|
||||
columns: useOrderColumns(
|
||||
onOrderAgentCode,
|
||||
onOrderNoClick,
|
||||
onOrderOperationClick,
|
||||
),
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: false,
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentOrderList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
@@ -53,12 +106,13 @@ const [Grid] = useVbenVxeGrid({
|
||||
total: 'total',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<AgentOrderRow>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="!agentId">
|
||||
<SettlementModalComponent />
|
||||
<Grid :table-title="agentId ? '订单记录列表' : '所有订单记录'" />
|
||||
</Page>
|
||||
</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'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'product_name',
|
||||
title: '产品名称',
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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 [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
field: 'agent_code',
|
||||
title: '代理编号',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
@@ -59,15 +72,19 @@ export function useRealNameColumns(): VxeTableGridOptions['columns'] {
|
||||
width: 160,
|
||||
sortable: true,
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
}
|
||||
|
||||
export function useRealNameFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
component: 'Input',
|
||||
fieldName: 'agent_id',
|
||||
label: '代理ID',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理ID',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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 {
|
||||
agentId?: number;
|
||||
@@ -20,29 +26,34 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||
}));
|
||||
|
||||
function onRealNameAgentCode(row: AgentRealNameRow) {
|
||||
if (row.agent_code != null) {
|
||||
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useRealNameFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useRealNameColumns(),
|
||||
columns: useRealNameColumns(onRealNameAgentCode),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentRealNameList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
|
||||
@@ -3,27 +3,60 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
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 [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
field: 'agent_code',
|
||||
title: '代理编号',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'source_agent_id',
|
||||
title: '来源代理ID',
|
||||
field: 'source_agent_code',
|
||||
title: '来源代理编号',
|
||||
width: 120,
|
||||
cellRender: onSourceAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onSourceAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'order_id',
|
||||
title: '订单ID',
|
||||
width: 100,
|
||||
field: 'order_no',
|
||||
title: '商户订单号',
|
||||
minWidth: 180,
|
||||
cellRender: onOrderNoClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onOrderNoClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'rebate_type',
|
||||
@@ -40,26 +73,56 @@ export function useRebateColumns(): VxeTableGridOptions['columns'] {
|
||||
formatter: ({ cellValue }: { cellValue: number }) =>
|
||||
`¥${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',
|
||||
title: '创建时间',
|
||||
width: 160,
|
||||
sortable: true,
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
}
|
||||
|
||||
export function useRebateFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'agent_id',
|
||||
label: '代理ID',
|
||||
component: 'Input',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'source_agent_id',
|
||||
label: '来源代理ID',
|
||||
component: 'Input',
|
||||
fieldName: 'source_agent_code',
|
||||
label: '来源代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入来源代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'order_no',
|
||||
label: '商户订单号',
|
||||
componentProps: {
|
||||
placeholder: '请输入商户订单号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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 {
|
||||
agentId?: number;
|
||||
/** 嵌套在弹窗内时,跳转到订单/代理列表后调用(用于关闭弹窗) */
|
||||
navigateAway?: () => void;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
@@ -20,29 +31,56 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(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({
|
||||
formOptions: {
|
||||
schema: useRebateFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useRebateColumns(),
|
||||
columns: useRebateColumns(onRebateAgentCode, onRebateSourceAgentCode, onOrderNoClick),
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: false,
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentRebateList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
@@ -53,7 +91,7 @@ const [Grid] = useVbenVxeGrid({
|
||||
total: 'total',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<AgentRebateRow>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,16 +33,13 @@ const [Grid] = useVbenVxeGrid({
|
||||
columns: useRewardColumns(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentRewardList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
getLevelName,
|
||||
getUpgradeStatusName,
|
||||
getUpgradeTypeName,
|
||||
} from '#/utils/agent';
|
||||
import { getLevelName, 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 [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'agent_id',
|
||||
title: '代理ID',
|
||||
width: 100,
|
||||
field: 'agent_code',
|
||||
title: '代理编号',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
field: 'from_level',
|
||||
@@ -64,7 +68,7 @@ export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
|
||||
cellRender: {
|
||||
name: 'CellTag',
|
||||
options: [
|
||||
{ value: 1, color: 'warning', label: '待处理' },
|
||||
{ value: 1, color: 'warning', label: '待支付' },
|
||||
{ value: 2, color: 'success', label: '已完成' },
|
||||
{ value: 3, color: 'error', label: '已失败' },
|
||||
],
|
||||
@@ -76,15 +80,19 @@ export function useUpgradeColumns(): VxeTableGridOptions['columns'] {
|
||||
width: 160,
|
||||
sortable: true,
|
||||
},
|
||||
] as const;
|
||||
];
|
||||
}
|
||||
|
||||
export function useUpgradeFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'agent_id',
|
||||
label: '代理ID',
|
||||
component: 'Input',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
@@ -105,7 +113,7 @@ export function useUpgradeFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '待支付', value: 1 },
|
||||
{ label: '已完成', value: 2 },
|
||||
{ label: '已失败', value: 3 },
|
||||
],
|
||||
@@ -113,4 +121,3 @@ export function useUpgradeFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
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 {
|
||||
agentId?: number;
|
||||
@@ -20,29 +26,34 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(props.agentId ? { agent_id: props.agentId } : {}),
|
||||
}));
|
||||
|
||||
function onUpgradeAgentCode(row: AgentUpgradeRow) {
|
||||
if (row.agent_code != null) {
|
||||
navigateToAgentList(router, { agentCode: row.agent_code });
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useUpgradeFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useUpgradeColumns(),
|
||||
columns: useUpgradeColumns(onUpgradeAgentCode),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
form: Record<string, any>;
|
||||
page: QueryParams;
|
||||
}) => {
|
||||
query: async (
|
||||
{ page }: { page: QueryParams },
|
||||
formValues: Record<string, any>,
|
||||
) => {
|
||||
return await getAgentUpgradeList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AgentApi } from '#/api/agent';
|
||||
|
||||
export function useWithdrawalColumns(
|
||||
onActionClick?: OnActionClickFn<AgentApi.AgentWithdrawalListItem>,
|
||||
onAgentCodeClick?: (row: AgentApi.AgentWithdrawalListItem) => void,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
@@ -14,9 +15,15 @@ export function useWithdrawalColumns(
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '代理ID',
|
||||
field: 'agent_id',
|
||||
width: 100,
|
||||
title: '代理编号',
|
||||
field: 'agent_code',
|
||||
width: 110,
|
||||
cellRender: onAgentCodeClick
|
||||
? {
|
||||
name: 'CellLink',
|
||||
props: { onClick: onAgentCodeClick },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
@@ -40,12 +47,12 @@ export function useWithdrawalColumns(
|
||||
title: '提现方式',
|
||||
field: 'withdrawal_type',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }: { cellValue: number }) => {
|
||||
const methodMap: Record<number, string> = {
|
||||
1: '支付宝',
|
||||
2: '银行卡',
|
||||
};
|
||||
return methodMap[cellValue] || '未知';
|
||||
cellRender: {
|
||||
name: 'CellTag',
|
||||
options: [
|
||||
{ value: 1, color: 'processing', label: '支付宝' },
|
||||
{ value: 2, color: 'success', label: '银行卡' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -126,16 +133,16 @@ export function useWithdrawalFormSchema(): VbenFormSchema[] {
|
||||
label: '提现单号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入提现单号',
|
||||
placeholder: '支持模糊匹配',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'agent_id',
|
||||
label: '代理ID',
|
||||
fieldName: 'agent_code',
|
||||
label: '代理编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入代理ID',
|
||||
placeholder: '请输入代理编号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
@@ -144,10 +151,28 @@ export function useWithdrawalFormSchema(): VbenFormSchema[] {
|
||||
label: '提现方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '支付宝', value: 1 },
|
||||
{ label: '银行卡', value: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'payee_account',
|
||||
label: '收款账户',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '支持模糊匹配',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'payee_name',
|
||||
label: '收款人',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '支持模糊匹配',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getAgentWithdrawalList } 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 { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
|
||||
@@ -22,10 +24,18 @@ interface QueryParams {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryParams = computed(() => ({
|
||||
...(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({
|
||||
connectedComponent: AuditModal,
|
||||
@@ -59,19 +69,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useWithdrawalColumns(handleActionClick),
|
||||
columns: useWithdrawalColumns(handleActionClick, onWithdrawalAgentCode),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
form,
|
||||
}: {
|
||||
page: QueryParams;
|
||||
form: Record<string, any>;
|
||||
}) => {
|
||||
query: async ({ page }: { page: QueryParams }, formValues: Record<string, any>) => {
|
||||
return await getAgentWithdrawalList({
|
||||
...queryParams.value,
|
||||
...form,
|
||||
...formValues,
|
||||
page: page.currentPage,
|
||||
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',
|
||||
fieldName: 'title',
|
||||
label: '通知标题',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '模糊匹配标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
component: 'Input',
|
||||
fieldName: 'notification_page',
|
||||
label: '通知页面',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '首页', value: 'home' },
|
||||
{ label: '个人中心', value: 'profile' },
|
||||
{ label: '订单页', value: 'order' },
|
||||
],
|
||||
placeholder: '模糊匹配路径',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -79,8 +79,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '禁用', value: 'inactive' },
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -115,7 +115,7 @@ export function useColumns<T = NotificationApi.NotificationItem>(
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: 'show_date',
|
||||
field: 'start_date',
|
||||
title: '展示日期',
|
||||
width: 300,
|
||||
formatter: ({ row }) => {
|
||||
@@ -125,7 +125,7 @@ export function useColumns<T = NotificationApi.NotificationItem>(
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'show_time',
|
||||
field: 'start_time',
|
||||
title: '展示时间',
|
||||
width: 300,
|
||||
formatter: ({ row }) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Button, message, Modal } from 'ant-design-vue';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteNotification,
|
||||
getNotificationDetail,
|
||||
getNotificationList,
|
||||
updateNotification,
|
||||
} 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({
|
||||
formOptions: {
|
||||
fieldMappingTime: [['date', ['startDate', 'endDate']]],
|
||||
fieldMappingTime: [['date_range', ['start_date', 'end_date']]],
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
keepSource: false,
|
||||
proxyConfig: {
|
||||
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({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
...buildNotificationListParams(filters),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -125,17 +159,8 @@ async function onStatusChange(
|
||||
`你要将通知"${row.title}"的状态切换为【${status[newStatus]}】吗?`,
|
||||
'切换状态',
|
||||
);
|
||||
// 获取完整的通知数据
|
||||
const notification = await getNotificationList({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
id: row.id,
|
||||
});
|
||||
const fullData = notification.items[0];
|
||||
if (!fullData) {
|
||||
message.error('获取通知数据失败');
|
||||
return false;
|
||||
}
|
||||
// 列表接口不支持按 id 筛选,必须用详情接口取完整字段再更新
|
||||
const fullData = await getNotificationDetail(row.id);
|
||||
await updateNotification(row.id, {
|
||||
id: row.id,
|
||||
status: newStatus,
|
||||
@@ -147,6 +172,7 @@ async function onStatusChange(
|
||||
end_date: fullData.end_date,
|
||||
end_time: fullData.end_time,
|
||||
});
|
||||
onRefresh();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -78,7 +78,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
));
|
||||
emit('success');
|
||||
drawerApi.close();
|
||||
} catch {
|
||||
} finally {
|
||||
drawerApi.unlock();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,8 +2,11 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { OrderApi } from '#/api/order';
|
||||
|
||||
type AgentClickFn = (row: OrderApi.Order) => void;
|
||||
|
||||
export function useColumns<T = OrderApi.Order>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
onAgentClick?: AgentClickFn,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
@@ -61,6 +64,45 @@ export function useColumns<T = OrderApi.Order>(
|
||||
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: {
|
||||
name: 'CellTag',
|
||||
@@ -130,7 +172,7 @@ export function useColumns<T = OrderApi.Order>(
|
||||
},
|
||||
},
|
||||
{
|
||||
code: 'query',
|
||||
code: 'openOrderQueryResultPage',
|
||||
text: '查询结果',
|
||||
disabled: (row: OrderApi.Order) => {
|
||||
return row.query_state !== 'success';
|
||||
@@ -205,6 +247,23 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'status',
|
||||
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',
|
||||
fieldName: 'create_time',
|
||||
|
||||
@@ -5,11 +5,17 @@ import type {
|
||||
} from '#/adapter/vxe-table';
|
||||
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 { 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 { useColumns, useGridFormSchema } from './data';
|
||||
@@ -26,7 +32,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
columns: useColumns(onActionClick, onAgentClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -46,7 +52,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
refresh: { code: 'query' },
|
||||
// 关闭工具栏刷新,避免与代理 query / 误触操作列逻辑产生冲突;可用搜索区「搜索」刷新列表
|
||||
refresh: false,
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
@@ -58,15 +65,25 @@ const [RefundDrawer, refundDrawerApi] = useVbenDrawer({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
|
||||
switch (e.code) {
|
||||
// 历史/误触:勿与表格代理 code 混用
|
||||
case 'query': {
|
||||
return;
|
||||
}
|
||||
case 'openOrderQueryResultPage': {
|
||||
const id = e.row?.id;
|
||||
const idStr = id == null ? '' : String(id).trim();
|
||||
if (!idStr || idStr === ':id') {
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
name: 'OrderQueryDetail',
|
||||
params: {
|
||||
id: e.row.id,
|
||||
id: idStr,
|
||||
},
|
||||
});
|
||||
break;
|
||||
@@ -75,6 +92,9 @@ function onActionClick(e: OnActionClickParams<OrderApi.Order>) {
|
||||
onRefund(e.row);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +109,89 @@ function onRefundSuccess() {
|
||||
function onRefresh() {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -22,7 +22,25 @@ import { getOrderQueryDetail } from '#/api/order/query';
|
||||
|
||||
const route = useRoute();
|
||||
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 queryDetail = ref<OrderQueryApi.QueryDetail>();
|
||||
|
||||
@@ -84,7 +102,10 @@ function handleBack() {
|
||||
|
||||
// 获取查询详情
|
||||
async function fetchQueryDetail() {
|
||||
if (!orderId) return;
|
||||
if (!isValidOrderIdForQuery(orderId)) {
|
||||
await router.replace({ path: '/order', query: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
|
||||
@@ -42,6 +42,15 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
// 搜索表单配置
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'user_id',
|
||||
label: '用户ID',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '平台用户主键,精确查询',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'mobile',
|
||||
|
||||
@@ -6,14 +6,24 @@ import type {
|
||||
} from '#/adapter/vxe-table';
|
||||
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 { 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 { useColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 表单抽屉
|
||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: Form,
|
||||
@@ -108,6 +118,88 @@ function onEdit(row: PlatformUserApi.PlatformUserItem) {
|
||||
function onRefresh() {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -28,16 +28,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'notes',
|
||||
label: '备注',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
fieldName: 'cost_price',
|
||||
label: '成本价',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
@@ -88,9 +78,9 @@ export function useColumns<T = ProductApi.ProductItem>(
|
||||
},
|
||||
{
|
||||
field: 'cost_price',
|
||||
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
|
||||
title: '成本价',
|
||||
width: 120,
|
||||
formatter: ({ cellValue }) => `¥${(cellValue || 0).toFixed(2)}`,
|
||||
title: '成本价(模块合计)',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
field: 'sell_price',
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
import type { FeatureApi } from '#/api/product-manage/feature';
|
||||
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';
|
||||
|
||||
@@ -85,6 +85,14 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const loading = ref(false);
|
||||
const tempFeatureList = ref<TempFeatureItem[]>([]);
|
||||
|
||||
/** 已关联模块成本合计(与后台汇总规则一致:各模块 cost_price 相加) */
|
||||
const totalLinkedCost = computed(() =>
|
||||
tempFeatureList.value.reduce(
|
||||
(sum, row) => sum + Number(row.cost_price ?? 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
// 表格配置
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
@@ -166,6 +174,15 @@ const columns: TableColumnsType<TempFeatureItem> = [
|
||||
title: '模块描述',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '模块成本(元)',
|
||||
dataIndex: 'cost_price',
|
||||
width: 130,
|
||||
customRender: ({ record }) => {
|
||||
const v = Number(record.cost_price ?? 0);
|
||||
return `¥${v.toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
dataIndex: 'enable',
|
||||
@@ -303,6 +320,7 @@ function handleAddFeature(feature: FeatureApi.FeatureItem) {
|
||||
feature_id: feature.id,
|
||||
api_id: feature.api_id,
|
||||
name: feature.name,
|
||||
cost_price: feature.cost_price ?? 0,
|
||||
sort: maxSort + 1,
|
||||
enable: 1,
|
||||
is_important: 0,
|
||||
@@ -342,7 +360,15 @@ function handleRemoveFeature(record: TempFeatureItem) {
|
||||
</div>
|
||||
<!-- 右侧:已关联模块列表 -->
|
||||
<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>
|
||||
|
||||
@@ -70,11 +70,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: 'api_name',
|
||||
label: $t('system.api.apiName'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'api_code',
|
||||
label: $t('system.api.apiCode'),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
@@ -103,11 +98,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: '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({
|
||||
formOptions: {
|
||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
|
||||
@@ -51,10 +51,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'role_name',
|
||||
fieldName: 'name',
|
||||
label: $t('system.role.roleName'),
|
||||
},
|
||||
{ component: 'Input', fieldName: 'id', label: $t('system.role.id') },
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
@@ -67,16 +66,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: '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({
|
||||
formOptions: {
|
||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
|
||||
@@ -76,11 +76,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
fieldName: '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: [
|
||||
{ code: 'edit', text: '编辑' },
|
||||
{ code: 'resetPassword', text: '重置密码' },
|
||||
{ code: 'changePassword', text: $t('system.user.changePassword') },
|
||||
{ code: 'delete', text: '删除' },
|
||||
],
|
||||
name: 'CellOperation',
|
||||
|
||||
@@ -25,14 +25,13 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [ResetPasswordDrawer, resetPasswordDrawerApi] = useVbenDrawer({
|
||||
const [ChangePasswordDrawer, changePasswordDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: ResetPasswordForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
fieldMappingTime: [['create_time', ['startTime', 'endTime']]],
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
@@ -75,8 +74,8 @@ function onActionClick(e: OnActionClickParams<SystemUserApi.SystemUser>) {
|
||||
onEdit(e.row);
|
||||
break;
|
||||
}
|
||||
case 'resetPassword': {
|
||||
onResetPassword(e.row);
|
||||
case 'changePassword': {
|
||||
onChangePassword(e.row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -132,8 +131,8 @@ function onEdit(row: SystemUserApi.SystemUser) {
|
||||
formDrawerApi.setData(row).open();
|
||||
}
|
||||
|
||||
function onResetPassword(row: SystemUserApi.SystemUser) {
|
||||
resetPasswordDrawerApi.setData(row).open();
|
||||
function onChangePassword(row: SystemUserApi.SystemUser) {
|
||||
changePasswordDrawerApi.setData(row).open();
|
||||
}
|
||||
|
||||
function onDelete(row: SystemUserApi.SystemUser) {
|
||||
@@ -166,7 +165,7 @@ function onCreate() {
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormDrawer />
|
||||
<ResetPasswordDrawer @success="onRefresh" />
|
||||
<ChangePasswordDrawer @success="onRefresh" />
|
||||
<Grid :table-title="$t('system.user.list')">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
|
||||
@@ -3,10 +3,13 @@ import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenDrawer, z } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { resetPassword } from '#/api/system/user';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
@@ -51,6 +54,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
drawerApi.lock();
|
||||
resetPassword(id.value, { password: values.password })
|
||||
.then(() => {
|
||||
message.success($t('system.user.changePasswordSuccess'));
|
||||
drawerApi.unlock();
|
||||
emits('success');
|
||||
drawerApi.close();
|
||||
})
|
||||
@@ -72,9 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return '重置密码';
|
||||
});
|
||||
const getDrawerTitle = computed(() => $t('system.user.changePassword'));
|
||||
</script>
|
||||
<template>
|
||||
<Drawer :title="getDrawerTitle">
|
||||
|
||||
Reference in New Issue
Block a user