20261030 #16
@ -75,7 +75,7 @@ OMS 当前是 **单运营方部署**(非多租户 SaaS):客户挂 `t_custo
|
|||||||
| 10 | **可见费用** | 代理可见客户侧应收/实付与返佣金额;**不可见** `payable` / 买价 / 毛利 |
|
| 10 | **可见费用** | 代理可见客户侧应收/实付与返佣金额;**不可见** `payable` / 买价 / 毛利 |
|
||||||
| 11 | **换绑** | 首版不做自助换绑;运营改 **主客户** `AgentId`(仅影响之后新单) |
|
| 11 | **换绑** | 首版不做自助换绑;运营改 **主客户** `AgentId`(仅影响之后新单) |
|
||||||
| 12 | **子客户归属** | **跟主客户**:快照前 `ResolveMainCustomer` |
|
| 12 | **子客户归属** | **跟主客户**:快照前 `ResolveMainCustomer` |
|
||||||
| 13 | **计提触发** | **`deliverySuccess` / `deliveryFailed`** 均计提;同单按 `OrderId` 幂等 |
|
| 13 | **计提触发** | **下单写 `estimated`(预计)**;**`deliverySuccess` / `deliveryFailed` 锁定为 `pending`**;取消/全额退 → `void`;同单按 `OrderId` 幂等 |
|
||||||
| 14 | **首版资金形态** | **只写返佣明细,不冻结、不入余额、不提现**;运营按清单线下打款;错账人工改明细(P2+ 再账户化) |
|
| 14 | **首版资金形态** | **只写返佣明细,不冻结、不入余额、不提现**;运营按清单线下打款;错账人工改明细(P2+ 再账户化) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -170,7 +170,8 @@ OMS 当前是 **单运营方部署**(非多租户 SaaS):客户挂 `t_custo
|
|||||||
→ 客户经链接注册 → 写 Customer.AgentId
|
→ 客户经链接注册 → 写 Customer.AgentId
|
||||||
→ 客户正常查价 / 下单 / 支付(价格逻辑不变)
|
→ 客户正常查价 / 下单 / 支付(价格逻辑不变)
|
||||||
→ 订单快照 Order.AgentId(跟主客户)
|
→ 订单快照 Order.AgentId(跟主客户)
|
||||||
→ 派送成功/失败 → 写一条返佣明细(金额快照)
|
→ 下单 → 写预计返佣(estimated)
|
||||||
|
→ 派送成功/失败 → 锁定为待结算(pending,金额重算快照)
|
||||||
→ 运营按明细线下打款
|
→ 运营按明细线下打款
|
||||||
→(P2+)冻结入账 / 提现 / 毛利基数 / 自动 clawback
|
→(P2+)冻结入账 / 提现 / 毛利基数 / 自动 clawback
|
||||||
```
|
```
|
||||||
|
|||||||
@ -11,8 +11,10 @@ export interface AgentDto {
|
|||||||
email?: string
|
email?: string
|
||||||
status: AgentStatus
|
status: AgentStatus
|
||||||
baseType: string
|
baseType: string
|
||||||
|
baseTypeName?: string
|
||||||
commissionRate: number
|
commissionRate: number
|
||||||
salesProductIds?: string
|
salesProductIds?: string
|
||||||
|
salesProductNames?: string[]
|
||||||
remark?: string
|
remark?: string
|
||||||
createTime?: string
|
createTime?: string
|
||||||
}
|
}
|
||||||
@ -46,6 +48,9 @@ export interface AgentPortalCustomerDto {
|
|||||||
phone?: string
|
phone?: string
|
||||||
agentBindTime?: string
|
agentBindTime?: string
|
||||||
createTime?: string
|
createTime?: string
|
||||||
|
lastOrderTime?: string
|
||||||
|
orderCount30d?: number
|
||||||
|
pendingPaymentCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentPortalOrderDto {
|
export interface AgentPortalOrderDto {
|
||||||
@ -62,6 +67,47 @@ export interface AgentPortalOrderDto {
|
|||||||
createTime?: string
|
createTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentWorkplaceStats {
|
||||||
|
customerCount: number
|
||||||
|
todayCreated: number
|
||||||
|
yesterdayCreated: number
|
||||||
|
dayOverDayGrowthPercent?: number | null
|
||||||
|
pendingPayment: number
|
||||||
|
shipping: number
|
||||||
|
problematic: number
|
||||||
|
deliverySuccess: number
|
||||||
|
deliveryFailed: number
|
||||||
|
estimatedCommissionCount?: number
|
||||||
|
estimatedCommissionAmount?: number
|
||||||
|
pendingCommissionCount: number
|
||||||
|
pendingCommissionAmount: number
|
||||||
|
paidCommissionAmount: number
|
||||||
|
weekNewCustomers: number
|
||||||
|
counts?: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentCommissionListQuery {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: string
|
||||||
|
keyword?: string
|
||||||
|
createTimeStart?: string
|
||||||
|
createTimeEnd?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentWorkplaceTrendPoint {
|
||||||
|
label: string
|
||||||
|
start?: string
|
||||||
|
end?: string
|
||||||
|
orderCount: number
|
||||||
|
commissionAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentWorkplaceTrend {
|
||||||
|
dimension: string
|
||||||
|
points: AgentWorkplaceTrendPoint[]
|
||||||
|
}
|
||||||
|
|
||||||
interface PageResult<T> {
|
interface PageResult<T> {
|
||||||
items: T[]
|
items: T[]
|
||||||
total: number
|
total: number
|
||||||
@ -75,6 +121,26 @@ function unwrapPage<T>(data: any): PageResult<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 匿名校验代理编号(注册页) */
|
||||||
|
export async function validateAgentCode(code: string): Promise<{
|
||||||
|
valid: boolean
|
||||||
|
agentCode?: string
|
||||||
|
agentNameMasked?: string
|
||||||
|
message?: string
|
||||||
|
}> {
|
||||||
|
const r = await axiosInstance.get(omsUrl('/agent/public/validate-code'), {
|
||||||
|
params: { code },
|
||||||
|
})
|
||||||
|
assertBusinessOk(r.data)
|
||||||
|
const raw = (r.data?.data ?? r.data) as Record<string, unknown>
|
||||||
|
return {
|
||||||
|
valid: Boolean(raw.valid ?? raw.Valid),
|
||||||
|
agentCode: String(raw.agentCode ?? raw.AgentCode ?? '').trim() || undefined,
|
||||||
|
agentNameMasked: String(raw.agentNameMasked ?? raw.AgentNameMasked ?? '').trim() || undefined,
|
||||||
|
message: String(raw.message ?? raw.Message ?? '').trim() || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const AgentAdminApi = {
|
export const AgentAdminApi = {
|
||||||
async list(body: {
|
async list(body: {
|
||||||
page?: number
|
page?: number
|
||||||
@ -143,11 +209,68 @@ export const AgentPortalApi = {
|
|||||||
return unwrapPage(r.data)
|
return unwrapPage(r.data)
|
||||||
},
|
},
|
||||||
|
|
||||||
async commissions(body: {
|
async commissions(body: AgentCommissionListQuery): Promise<PageResult<AgentCommissionDto>> {
|
||||||
page?: number
|
|
||||||
pageSize?: number
|
|
||||||
}): Promise<PageResult<AgentCommissionDto>> {
|
|
||||||
const r = await axiosInstance.post(omsUrl('/agent/portal/commission/list'), body)
|
const r = await axiosInstance.post(omsUrl('/agent/portal/commission/list'), body)
|
||||||
return unwrapPage(r.data)
|
return unwrapPage(r.data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async exportCommissions(body: AgentCommissionListQuery): Promise<void> {
|
||||||
|
const r = await axiosInstance.post(omsUrl('/agent/portal/commission/export'), body, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
const blob = r.data instanceof Blob ? r.data : new Blob([r.data])
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `代理返佣明细_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
},
|
||||||
|
|
||||||
|
async workplaceStats(): Promise<AgentWorkplaceStats> {
|
||||||
|
const r = await axiosInstance.get(omsUrl('/agent/portal/workplace/stats'))
|
||||||
|
assertBusinessOk(r.data)
|
||||||
|
const raw = (r.data?.data ?? r.data) as Record<string, unknown>
|
||||||
|
const growthRaw = raw.dayOverDayGrowthPercent ?? raw.DayOverDayGrowthPercent
|
||||||
|
return {
|
||||||
|
customerCount: Number(raw.customerCount ?? raw.CustomerCount ?? 0),
|
||||||
|
todayCreated: Number(raw.todayCreated ?? raw.TodayCreated ?? 0),
|
||||||
|
yesterdayCreated: Number(raw.yesterdayCreated ?? raw.YesterdayCreated ?? 0),
|
||||||
|
dayOverDayGrowthPercent:
|
||||||
|
growthRaw == null || growthRaw === '' ? null : Number(growthRaw),
|
||||||
|
pendingPayment: Number(raw.pendingPayment ?? raw.PendingPayment ?? 0),
|
||||||
|
shipping: Number(raw.shipping ?? raw.Shipping ?? 0),
|
||||||
|
problematic: Number(raw.problematic ?? raw.Problematic ?? 0),
|
||||||
|
deliverySuccess: Number(raw.deliverySuccess ?? raw.DeliverySuccess ?? 0),
|
||||||
|
deliveryFailed: Number(raw.deliveryFailed ?? raw.DeliveryFailed ?? 0),
|
||||||
|
pendingCommissionCount: Number(
|
||||||
|
raw.pendingCommissionCount ?? raw.PendingCommissionCount ?? 0,
|
||||||
|
),
|
||||||
|
pendingCommissionAmount: Number(
|
||||||
|
raw.pendingCommissionAmount ?? raw.PendingCommissionAmount ?? 0,
|
||||||
|
),
|
||||||
|
paidCommissionAmount: Number(raw.paidCommissionAmount ?? raw.PaidCommissionAmount ?? 0),
|
||||||
|
weekNewCustomers: Number(raw.weekNewCustomers ?? raw.WeekNewCustomers ?? 0),
|
||||||
|
counts: (raw.counts ?? raw.Counts) as Record<string, number> | undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async workplaceTrend(days = 7): Promise<AgentWorkplaceTrend> {
|
||||||
|
const r = await axiosInstance.get(omsUrl('/agent/portal/workplace/trend'), {
|
||||||
|
params: { days },
|
||||||
|
})
|
||||||
|
assertBusinessOk(r.data)
|
||||||
|
const raw = (r.data?.data ?? r.data) as Record<string, unknown>
|
||||||
|
const pointsRaw = (raw.points ?? raw.Points ?? []) as Array<Record<string, unknown>>
|
||||||
|
return {
|
||||||
|
dimension: String(raw.dimension ?? raw.Dimension ?? 'day'),
|
||||||
|
points: pointsRaw.map((p) => ({
|
||||||
|
label: String(p.label ?? p.Label ?? ''),
|
||||||
|
start: p.start != null || p.Start != null ? String(p.start ?? p.Start) : undefined,
|
||||||
|
end: p.end != null || p.End != null ? String(p.end ?? p.End) : undefined,
|
||||||
|
orderCount: Number(p.orderCount ?? p.OrderCount ?? 0),
|
||||||
|
commissionAmount: Number(p.commissionAmount ?? p.CommissionAmount ?? 0),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,9 @@ export interface CustomerDto {
|
|||||||
salesman?: string | number
|
salesman?: string | number
|
||||||
salesPerson?: string | number
|
salesPerson?: string | number
|
||||||
customerService?: string | number
|
customerService?: string | number
|
||||||
|
agentId?: number | null
|
||||||
|
agentCode?: string
|
||||||
|
agentName?: string
|
||||||
customerGroup?: CustomerGroup
|
customerGroup?: CustomerGroup
|
||||||
memberLevelId?: number | null
|
memberLevelId?: number | null
|
||||||
memberLevelName?: string
|
memberLevelName?: string
|
||||||
@ -53,6 +56,7 @@ export interface CustomerListParams {
|
|||||||
customerGroup?: number
|
customerGroup?: number
|
||||||
salesman?: string
|
salesman?: string
|
||||||
customerService?: string
|
customerService?: string
|
||||||
|
agentId?: number
|
||||||
sortField?: string
|
sortField?: string
|
||||||
sortOrder?: string
|
sortOrder?: string
|
||||||
}
|
}
|
||||||
@ -88,6 +92,8 @@ function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
|||||||
remark: data.remark,
|
remark: data.remark,
|
||||||
salesman: data.salesman ?? data.salesPerson,
|
salesman: data.salesman ?? data.salesPerson,
|
||||||
customerService: data.customerService,
|
customerService: data.customerService,
|
||||||
|
// 空串表示解绑;有值则按编号绑定
|
||||||
|
agentCode: data.agentCode !== undefined ? String(data.agentCode ?? '').trim() : undefined,
|
||||||
customerGroup: data.customerGroup,
|
customerGroup: data.customerGroup,
|
||||||
memberLevelId: data.memberLevelId,
|
memberLevelId: data.memberLevelId,
|
||||||
priceTierKey: data.priceTierKey,
|
priceTierKey: data.priceTierKey,
|
||||||
|
|||||||
@ -28,6 +28,14 @@ export function mapCustomerRow(r: Record<string, unknown>): CustomerDto {
|
|||||||
remark: (r.remark ?? r.Remark) as string | undefined,
|
remark: (r.remark ?? r.Remark) as string | undefined,
|
||||||
salesPerson: (r.salesPerson ?? r.salesman ?? r.Salesman) as string | number | undefined,
|
salesPerson: (r.salesPerson ?? r.salesman ?? r.Salesman) as string | number | undefined,
|
||||||
customerService: (r.customerService ?? r.CustomerService) as string | number | undefined,
|
customerService: (r.customerService ?? r.CustomerService) as string | number | undefined,
|
||||||
|
agentId: (() => {
|
||||||
|
const raw = r.agentId ?? r.AgentId
|
||||||
|
if (raw == null || raw === '') return null
|
||||||
|
const n = Number(raw)
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : null
|
||||||
|
})(),
|
||||||
|
agentCode: (r.agentCode ?? r.AgentCode) as string | undefined,
|
||||||
|
agentName: (r.agentName ?? r.AgentName) as string | undefined,
|
||||||
lastOnlineTime: (r.lastOnlineTime ?? r.lastLoginTime ?? r.LastLoginTime) as string | undefined,
|
lastOnlineTime: (r.lastOnlineTime ?? r.lastLoginTime ?? r.LastLoginTime) as string | undefined,
|
||||||
lastOnlineIp: (r.lastOnlineIp ?? r.lastLoginIp ?? r.LastLoginIp) as string | undefined,
|
lastOnlineIp: (r.lastOnlineIp ?? r.lastLoginIp ?? r.LastLoginIp) as string | undefined,
|
||||||
createTime: (r.createTime ?? r.CreateTime ?? r.createdAt) as string | undefined,
|
createTime: (r.createTime ?? r.CreateTime ?? r.createdAt) as string | undefined,
|
||||||
@ -55,6 +63,9 @@ function normalizeCustomerDetail(data: CustomerDto): CustomerDto {
|
|||||||
...data,
|
...data,
|
||||||
accountBalance: data.accountBalance ?? data.balance,
|
accountBalance: data.accountBalance ?? data.balance,
|
||||||
salesPerson: data.salesPerson ?? data.salesman,
|
salesPerson: data.salesPerson ?? data.salesman,
|
||||||
|
agentId: data.agentId ?? null,
|
||||||
|
agentCode: data.agentCode,
|
||||||
|
agentName: data.agentName,
|
||||||
lastOnlineTime: data.lastOnlineTime ?? data.lastLoginTime,
|
lastOnlineTime: data.lastOnlineTime ?? data.lastLoginTime,
|
||||||
lastOnlineIp: data.lastOnlineIp ?? data.lastLoginIp,
|
lastOnlineIp: data.lastOnlineIp ?? data.lastLoginIp,
|
||||||
wechatAvatarUrl: resolveAvatarUrl(data.wechatAvatarUrl ?? data.wechatAvatar),
|
wechatAvatarUrl: resolveAvatarUrl(data.wechatAvatarUrl ?? data.wechatAvatar),
|
||||||
|
|||||||
@ -1519,6 +1519,8 @@ export interface PriceCheckRequest {
|
|||||||
postalCode?: string
|
postalCode?: string
|
||||||
/** 选用的优惠券实例 Id */
|
/** 选用的优惠券实例 Id */
|
||||||
couponInstanceId?: number
|
couponInstanceId?: number
|
||||||
|
/** 代客查价客户(管理端/代理可选;影响会员价与券) */
|
||||||
|
customerId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketingPriceBreakdown {
|
export interface MarketingPriceBreakdown {
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="agent-side-rail__dock">
|
<div class="agent-side-rail__dock">
|
||||||
<div class="agent-side-rail__toolbar">
|
<div class="agent-side-rail__account">
|
||||||
<div class="agent-side-rail__user">
|
<div class="agent-side-rail__user">
|
||||||
<RightTopUser
|
<RightTopUser
|
||||||
variant="sider"
|
variant="sider"
|
||||||
@ -61,13 +61,18 @@
|
|||||||
:show-recharge-cta="false"
|
:show-recharge-cta="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-side-rail__agent-meta agent-side-rail__fade">
|
<div class="agent-side-rail__account-meta agent-side-rail__fade">
|
||||||
<span class="agent-side-rail__agent-name">{{ agentDisplayName }}</span>
|
<div class="agent-side-rail__account-name" :title="agentDisplayName">
|
||||||
|
{{ agentDisplayName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="agentDisplayCode"
|
||||||
|
class="agent-side-rail__account-code"
|
||||||
|
:title="agentDisplayCode"
|
||||||
|
>
|
||||||
|
{{ agentDisplayCode }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="agent-side-rail__footer-row">
|
|
||||||
<span class="agent-side-rail__portal-tag agent-side-rail__fade">渠道代理</span>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="agent-side-rail__toggle"
|
class="agent-side-rail__toggle"
|
||||||
@ -89,10 +94,14 @@ import { computed, onMounted, onUnmounted, ref, type Component } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
|
IconDashboard,
|
||||||
IconDoubleLeft,
|
IconDoubleLeft,
|
||||||
IconDoubleRight,
|
IconDoubleRight,
|
||||||
|
IconGift,
|
||||||
IconList,
|
IconList,
|
||||||
|
IconPlusCircle,
|
||||||
IconShareInternal,
|
IconShareInternal,
|
||||||
|
IconThunderbolt,
|
||||||
IconUserGroup,
|
IconUserGroup,
|
||||||
} from '@arco-design/web-vue/es/icon'
|
} from '@arco-design/web-vue/es/icon'
|
||||||
import RightTopUser from '@/components/top-nav/RightTopUser.vue'
|
import RightTopUser from '@/components/top-nav/RightTopUser.vue'
|
||||||
@ -130,11 +139,26 @@ const railWidth = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const navItems = computed<AgentNavItem[]>(() => [
|
const navItems = computed<AgentNavItem[]>(() => [
|
||||||
|
{
|
||||||
|
path: '/agent/workplace',
|
||||||
|
label: '工作台',
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/invite',
|
path: '/agent/invite',
|
||||||
label: '邀请获客',
|
label: '邀请获客',
|
||||||
icon: IconShareInternal,
|
icon: IconShareInternal,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/priceCheck',
|
||||||
|
label: '运费查价',
|
||||||
|
icon: IconThunderbolt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/placeOrder',
|
||||||
|
label: '运单下单',
|
||||||
|
icon: IconPlusCircle,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/customers',
|
path: '/agent/customers',
|
||||||
label: '名下客户',
|
label: '名下客户',
|
||||||
@ -142,15 +166,28 @@ const navItems = computed<AgentNavItem[]>(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/orders',
|
path: '/agent/orders',
|
||||||
label: '订单与返佣',
|
label: '运单列表',
|
||||||
icon: IconList,
|
icon: IconList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/commission',
|
||||||
|
label: '返佣明细',
|
||||||
|
icon: IconGift,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const agentDisplayName = computed(
|
const agentDisplayName = computed(
|
||||||
() => profile.value?.agentName || profile.value?.agentCode || '代理',
|
() => profile.value?.agentName?.trim() || profile.value?.agentCode || '代理',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const agentDisplayCode = computed(() => {
|
||||||
|
const code = profile.value?.agentCode?.trim() || ''
|
||||||
|
const name = profile.value?.agentName?.trim() || ''
|
||||||
|
// 名称与编号相同时只展示一行,避免「一代 / 一代」重复
|
||||||
|
if (!code || code === name) return ''
|
||||||
|
return code
|
||||||
|
})
|
||||||
|
|
||||||
function readCollapsed(): boolean {
|
function readCollapsed(): boolean {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(AGENT_SIDE_RAIL_STORAGE_KEY) === '1'
|
return localStorage.getItem(AGENT_SIDE_RAIL_STORAGE_KEY) === '1'
|
||||||
@ -397,54 +434,44 @@ onUnmounted(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 10px var(--asr-pad-x) 12px;
|
padding: 10px var(--asr-pad-x) 12px;
|
||||||
border-top: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
border-top: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__toolbar {
|
&__account {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 4px 4px 4px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__user {
|
&__user {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__agent-meta {
|
&__account-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__agent-name {
|
&__account-name {
|
||||||
display: block;
|
font-size: 13px;
|
||||||
font-size: 12px;
|
font-weight: 600;
|
||||||
color: var(--color-text-3);
|
line-height: 1.25;
|
||||||
|
color: var(--color-text-1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer-row {
|
&__account-code {
|
||||||
display: flex;
|
font-size: 11px;
|
||||||
align-items: center;
|
line-height: 1.2;
|
||||||
gap: 2px;
|
color: var(--color-text-3);
|
||||||
min-width: 0;
|
letter-spacing: 0.02em;
|
||||||
}
|
|
||||||
|
|
||||||
&__portal-tag {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
color: var(--color-text-2);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 30px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -452,8 +479,9 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
&__toggle {
|
&__toggle {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 30px;
|
width: 28px;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
|
margin-left: auto;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -464,8 +492,8 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--app-primary-color, #1d7281);
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
background: rgba(255, 255, 255, 0.85);
|
background: rgba(var(--primary-6), 0.08);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,12 +507,25 @@ onUnmounted(() => {
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-compact &__toolbar {
|
&.is-compact &__dock {
|
||||||
justify-content: center;
|
padding: 8px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-compact &__portal-tag {
|
&.is-compact &__account {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-compact &__account-meta {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-compact &__toggle {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -337,11 +337,15 @@ const displayNameInitial = computed(() => {
|
|||||||
return name ? name.slice(0, 1).toUpperCase() : '?';
|
return name ? name.slice(0, 1).toUpperCase() : '?';
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 后端占位邮箱,不当真实邮箱展示 */
|
/** 后端占位邮箱,不当真实邮箱展示(客户 @noemail.local / 代理 @agent.local) */
|
||||||
function isPlaceholderEmail(value: string) {
|
function isPlaceholderEmail(value: string) {
|
||||||
const v = value.trim().toLowerCase();
|
const v = value.trim().toLowerCase();
|
||||||
if (!v) return true;
|
if (!v) return true;
|
||||||
return v.endsWith('@noemail.local') || v.includes('@noemail.');
|
return (
|
||||||
|
v.endsWith('@noemail.local') ||
|
||||||
|
v.includes('@noemail.') ||
|
||||||
|
v.endsWith('@agent.local')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountSubtitle = computed(() => {
|
const accountSubtitle = computed(() => {
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { CustomerService } from '@/api/customer'
|
import { CustomerService } from '@/api/customer'
|
||||||
|
import { AgentPortalApi } from '@/api/agent'
|
||||||
import { extractCustomerId, mapCustomerRow } from '@/api/customer/resolve'
|
import { extractCustomerId, mapCustomerRow } from '@/api/customer/resolve'
|
||||||
import { $baseMessage } from '@/utils'
|
import { $baseMessage } from '@/utils'
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仓储页管理端客户上下文:
|
* 仓储/下单页「非客户门户」客户上下文:
|
||||||
* - 列表/汇总:未选客户 → 不传 customerId → 查全部
|
* - 管理端:列表未选客户 → 不传 customerId → 查全部;写入须显式选客户
|
||||||
* - 写入:管理端须显式选择客户
|
* - 渠道代理:仅能选名下绑定客户,写入须显式选客户
|
||||||
* 客户门户登录时由后端会话强制归属,前端不传 customerId。
|
* - 客户门户:由后端会话强制归属,前端不传 customerId
|
||||||
*/
|
*/
|
||||||
export function useWarehouseAdminCustomer() {
|
export function useWarehouseAdminCustomer() {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const rbacUserStore = useRbacUserStore()
|
const rbacUserStore = useRbacUserStore()
|
||||||
|
|
||||||
const isAdmin = computed(() => !rbacUserStore.isCustomerPortal())
|
const isCustomer = computed(() => rbacUserStore.isCustomerPortal())
|
||||||
|
const isAgent = computed(() => rbacUserStore.isAgentPortal())
|
||||||
|
/** 需前端显式选客户:管理端 + 渠道代理 */
|
||||||
|
const isAdmin = computed(() => !isCustomer.value)
|
||||||
/** undefined = 不筛选(查全部);仅 >0 才作为筛选条件 */
|
/** undefined = 不筛选(查全部);仅 >0 才作为筛选条件 */
|
||||||
const customerId = ref<number>()
|
const customerId = ref<number>()
|
||||||
const customerOptions = ref<Array<{ label: string; value: number }>>([])
|
const customerOptions = ref<Array<{ label: string; value: number }>>([])
|
||||||
@ -35,17 +39,32 @@ export function useWarehouseAdminCustomer() {
|
|||||||
}
|
}
|
||||||
customerLoading.value = true
|
customerLoading.value = true
|
||||||
try {
|
try {
|
||||||
const { items } = await CustomerService.getList({ page: 1, pageSize: 500 })
|
if (isAgent.value) {
|
||||||
customerOptions.value = (items || [])
|
const { items } = await AgentPortalApi.customers({ page: 1, pageSize: 500 })
|
||||||
.map((row) => mapCustomerRow(row as Record<string, unknown>))
|
customerOptions.value = (items || [])
|
||||||
.map((c) => {
|
.map((row) => {
|
||||||
const id = extractCustomerId(c as Record<string, unknown>)
|
const r = row as unknown as Record<string, unknown>
|
||||||
if (!id) return null
|
const id = Number(r.id ?? r.Id ?? 0)
|
||||||
const name = c.customerName || c.customerCode || c.phone || `#${id}`
|
if (!id) return null
|
||||||
const code = c.customerCode ? ` (${c.customerCode})` : ''
|
const name = String(r.customerName ?? r.CustomerName ?? r.customerCode ?? r.CustomerCode ?? `#${id}`)
|
||||||
return { label: `${name}${code}`, value: id }
|
const code = String(r.customerCode ?? r.CustomerCode ?? '')
|
||||||
})
|
const suffix = code ? ` (${code})` : ''
|
||||||
.filter((x): x is { label: string; value: number } => !!x)
|
return { label: `${name}${suffix}`, value: id }
|
||||||
|
})
|
||||||
|
.filter((x): x is { label: string; value: number } => !!x)
|
||||||
|
} else {
|
||||||
|
const { items } = await CustomerService.getList({ page: 1, pageSize: 500 })
|
||||||
|
customerOptions.value = (items || [])
|
||||||
|
.map((row) => mapCustomerRow(row as Record<string, unknown>))
|
||||||
|
.map((c) => {
|
||||||
|
const id = extractCustomerId(c as Record<string, unknown>)
|
||||||
|
if (!id) return null
|
||||||
|
const name = c.customerName || c.customerCode || c.phone || `#${id}`
|
||||||
|
const code = c.customerCode ? ` (${c.customerCode})` : ''
|
||||||
|
return { label: `${name}${code}`, value: id }
|
||||||
|
})
|
||||||
|
.filter((x): x is { label: string; value: number } => !!x)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
customerOptions.value = []
|
customerOptions.value = []
|
||||||
} finally {
|
} finally {
|
||||||
@ -56,6 +75,7 @@ export function useWarehouseAdminCustomer() {
|
|||||||
/**
|
/**
|
||||||
* 追加到 list/save 请求体。
|
* 追加到 list/save 请求体。
|
||||||
* 管理端未选客户时显式去掉 customerId,保证「不选 = 查全部」。
|
* 管理端未选客户时显式去掉 customerId,保证「不选 = 查全部」。
|
||||||
|
* 代理写入必须带 customerId(由 ensureMutationCustomerId 校验)。
|
||||||
*/
|
*/
|
||||||
function withCustomerId<T extends Record<string, unknown>>(body: T): T & { customerId?: number } {
|
function withCustomerId<T extends Record<string, unknown>>(body: T): T & { customerId?: number } {
|
||||||
const next = { ...body } as T & { customerId?: number }
|
const next = { ...body } as T & { customerId?: number }
|
||||||
@ -66,12 +86,17 @@ export function useWarehouseAdminCustomer() {
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 管理端写操作前校验已选客户;客户门户返回 undefined 由后端会话解析 */
|
/** 管理端/代理写操作前校验已选客户;客户门户返回 undefined 由后端会话解析 */
|
||||||
function ensureMutationCustomerId(): number | undefined {
|
function ensureMutationCustomerId(): number | undefined {
|
||||||
if (!isAdmin.value) return undefined
|
if (!isAdmin.value) return undefined
|
||||||
const id = resolveSelectedCustomerId()
|
const id = resolveSelectedCustomerId()
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
$baseMessage(t('warehouse.common.selectCustomerRequired'), 'warning')
|
$baseMessage(
|
||||||
|
isAgent.value
|
||||||
|
? '请先选择名下客户再下单'
|
||||||
|
: t('warehouse.common.selectCustomerRequired'),
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
@ -83,6 +108,7 @@ export function useWarehouseAdminCustomer() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
isAgent,
|
||||||
customerId,
|
customerId,
|
||||||
customerOptions,
|
customerOptions,
|
||||||
customerLoading,
|
customerLoading,
|
||||||
|
|||||||
@ -4906,7 +4906,8 @@ export default {
|
|||||||
'user.customer.fields.createdBy': 'Created By',
|
'user.customer.fields.createdBy': 'Created By',
|
||||||
'user.customer.fields.updateTime': 'Update Time',
|
'user.customer.fields.updateTime': 'Update Time',
|
||||||
'user.customer.fields.updateBy': 'Update By',
|
'user.customer.fields.updateBy': 'Update By',
|
||||||
'user.customer.fields.salesPerson': 'Sales Person',
|
'user.customer.fields.salesPerson': 'Bound Agent',
|
||||||
|
'user.customer.fields.agent': 'Bound Agent',
|
||||||
'user.customer.fields.customerService': 'Advisor',
|
'user.customer.fields.customerService': 'Advisor',
|
||||||
'user.customer.fields.role': 'Role',
|
'user.customer.fields.role': 'Role',
|
||||||
'user.customer.role.customer': 'Customer',
|
'user.customer.role.customer': 'Customer',
|
||||||
|
|||||||
@ -468,6 +468,15 @@ export default {
|
|||||||
'marketing.register.referrerLoading': 'Validating invite code…',
|
'marketing.register.referrerLoading': 'Validating invite code…',
|
||||||
'marketing.register.referrerTimeout':
|
'marketing.register.referrerTimeout':
|
||||||
'Invite validation timed out. Check your network and refresh; you can still register.',
|
'Invite validation timed out. Check your network and refresh; you can still register.',
|
||||||
|
'marketing.register.agentHint': 'Channel partner · {name} ({code})',
|
||||||
|
'marketing.register.agentField': 'Agent code (optional)',
|
||||||
|
'marketing.register.agentPlaceholder': 'Enter agent code if you were referred by a partner',
|
||||||
|
'marketing.register.agentLockedHint':
|
||||||
|
'Locked by invite link; you will be bound to this agent after register',
|
||||||
|
'marketing.register.agentInvalid': 'Agent code is invalid or disabled',
|
||||||
|
'marketing.register.agentValidating': 'Validating agent code…',
|
||||||
|
'marketing.register.agentPriorityHint':
|
||||||
|
'Agent code filled — channel agent binding takes priority over friend invite',
|
||||||
'marketing.register.customerName': 'Name',
|
'marketing.register.customerName': 'Name',
|
||||||
'marketing.register.phone': 'Phone',
|
'marketing.register.phone': 'Phone',
|
||||||
'marketing.register.email': 'Email',
|
'marketing.register.email': 'Email',
|
||||||
|
|||||||
@ -464,6 +464,13 @@ export default {
|
|||||||
'marketing.register.invalidRef': '邀请码无效或已失效,您仍可正常注册',
|
'marketing.register.invalidRef': '邀请码无效或已失效,您仍可正常注册',
|
||||||
'marketing.register.referrerLoading': '正在校验邀请码…',
|
'marketing.register.referrerLoading': '正在校验邀请码…',
|
||||||
'marketing.register.referrerTimeout': '邀请码校验超时,请检查网络后刷新;您仍可继续注册',
|
'marketing.register.referrerTimeout': '邀请码校验超时,请检查网络后刷新;您仍可继续注册',
|
||||||
|
'marketing.register.agentHint': '渠道推荐 · {name}({code})',
|
||||||
|
'marketing.register.agentField': '代理编号(选填)',
|
||||||
|
'marketing.register.agentPlaceholder': '有渠道推荐人可填写其代理编号',
|
||||||
|
'marketing.register.agentLockedHint': '已由专属链接锁定,注册后自动绑定该代理',
|
||||||
|
'marketing.register.agentInvalid': '代理编号无效或已停用',
|
||||||
|
'marketing.register.agentValidating': '正在校验代理编号…',
|
||||||
|
'marketing.register.agentPriorityHint': '已填写代理编号,将优先绑定渠道代理(不走好友邀请)',
|
||||||
'marketing.register.customerName': '客户名称',
|
'marketing.register.customerName': '客户名称',
|
||||||
'marketing.register.phone': '手机号',
|
'marketing.register.phone': '手机号',
|
||||||
'marketing.register.email': '邮箱',
|
'marketing.register.email': '邮箱',
|
||||||
|
|||||||
@ -4929,7 +4929,8 @@ export default {
|
|||||||
'user.customer.fields.createdBy': '创建人',
|
'user.customer.fields.createdBy': '创建人',
|
||||||
'user.customer.fields.updateTime': '更新时间',
|
'user.customer.fields.updateTime': '更新时间',
|
||||||
'user.customer.fields.updateBy': '更新人',
|
'user.customer.fields.updateBy': '更新人',
|
||||||
'user.customer.fields.salesPerson': '所属销售',
|
'user.customer.fields.salesPerson': '绑定代理',
|
||||||
|
'user.customer.fields.agent': '绑定代理',
|
||||||
'user.customer.fields.customerService': '所属顾问',
|
'user.customer.fields.customerService': '所属顾问',
|
||||||
'user.customer.fields.role': '角色',
|
'user.customer.fields.role': '角色',
|
||||||
'user.customer.role.customer': '客户',
|
'user.customer.role.customer': '客户',
|
||||||
|
|||||||
@ -3,9 +3,13 @@ export const WHITE_LIST = [
|
|||||||
{ name: 'login', children: [] },
|
{ name: 'login', children: [] },
|
||||||
{ name: 'adminLogin', children: [] },
|
{ name: 'adminLogin', children: [] },
|
||||||
{ name: 'agentLogin', children: [] },
|
{ name: 'agentLogin', children: [] },
|
||||||
|
{ name: 'AgentPortalWorkplace', children: [] },
|
||||||
{ name: 'AgentPortalInvite', children: [] },
|
{ name: 'AgentPortalInvite', children: [] },
|
||||||
{ name: 'AgentPortalCustomers', children: [] },
|
{ name: 'AgentPortalCustomers', children: [] },
|
||||||
{ name: 'AgentPortalOrders', children: [] },
|
{ name: 'AgentPortalOrders', children: [] },
|
||||||
|
{ name: 'AgentPortalCommission', children: [] },
|
||||||
|
{ name: 'AgentPortalPriceCheck', children: [] },
|
||||||
|
{ name: 'AgentPortalPlaceOrder', children: [] },
|
||||||
{ name: 'siteRoot', children: [] },
|
{ name: 'siteRoot', children: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -64,13 +64,23 @@ const router = createRouter({
|
|||||||
path: '/agent-portal',
|
path: '/agent-portal',
|
||||||
name: 'AgentPortal',
|
name: 'AgentPortal',
|
||||||
component: () => import('@/layout/default-layout.vue'),
|
component: () => import('@/layout/default-layout.vue'),
|
||||||
redirect: '/agent/invite',
|
redirect: '/agent/workplace',
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
agentPortalOnly: true,
|
agentPortalOnly: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '/agent/workplace',
|
||||||
|
name: 'AgentPortalWorkplace',
|
||||||
|
component: () => import('@/views/agent/workplace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
agentPortalOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/invite',
|
path: '/agent/invite',
|
||||||
name: 'AgentPortalInvite',
|
name: 'AgentPortalInvite',
|
||||||
@ -94,7 +104,37 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/agent/orders',
|
path: '/agent/orders',
|
||||||
name: 'AgentPortalOrders',
|
name: 'AgentPortalOrders',
|
||||||
component: () => import('@/views/agent/orders/index.vue'),
|
component: () => import('@/views/order/myOrders/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
agentPortalOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/commission',
|
||||||
|
name: 'AgentPortalCommission',
|
||||||
|
component: () => import('@/views/agent/commission/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
agentPortalOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/priceCheck',
|
||||||
|
name: 'AgentPortalPriceCheck',
|
||||||
|
component: () => import('@/views/order/priceCheck/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
agentPortalOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agent/placeOrder',
|
||||||
|
name: 'AgentPortalPlaceOrder',
|
||||||
|
component: () => import('@/views/order/placeOrder/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
|
|||||||
1
src/router/typings.d.ts
vendored
1
src/router/typings.d.ts
vendored
@ -13,6 +13,7 @@ declare module 'vue-router' {
|
|||||||
menuPermission?: string;
|
menuPermission?: string;
|
||||||
customerPortalOnly?: boolean; // If true, only customer portal can access
|
customerPortalOnly?: boolean; // If true, only customer portal can access
|
||||||
adminPortalOnly?: boolean; // If true, only admin (internal user) portal can access
|
adminPortalOnly?: boolean; // If true, only admin (internal user) portal can access
|
||||||
|
agentPortalOnly?: boolean; // If true, only channel agent portal can access
|
||||||
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
|
hideChildrenInMenu?: boolean; // if set true, the children are not displayed in the side menu
|
||||||
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
|
activeMenu?: string; // if set name, the menu will be highlighted according to the name you set
|
||||||
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
|
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
|
||||||
|
|||||||
108
src/utils/agentInviteStorage.ts
Normal file
108
src/utils/agentInviteStorage.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/** 渠道代理注册归因暂存:7 天 last-touch(与 invite / advisor 并列,参数名为 agent) */
|
||||||
|
|
||||||
|
const AGENT_INVITE_STORAGE_KEY = 'oms_agent_invite_v1'
|
||||||
|
const AGENT_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
type AgentInvitePayload = {
|
||||||
|
code: string
|
||||||
|
/** URL 带来的锁定码,手填不应覆盖 */
|
||||||
|
locked?: boolean
|
||||||
|
ts: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParse(raw: string | null): AgentInvitePayload | null {
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as AgentInvitePayload
|
||||||
|
if (!parsed?.code || typeof parsed.ts !== 'number') return null
|
||||||
|
return {
|
||||||
|
code: String(parsed.code).trim(),
|
||||||
|
locked: Boolean(parsed.locked),
|
||||||
|
ts: parsed.ts,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpired(ts: number, now = Date.now()): boolean {
|
||||||
|
return now - ts > AGENT_INVITE_TTL_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stashAgentInvite(agentCode?: string | null, locked = false): void {
|
||||||
|
const code = String(agentCode ?? '').trim()
|
||||||
|
if (!code) return
|
||||||
|
const payload: AgentInvitePayload = { code, locked, ts: Date.now() }
|
||||||
|
const raw = JSON.stringify(payload)
|
||||||
|
try {
|
||||||
|
localStorage.setItem(AGENT_INVITE_STORAGE_KEY, raw)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(AGENT_INVITE_STORAGE_KEY, raw)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readStashedAgentInvite(): { code: string; locked: boolean } {
|
||||||
|
const now = Date.now()
|
||||||
|
const fromSession = safeParse(
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(AGENT_INVITE_STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
)
|
||||||
|
if (fromSession && !isExpired(fromSession.ts, now)) {
|
||||||
|
return { code: fromSession.code, locked: Boolean(fromSession.locked) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromLocal = safeParse(
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(AGENT_INVITE_STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
)
|
||||||
|
if (fromLocal && !isExpired(fromLocal.ts, now)) {
|
||||||
|
stashAgentInvite(fromLocal.code, Boolean(fromLocal.locked))
|
||||||
|
return { code: fromLocal.code, locked: Boolean(fromLocal.locked) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromLocal || fromSession) {
|
||||||
|
clearStashedAgentInvite()
|
||||||
|
}
|
||||||
|
return { code: '', locked: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStashedAgentInvite(): void {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(AGENT_INVITE_STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem(AGENT_INVITE_STORAGE_KEY)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析代理归因:query.agent → 7 天暂存 */
|
||||||
|
export function resolveAgentInvite(query: Record<string, unknown>): {
|
||||||
|
code: string
|
||||||
|
locked: boolean
|
||||||
|
} {
|
||||||
|
const fromQuery = String(query.agent ?? query.agentCode ?? '').trim()
|
||||||
|
if (fromQuery) {
|
||||||
|
stashAgentInvite(fromQuery, true)
|
||||||
|
return { code: fromQuery, locked: true }
|
||||||
|
}
|
||||||
|
return readStashedAgentInvite()
|
||||||
|
}
|
||||||
44
src/utils/agentPortalCopy.ts
Normal file
44
src/utils/agentPortalCopy.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/** 代理门户:催付款 / 邀请话术文案 */
|
||||||
|
|
||||||
|
export function buildPendingPaymentReminderText(opts: {
|
||||||
|
customerName?: string
|
||||||
|
pendingCount?: number
|
||||||
|
agentName?: string
|
||||||
|
}): string {
|
||||||
|
const name = (opts.customerName || '客户').trim()
|
||||||
|
const count = Math.max(0, Number(opts.pendingCount) || 0)
|
||||||
|
const agent = (opts.agentName || '您的渠道顾问').trim()
|
||||||
|
const countPart = count > 0 ? `目前有 ${count} 票运单待付款` : '您有运单待付款'
|
||||||
|
return (
|
||||||
|
`【物流提醒】您好${name !== '客户' ? `,${name}` : ''},` +
|
||||||
|
`${countPart},请尽快登录系统完成支付,以免影响出货。` +
|
||||||
|
`如有疑问可联系${agent}。`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentInviteTemplateKey = 'wechat' | 'moments' | 'formal'
|
||||||
|
|
||||||
|
export const AGENT_INVITE_TEMPLATES: Array<{
|
||||||
|
key: AgentInviteTemplateKey
|
||||||
|
label: string
|
||||||
|
build: (agentCode: string, inviteUrl: string) => string
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: 'wechat',
|
||||||
|
label: '微信私聊',
|
||||||
|
build: (code, url) =>
|
||||||
|
`嗨,推荐你用这个渠道寄国际件,注册就送专属通道:${url}\n我的代理编号:${code}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'moments',
|
||||||
|
label: '朋友圈',
|
||||||
|
build: (code, url) =>
|
||||||
|
`国际物流靠谱渠道分享|扫码/点开即可注册下单\n${url}\n编号 ${code}(备注来找我可跟进)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'formal',
|
||||||
|
label: '商务简介',
|
||||||
|
build: (code, url) =>
|
||||||
|
`【IDE Express 渠道邀请】请通过专属链接完成注册绑定:${url}\n代理编号:${code}\n注册后即可查价下单,运单派送完成后按约定结算返佣。`,
|
||||||
|
},
|
||||||
|
]
|
||||||
@ -174,7 +174,7 @@ export function adaptOrderListHeaderFilters(
|
|||||||
|
|
||||||
export interface CustomerListHeaderQuery {
|
export interface CustomerListHeaderQuery {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
salesman?: string;
|
agentId?: number;
|
||||||
customerService?: string;
|
customerService?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
@ -191,9 +191,15 @@ export function adaptCustomerListHeaderFilters(
|
|||||||
'phone',
|
'phone',
|
||||||
'wechatNickname',
|
'wechatNickname',
|
||||||
]) || undefined;
|
]) || undefined;
|
||||||
const salesman = pickHeaderText(params, ['salesPerson', 'salesman']) || undefined;
|
|
||||||
const customerService = pickHeaderText(params, ['customerService']) || undefined;
|
const customerService = pickHeaderText(params, ['customerService']) || undefined;
|
||||||
|
|
||||||
|
let agentId: number | undefined;
|
||||||
|
const agentRaw = pickHeaderText(params, ['agent', 'agentId', 'agentCode']);
|
||||||
|
if (agentRaw) {
|
||||||
|
const n = Number(agentRaw);
|
||||||
|
if (!Number.isNaN(n) && n > 0) agentId = n;
|
||||||
|
}
|
||||||
|
|
||||||
let status: number | undefined;
|
let status: number | undefined;
|
||||||
const statusRaw = asTrimmedString(params.status);
|
const statusRaw = asTrimmedString(params.status);
|
||||||
if (statusRaw) {
|
if (statusRaw) {
|
||||||
@ -205,7 +211,7 @@ export function adaptCustomerListHeaderFilters(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { keyword, salesman, customerService, status };
|
return { keyword, agentId, customerService, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountFlowHeaderQuery {
|
export interface AccountFlowHeaderQuery {
|
||||||
|
|||||||
@ -44,10 +44,10 @@ export function buildInviteShareText(inviteCode: string, inviteUrl: string): str
|
|||||||
return `【IDE Express】送你一个专属邀请,微信打开链接注册即可:${url}\n邀请码:${code}`;
|
return `【IDE Express】送你一个专属邀请,微信打开链接注册即可:${url}\n邀请码:${code}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渠道代理分享话术 */
|
/** 渠道代理分享话术(默认微信私聊) */
|
||||||
export function buildAgentInviteShareText(agentCode: string, inviteUrl: string): string {
|
export function buildAgentInviteShareText(agentCode: string, inviteUrl: string): string {
|
||||||
const code = agentCode.trim();
|
const code = agentCode.trim();
|
||||||
const url = inviteUrl.trim();
|
const url = inviteUrl.trim();
|
||||||
if (!code || !url) return url || code;
|
if (!code || !url) return url || code;
|
||||||
return `【IDE Express】通过我的专属链接注册,微信打开即可:${url}\n代理编号:${code}`;
|
return `嗨,推荐你用这个渠道寄国际件,注册就送专属通道:${url}\n我的代理编号:${code}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const ADMIN_LOGIN_PATH = '/admin'
|
|||||||
/** 渠道代理登录路径 / 路由名 */
|
/** 渠道代理登录路径 / 路由名 */
|
||||||
export const AGENT_LOGIN_PATH = '/agent'
|
export const AGENT_LOGIN_PATH = '/agent'
|
||||||
export const AGENT_LOGIN_ROUTE_NAME = 'agentLogin'
|
export const AGENT_LOGIN_ROUTE_NAME = 'agentLogin'
|
||||||
export const AGENT_HOME_PATH = '/agent/invite'
|
export const AGENT_HOME_PATH = '/agent/workplace'
|
||||||
|
|
||||||
/** 客户端(客户)登录路由 */
|
/** 客户端(客户)登录路由 */
|
||||||
export const CUSTOMER_LOGIN_ROUTE_NAME = 'login'
|
export const CUSTOMER_LOGIN_ROUTE_NAME = 'login'
|
||||||
|
|||||||
@ -11,8 +11,10 @@
|
|||||||
style="width: 140px"
|
style="width: 140px"
|
||||||
/>
|
/>
|
||||||
<a-select v-model="status" allow-clear placeholder="状态" style="width: 140px">
|
<a-select v-model="status" allow-clear placeholder="状态" style="width: 140px">
|
||||||
|
<a-option value="estimated">预计返佣</a-option>
|
||||||
<a-option value="pending">待结算</a-option>
|
<a-option value="pending">待结算</a-option>
|
||||||
<a-option value="paid">已打款</a-option>
|
<a-option value="paid">已打款</a-option>
|
||||||
|
<a-option value="void">已作废</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-button class="app-btn-search" @click="handleSearch">查询</a-button>
|
<a-button class="app-btn-search" @click="handleSearch">查询</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -129,14 +131,44 @@ function mapRow(raw: AgentCommissionDto): CommissionRow {
|
|||||||
commissionRate: Number(pick(r, 'commissionRate', 'CommissionRate') ?? 0),
|
commissionRate: Number(pick(r, 'commissionRate', 'CommissionRate') ?? 0),
|
||||||
commissionAmount: Number(pick(r, 'commissionAmount', 'CommissionAmount') ?? 0),
|
commissionAmount: Number(pick(r, 'commissionAmount', 'CommissionAmount') ?? 0),
|
||||||
status: st,
|
status: st,
|
||||||
statusName: st === 'paid' ? '已打款' : '待结算',
|
statusName: resolveStatusName(st),
|
||||||
triggerOrderStatus: String(pick(r, 'triggerOrderStatus', 'TriggerOrderStatus') ?? ''),
|
triggerOrderStatus: String(pick(r, 'triggerOrderStatus', 'TriggerOrderStatus') ?? ''),
|
||||||
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStatusName(st: string) {
|
||||||
|
switch (st) {
|
||||||
|
case 'estimated':
|
||||||
|
return '预计返佣'
|
||||||
|
case 'pending':
|
||||||
|
return '待结算'
|
||||||
|
case 'paid':
|
||||||
|
return '已打款'
|
||||||
|
case 'void':
|
||||||
|
return '已作废'
|
||||||
|
default:
|
||||||
|
return st || '--'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStatusTone(st: string): 'ok' | 'wait' | 'brand' | 'alert' | 'mute' {
|
||||||
|
switch (st) {
|
||||||
|
case 'paid':
|
||||||
|
return 'ok'
|
||||||
|
case 'pending':
|
||||||
|
return 'wait'
|
||||||
|
case 'estimated':
|
||||||
|
return 'brand'
|
||||||
|
case 'void':
|
||||||
|
return 'mute'
|
||||||
|
default:
|
||||||
|
return 'mute'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function actionRenderer(row: CommissionRow, rowIndex: number): string {
|
function actionRenderer(row: CommissionRow, rowIndex: number): string {
|
||||||
if (row.status === 'paid') {
|
if (row.status !== 'pending') {
|
||||||
return '<span style="color:var(--color-text-3);font-size:12px;">--</span>'
|
return '<span style="color:var(--color-text-3);font-size:12px;">--</span>'
|
||||||
}
|
}
|
||||||
return wrapWarehouseActions(
|
return wrapWarehouseActions(
|
||||||
@ -155,7 +187,7 @@ function customCellRenderer(_row: number, col: number, _value: unknown, rowData:
|
|||||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
||||||
const colKey = hotColumns.value[dataColIndex].key
|
const colKey = hotColumns.value[dataColIndex].key
|
||||||
if (colKey === 'statusName') {
|
if (colKey === 'statusName') {
|
||||||
return buildStatusTagHtml(rowData.statusName, rowData.status === 'paid' ? 'ok' : 'wait')
|
return buildStatusTagHtml(rowData.statusName, resolveStatusTone(rowData.status))
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="agent-orders-page">
|
<div class="agent-commission-page">
|
||||||
<div class="agent-orders-page-card">
|
<div class="agent-commission-page-card">
|
||||||
<a-tabs v-model:active-key="tab" class="orders-tabs" @change="onTabChange">
|
|
||||||
<a-tab-pane key="orders" title="订单" />
|
|
||||||
<a-tab-pane key="commission" title="返佣明细" />
|
|
||||||
</a-tabs>
|
|
||||||
|
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
|
<a-select
|
||||||
|
v-model="status"
|
||||||
|
allow-clear
|
||||||
|
placeholder="结算状态"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="statusOptions"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
|
<a-range-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
style="width: 260px"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
<a-input
|
<a-input
|
||||||
v-if="tab === 'orders'"
|
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="运单号/追踪号/产品"
|
placeholder="运单号"
|
||||||
style="width: 240px"
|
style="width: 180px"
|
||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-button v-if="tab === 'orders'" class="app-btn-search" @click="handleSearch">
|
<a-button class="app-btn-search" @click="handleSearch">查询</a-button>
|
||||||
查询
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<a-space :size="8">
|
<a-space :size="8">
|
||||||
|
<a-button :loading="exporting" @click="handleExport">导出</a-button>
|
||||||
<a-button @click="loadList">刷新</a-button>
|
<a-button @click="loadList">刷新</a-button>
|
||||||
<PaginationPro
|
<PaginationPro
|
||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
@ -38,7 +45,7 @@
|
|||||||
<a-spin :loading="listLoading" class="list-spin">
|
<a-spin :loading="listLoading" class="list-spin">
|
||||||
<div ref="tableContainerRef" class="list-wrap">
|
<div ref="tableContainerRef" class="list-wrap">
|
||||||
<HandsontablePro
|
<HandsontablePro
|
||||||
:columns="hotColumns"
|
:columns="commissionColumns"
|
||||||
:data="tableRows"
|
:data="tableRows"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
@ -56,36 +63,20 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { buildStatusTagHtml } from '@/utils/statusTag'
|
import { buildStatusTagHtml } from '@/utils/statusTag'
|
||||||
import {
|
import { AgentPortalApi, type AgentCommissionDto } from '@/api/agent'
|
||||||
AgentPortalApi,
|
|
||||||
type AgentCommissionDto,
|
|
||||||
type AgentPortalOrderDto,
|
|
||||||
} from '@/api/agent'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AgentPortalOrders' })
|
defineOptions({ name: 'AgentPortalCommission' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 320))
|
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 320))
|
||||||
|
|
||||||
type TabKey = 'orders' | 'commission'
|
|
||||||
|
|
||||||
interface OrderRow {
|
|
||||||
id: number
|
|
||||||
serviceNo: string
|
|
||||||
trackNo: string
|
|
||||||
customerName: string
|
|
||||||
productCode: string
|
|
||||||
consigneeCountryName: string
|
|
||||||
orderStatus: string
|
|
||||||
receivableSubtotal: number | string
|
|
||||||
createTime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommissionRow {
|
interface CommissionRow {
|
||||||
id: number
|
id: number
|
||||||
serviceNo: string
|
serviceNo: string
|
||||||
@ -100,25 +91,53 @@ interface CommissionRow {
|
|||||||
createTime: string
|
createTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const tab = ref<TabKey>('orders')
|
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const tableRows = ref<Array<OrderRow | CommissionRow>>([])
|
const exporting = ref(false)
|
||||||
|
const tableRows = ref<CommissionRow[]>([])
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
const status = ref<string | undefined>()
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
|
const dateRange = ref<string[] | undefined>()
|
||||||
|
|
||||||
const orderColumns = [
|
const statusOptions = [
|
||||||
{ key: 'serviceNo', label: '运单号', width: 140 },
|
{ label: '预计返佣', value: 'estimated' },
|
||||||
{ key: 'trackNo', label: '追踪号', width: 140 },
|
{ label: '待结算', value: 'pending' },
|
||||||
{ key: 'customerName', label: '客户', width: 140 },
|
{ label: '已打款', value: 'paid' },
|
||||||
{ key: 'productCode', label: '产品', width: 120 },
|
{ label: '已作废', value: 'void' },
|
||||||
{ key: 'consigneeCountryName', label: '国家', width: 100 },
|
|
||||||
{ key: 'orderStatus', label: '状态', width: 110 },
|
|
||||||
{ key: 'receivableSubtotal', label: '应收', width: 100 },
|
|
||||||
{ key: 'createTime', label: '下单时间', width: 170 },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function resolveStatusName(st: string) {
|
||||||
|
switch (st) {
|
||||||
|
case 'estimated':
|
||||||
|
return '预计返佣'
|
||||||
|
case 'pending':
|
||||||
|
return '待结算'
|
||||||
|
case 'paid':
|
||||||
|
return '已打款'
|
||||||
|
case 'void':
|
||||||
|
return '已作废'
|
||||||
|
default:
|
||||||
|
return st || '--'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStatusTone(st: string): 'ok' | 'wait' | 'brand' | 'alert' | 'mute' {
|
||||||
|
switch (st) {
|
||||||
|
case 'paid':
|
||||||
|
return 'ok'
|
||||||
|
case 'pending':
|
||||||
|
return 'wait'
|
||||||
|
case 'estimated':
|
||||||
|
return 'brand'
|
||||||
|
case 'void':
|
||||||
|
return 'mute'
|
||||||
|
default:
|
||||||
|
return 'mute'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const commissionColumns = [
|
const commissionColumns = [
|
||||||
{ key: 'serviceNo', label: '运单号', width: 140 },
|
{ key: 'serviceNo', label: '运单号', width: 140 },
|
||||||
{ key: 'customerName', label: '客户', width: 140 },
|
{ key: 'customerName', label: '客户', width: 140 },
|
||||||
@ -131,60 +150,52 @@ const commissionColumns = [
|
|||||||
{ key: 'createTime', label: '计提时间', width: 170 },
|
{ key: 'createTime', label: '计提时间', width: 170 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const hotColumns = computed(() =>
|
|
||||||
tab.value === 'orders' ? orderColumns : commissionColumns,
|
|
||||||
)
|
|
||||||
|
|
||||||
function pick(row: Record<string, unknown>, camel: string, pascal: string) {
|
function pick(row: Record<string, unknown>, camel: string, pascal: string) {
|
||||||
return row[camel] ?? row[pascal]
|
return row[camel] ?? row[pascal]
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapOrder(raw: AgentPortalOrderDto): OrderRow {
|
|
||||||
const r = raw as unknown as Record<string, unknown>
|
|
||||||
return {
|
|
||||||
id: Number(pick(r, 'id', 'Id') ?? 0),
|
|
||||||
serviceNo: String(pick(r, 'serviceNo', 'ServiceNo') ?? ''),
|
|
||||||
trackNo: String(pick(r, 'trackNo', 'TrackNo') ?? ''),
|
|
||||||
customerName: String(pick(r, 'customerName', 'CustomerName') ?? ''),
|
|
||||||
productCode: String(pick(r, 'productCode', 'ProductCode') ?? ''),
|
|
||||||
consigneeCountryName: String(pick(r, 'consigneeCountryName', 'ConsigneeCountryName') ?? ''),
|
|
||||||
orderStatus: String(pick(r, 'orderStatus', 'OrderStatus') ?? ''),
|
|
||||||
receivableSubtotal: pick(r, 'receivableSubtotal', 'ReceivableSubtotal') as number | string,
|
|
||||||
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapCommission(raw: AgentCommissionDto): CommissionRow {
|
function mapCommission(raw: AgentCommissionDto): CommissionRow {
|
||||||
const r = raw as unknown as Record<string, unknown>
|
const r = raw as unknown as Record<string, unknown>
|
||||||
const st = String(pick(r, 'status', 'Status') ?? '')
|
const st = String(pick(r, 'status', 'Status') ?? '')
|
||||||
|
const baseType = String(pick(r, 'baseType', 'BaseType') ?? '')
|
||||||
return {
|
return {
|
||||||
id: Number(pick(r, 'id', 'Id') ?? 0),
|
id: Number(pick(r, 'id', 'Id') ?? 0),
|
||||||
serviceNo: String(pick(r, 'serviceNo', 'ServiceNo') ?? ''),
|
serviceNo: String(pick(r, 'serviceNo', 'ServiceNo') ?? ''),
|
||||||
customerName: String(pick(r, 'customerName', 'CustomerName') ?? ''),
|
customerName: String(pick(r, 'customerName', 'CustomerName') ?? ''),
|
||||||
baseType: String(pick(r, 'baseType', 'BaseType') ?? ''),
|
baseType: baseType === 'receivable' ? '应收' : baseType === 'freight' ? '运费' : baseType,
|
||||||
baseAmount: pick(r, 'baseAmount', 'BaseAmount') as number | string,
|
baseAmount: pick(r, 'baseAmount', 'BaseAmount') as number | string,
|
||||||
commissionRate: pick(r, 'commissionRate', 'CommissionRate') as number | string,
|
commissionRate: pick(r, 'commissionRate', 'CommissionRate') as number | string,
|
||||||
commissionAmount: pick(r, 'commissionAmount', 'CommissionAmount') as number | string,
|
commissionAmount: pick(r, 'commissionAmount', 'CommissionAmount') as number | string,
|
||||||
status: st,
|
status: st,
|
||||||
statusName: st === 'paid' ? '已打款' : '待结算',
|
statusName: resolveStatusName(st),
|
||||||
triggerOrderStatus: String(pick(r, 'triggerOrderStatus', 'TriggerOrderStatus') ?? ''),
|
triggerOrderStatus: String(pick(r, 'triggerOrderStatus', 'TriggerOrderStatus') ?? ''),
|
||||||
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildQuery() {
|
||||||
|
const range = dateRange.value || []
|
||||||
|
return {
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
status: status.value || undefined,
|
||||||
|
keyword: keyword.value.trim() || undefined,
|
||||||
|
createTimeStart: range[0] || undefined,
|
||||||
|
createTimeEnd: range[1] || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function customCellRenderer(
|
function customCellRenderer(
|
||||||
_row: number,
|
_row: number,
|
||||||
col: number,
|
col: number,
|
||||||
_value: unknown,
|
_value: unknown,
|
||||||
rowData: OrderRow | CommissionRow,
|
rowData: CommissionRow,
|
||||||
): string | null {
|
): string | null {
|
||||||
// 仅序号列
|
|
||||||
const dataColIndex = col - 1
|
const dataColIndex = col - 1
|
||||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
if (dataColIndex < 0 || dataColIndex >= commissionColumns.length) return null
|
||||||
const colKey = hotColumns.value[dataColIndex].key
|
const colKey = commissionColumns[dataColIndex].key
|
||||||
if (tab.value === 'commission' && colKey === 'statusName') {
|
if (colKey === 'statusName') {
|
||||||
const c = rowData as CommissionRow
|
return buildStatusTagHtml(rowData.statusName, resolveStatusTone(rowData.status))
|
||||||
return buildStatusTagHtml(c.statusName, c.status === 'paid' ? 'ok' : 'wait')
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -192,22 +203,9 @@ function customCellRenderer(
|
|||||||
async function loadList() {
|
async function loadList() {
|
||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
try {
|
try {
|
||||||
if (tab.value === 'orders') {
|
const res = await AgentPortalApi.commissions(buildQuery())
|
||||||
const res = await AgentPortalApi.orders({
|
tableRows.value = (res.items || []).map(mapCommission)
|
||||||
page: currentPage.value,
|
totalCount.value = Number(res.total || 0)
|
||||||
pageSize: pageSize.value,
|
|
||||||
keyword: keyword.value.trim() || undefined,
|
|
||||||
})
|
|
||||||
tableRows.value = (res.items || []).map(mapOrder)
|
|
||||||
totalCount.value = Number(res.total || 0)
|
|
||||||
} else {
|
|
||||||
const res = await AgentPortalApi.commissions({
|
|
||||||
page: currentPage.value,
|
|
||||||
pageSize: pageSize.value,
|
|
||||||
})
|
|
||||||
tableRows.value = (res.items || []).map(mapCommission)
|
|
||||||
totalCount.value = Number(res.total || 0)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e?.message || '加载失败')
|
Message.error(e?.message || '加载失败')
|
||||||
tableRows.value = []
|
tableRows.value = []
|
||||||
@ -217,22 +215,38 @@ async function loadList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
await AgentPortalApi.exportCommissions({
|
||||||
|
...buildQuery(),
|
||||||
|
page: 1,
|
||||||
|
pageSize: 5000,
|
||||||
|
})
|
||||||
|
Message.success('导出已开始')
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || '导出失败')
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
void loadList()
|
void loadList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTabChange() {
|
onMounted(() => {
|
||||||
currentPage.value = 1
|
const q = String(route.query.status || '').trim()
|
||||||
keyword.value = ''
|
if (['estimated', 'pending', 'paid', 'void'].includes(q)) {
|
||||||
|
status.value = q
|
||||||
|
}
|
||||||
void loadList()
|
void loadList()
|
||||||
}
|
})
|
||||||
|
|
||||||
onMounted(() => void loadList())
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.agent-orders-page {
|
.agent-commission-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--app-page-bg);
|
background: var(--app-page-bg);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -241,7 +255,7 @@ onMounted(() => void loadList())
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-orders-page-card {
|
.agent-commission-page-card {
|
||||||
background: var(--app-card-bg);
|
background: var(--app-card-bg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@ -251,17 +265,14 @@ onMounted(() => void loadList())
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orders-tabs {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin: 8px 0 12px;
|
margin: 0 0 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-left,
|
.toolbar-left,
|
||||||
@ -269,6 +280,7 @@ onMounted(() => void loadList())
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-spin {
|
.list-spin {
|
||||||
@ -38,6 +38,9 @@
|
|||||||
:stretch-columns="true"
|
:stretch-columns="true"
|
||||||
:height="handsontableHeight"
|
:height="handsontableHeight"
|
||||||
:read-only="true"
|
:read-only="true"
|
||||||
|
:show-action-column="true"
|
||||||
|
:action-renderer="actionRenderer"
|
||||||
|
@action-click="handleActionClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
@ -47,14 +50,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { AgentPortalApi, type AgentPortalCustomerDto } from '@/api/agent'
|
import { AgentPortalApi, type AgentPortalCustomerDto } from '@/api/agent'
|
||||||
|
import { buildPendingPaymentReminderText } from '@/utils/agentPortalCopy'
|
||||||
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||||
|
|
||||||
defineOptions({ name: 'AgentPortalCustomers' })
|
defineOptions({ name: 'AgentPortalCustomers' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 320))
|
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 320))
|
||||||
|
|
||||||
@ -65,6 +72,9 @@ interface CustomerRow {
|
|||||||
phone: string
|
phone: string
|
||||||
agentBindTime: string
|
agentBindTime: string
|
||||||
createTime: string
|
createTime: string
|
||||||
|
lastOrderTime: string
|
||||||
|
orderCount30d: number
|
||||||
|
pendingPaymentCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableRows = ref<CustomerRow[]>([])
|
const tableRows = ref<CustomerRow[]>([])
|
||||||
@ -73,11 +83,15 @@ const listLoading = ref(false)
|
|||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
const agentDisplayName = ref('渠道顾问')
|
||||||
|
|
||||||
const hotColumns = computed(() => [
|
const hotColumns = computed(() => [
|
||||||
{ key: 'customerCode', label: '客户编号', width: 140 },
|
{ key: 'customerCode', label: '客户编号', width: 130 },
|
||||||
{ key: 'customerName', label: '客户名称', width: 160 },
|
{ key: 'customerName', label: '客户名称', width: 140 },
|
||||||
{ key: 'phone', label: '手机', width: 130 },
|
{ key: 'phone', label: '手机', width: 120 },
|
||||||
|
{ key: 'orderCount30d', label: '近30天票数', width: 110 },
|
||||||
|
{ key: 'pendingPaymentCount', label: '待付款', width: 90 },
|
||||||
|
{ key: 'lastOrderTime', label: '最近下单', width: 170 },
|
||||||
{ key: 'agentBindTime', label: '绑定时间', width: 170 },
|
{ key: 'agentBindTime', label: '绑定时间', width: 170 },
|
||||||
{ key: 'createTime', label: '注册时间', width: 170 },
|
{ key: 'createTime', label: '注册时间', width: 170 },
|
||||||
])
|
])
|
||||||
@ -95,6 +109,45 @@ function mapRow(raw: AgentPortalCustomerDto): CustomerRow {
|
|||||||
phone: String(pick(r, 'phone', 'Phone') ?? ''),
|
phone: String(pick(r, 'phone', 'Phone') ?? ''),
|
||||||
agentBindTime: String(pick(r, 'agentBindTime', 'AgentBindTime') ?? ''),
|
agentBindTime: String(pick(r, 'agentBindTime', 'AgentBindTime') ?? ''),
|
||||||
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
createTime: String(pick(r, 'createTime', 'CreateTime') ?? ''),
|
||||||
|
lastOrderTime: String(pick(r, 'lastOrderTime', 'LastOrderTime') ?? ''),
|
||||||
|
orderCount30d: Number(pick(r, 'orderCount30d', 'OrderCount30d') ?? 0),
|
||||||
|
pendingPaymentCount: Number(pick(r, 'pendingPaymentCount', 'PendingPaymentCount') ?? 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionRenderer(rowData: CustomerRow) {
|
||||||
|
const buttons = [
|
||||||
|
renderWarehouseActionBtn('placeOrder', '代下单', 'ship'),
|
||||||
|
renderWarehouseActionBtn('orders', '看运单', 'view'),
|
||||||
|
]
|
||||||
|
if (rowData.pendingPaymentCount > 0) {
|
||||||
|
buttons.push(renderWarehouseActionBtn('remindPay', '催付款', 'pay'))
|
||||||
|
}
|
||||||
|
return wrapWarehouseActions(buttons)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActionClick(row: CustomerRow, _rowIndex: number, action?: string) {
|
||||||
|
if (!row || !action) return
|
||||||
|
if (action === 'placeOrder') {
|
||||||
|
void router.push({ path: '/agent/placeOrder', query: { customerId: String(row.id) } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action === 'orders') {
|
||||||
|
void router.push({ path: '/agent/orders', query: { customerId: String(row.id) } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action === 'remindPay') {
|
||||||
|
const text = buildPendingPaymentReminderText({
|
||||||
|
customerName: row.customerName,
|
||||||
|
pendingCount: row.pendingPaymentCount,
|
||||||
|
agentName: agentDisplayName.value,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
Message.success('催付款话术已复制')
|
||||||
|
} catch {
|
||||||
|
Message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +175,16 @@ function handleSearch() {
|
|||||||
void loadList()
|
void loadList()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => void loadList())
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const profile = await AgentPortalApi.profile()
|
||||||
|
const raw = profile as unknown as Record<string, unknown>
|
||||||
|
agentDisplayName.value = String(raw.agentName ?? raw.AgentName ?? raw.agentCode ?? raw.AgentCode ?? '渠道顾问')
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
void loadList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@ -13,6 +13,12 @@
|
|||||||
客户扫码或打开专属链接完成注册后,自动绑定到你名下;派送成功/失败后按比例计提返佣。
|
客户扫码或打开专属链接完成注册后,自动绑定到你名下;派送成功/失败后按比例计提返佣。
|
||||||
</p>
|
</p>
|
||||||
<div class="invite-card__actions">
|
<div class="invite-card__actions">
|
||||||
|
<a-select
|
||||||
|
v-model="templateKey"
|
||||||
|
:options="templateOptions"
|
||||||
|
style="width: 128px"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
<a-button type="primary" class="app-btn-primary-soft" @click="copyShareText">
|
<a-button type="primary" class="app-btn-primary-soft" @click="copyShareText">
|
||||||
复制邀请话术
|
复制邀请话术
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -20,6 +26,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="invite-card invite-card--rule">
|
||||||
|
<div class="invite-card__head">
|
||||||
|
<h3 class="invite-card__title">我的返佣规则</h3>
|
||||||
|
</div>
|
||||||
|
<ul class="rule-list">
|
||||||
|
<li>
|
||||||
|
<span class="rule-label">基数类型</span>
|
||||||
|
<span class="rule-value">{{ baseTypeName }}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="rule-label">返佣比例</span>
|
||||||
|
<span class="rule-value">{{ commissionRateText }}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="rule-label">适用产品</span>
|
||||||
|
<span class="rule-value">{{ productNamesText }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="invite-card__hint">规则由运营配置;改规则不追溯历史明细</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="invite-card invite-card--qr">
|
<section class="invite-card invite-card--qr">
|
||||||
<div class="invite-card__head">
|
<div class="invite-card__head">
|
||||||
<h3 class="invite-card__title">微信扫码注册</h3>
|
<h3 class="invite-card__title">微信扫码注册</h3>
|
||||||
@ -74,7 +101,7 @@
|
|||||||
<span class="howto-index">03</span>
|
<span class="howto-index">03</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="howto-title">查看客户与返佣</div>
|
<div class="howto-title">查看客户与返佣</div>
|
||||||
<div class="howto-desc">「名下客户」看绑定,「订单与返佣」看计提</div>
|
<div class="howto-desc">「名下客户」看绑定,「运单列表 / 返佣明细」看业务与计提</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -97,25 +124,63 @@ import InviteQrImage from '@/components/InviteQrImage.vue'
|
|||||||
import { AgentPortalApi, type AgentDto } from '@/api/agent'
|
import { AgentPortalApi, type AgentDto } from '@/api/agent'
|
||||||
import {
|
import {
|
||||||
buildAgentInviteRegisterUrl,
|
buildAgentInviteRegisterUrl,
|
||||||
buildAgentInviteShareText,
|
|
||||||
buildAgentInviteWechatRegisterUrl,
|
buildAgentInviteWechatRegisterUrl,
|
||||||
} from '@/utils/inviteLink'
|
} from '@/utils/inviteLink'
|
||||||
|
import {
|
||||||
|
AGENT_INVITE_TEMPLATES,
|
||||||
|
type AgentInviteTemplateKey,
|
||||||
|
} from '@/utils/agentPortalCopy'
|
||||||
|
|
||||||
defineOptions({ name: 'AgentPortalInvite' })
|
defineOptions({ name: 'AgentPortalInvite' })
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const profile = ref<AgentDto | null>(null)
|
const profile = ref<AgentDto | null>(null)
|
||||||
|
const templateKey = ref<AgentInviteTemplateKey>('wechat')
|
||||||
|
const templateOptions = AGENT_INVITE_TEMPLATES.map((t) => ({
|
||||||
|
label: t.label,
|
||||||
|
value: t.key,
|
||||||
|
}))
|
||||||
|
|
||||||
const agentCode = computed(() => {
|
const agentCode = computed(() => {
|
||||||
const raw = profile.value as unknown as Record<string, unknown> | null
|
const raw = profile.value as unknown as Record<string, unknown> | null
|
||||||
return String(raw?.agentCode ?? raw?.AgentCode ?? '').trim()
|
return String(raw?.agentCode ?? raw?.AgentCode ?? '').trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const profileRaw = computed(
|
||||||
|
() => (profile.value as unknown as Record<string, unknown> | null) || {},
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseTypeName = computed(() => {
|
||||||
|
const name = String(profileRaw.value.baseTypeName ?? profileRaw.value.BaseTypeName ?? '').trim()
|
||||||
|
if (name) return name
|
||||||
|
const code = String(profileRaw.value.baseType ?? profileRaw.value.BaseType ?? '').trim()
|
||||||
|
if (code === 'receivable') return '应收'
|
||||||
|
if (code === 'freight') return '运费'
|
||||||
|
return code || '运费'
|
||||||
|
})
|
||||||
|
|
||||||
|
const commissionRateText = computed(() => {
|
||||||
|
const rate = Number(profileRaw.value.commissionRate ?? profileRaw.value.CommissionRate ?? 0)
|
||||||
|
return `${(rate * 100).toFixed(2)}%`
|
||||||
|
})
|
||||||
|
|
||||||
|
const productNamesText = computed(() => {
|
||||||
|
const names =
|
||||||
|
(profileRaw.value.salesProductNames as string[] | undefined) ||
|
||||||
|
(profileRaw.value.SalesProductNames as string[] | undefined) ||
|
||||||
|
[]
|
||||||
|
if (!names.length) return '不限产品'
|
||||||
|
return names.join('、')
|
||||||
|
})
|
||||||
|
|
||||||
const pcUrl = computed(() => buildAgentInviteRegisterUrl(agentCode.value))
|
const pcUrl = computed(() => buildAgentInviteRegisterUrl(agentCode.value))
|
||||||
const wechatUrl = computed(() => buildAgentInviteWechatRegisterUrl(agentCode.value))
|
const wechatUrl = computed(() => buildAgentInviteWechatRegisterUrl(agentCode.value))
|
||||||
const shareText = computed(() =>
|
const shareText = computed(() => {
|
||||||
buildAgentInviteShareText(agentCode.value, wechatUrl.value || pcUrl.value),
|
const url = wechatUrl.value || pcUrl.value
|
||||||
)
|
const tpl =
|
||||||
|
AGENT_INVITE_TEMPLATES.find((t) => t.key === templateKey.value) || AGENT_INVITE_TEMPLATES[0]
|
||||||
|
return tpl.build(agentCode.value, url)
|
||||||
|
})
|
||||||
|
|
||||||
async function copyText(text: string) {
|
async function copyText(text: string) {
|
||||||
const value = text.trim()
|
const value = text.trim()
|
||||||
@ -169,6 +234,7 @@ onMounted(() => void loadProfile())
|
|||||||
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
|
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'hero qr'
|
'hero qr'
|
||||||
|
'rule howto'
|
||||||
'pc howto';
|
'pc howto';
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@ -189,6 +255,10 @@ onMounted(() => void loadProfile())
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invite-card--rule {
|
||||||
|
grid-area: rule;
|
||||||
|
}
|
||||||
|
|
||||||
.invite-card--qr {
|
.invite-card--qr {
|
||||||
grid-area: qr;
|
grid-area: qr;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -203,6 +273,34 @@ onMounted(() => void loadProfile())
|
|||||||
grid-area: howto;
|
grid-area: howto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-label {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-value {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.invite-card--empty {
|
.invite-card--empty {
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
margin: 48px auto;
|
margin: 48px auto;
|
||||||
@ -366,6 +464,7 @@ onMounted(() => void loadProfile())
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'hero'
|
'hero'
|
||||||
|
'rule'
|
||||||
'qr'
|
'qr'
|
||||||
'pc'
|
'pc'
|
||||||
'howto';
|
'howto';
|
||||||
|
|||||||
152
src/views/agent/workplace/components/AgentAlertBar.vue
Normal file
152
src/views/agent/workplace/components/AgentAlertBar.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="alerts.length" class="alert-bar">
|
||||||
|
<button
|
||||||
|
v-for="item in alerts"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="alert-chip"
|
||||||
|
:class="`alert-chip--${item.tone}`"
|
||||||
|
@click="item.onClick()"
|
||||||
|
>
|
||||||
|
<span class="alert-chip__label">{{ item.label }}</span>
|
||||||
|
<span class="alert-chip__value">{{ item.value }}</span>
|
||||||
|
<span v-if="item.action" class="alert-chip__action">{{ item.action }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import type { AgentWorkplaceStats } from '@/api/agent'
|
||||||
|
import { buildPendingPaymentReminderText } from '@/utils/agentPortalCopy'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stats: AgentWorkplaceStats | null
|
||||||
|
agentName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const alerts = computed(() => {
|
||||||
|
const s = props.stats
|
||||||
|
if (!s) return []
|
||||||
|
const list: Array<{
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
tone: 'warning' | 'danger' | 'primary'
|
||||||
|
action?: string
|
||||||
|
onClick: () => void
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
if (s.pendingPayment > 0) {
|
||||||
|
list.push({
|
||||||
|
key: 'pendingPayment',
|
||||||
|
label: '待付款',
|
||||||
|
value: `${s.pendingPayment} 票`,
|
||||||
|
tone: 'warning',
|
||||||
|
action: '复制催付话术',
|
||||||
|
onClick: async () => {
|
||||||
|
const text = buildPendingPaymentReminderText({
|
||||||
|
pendingCount: s.pendingPayment,
|
||||||
|
agentName: props.agentName || '渠道顾问',
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
Message.success('催付款话术已复制,可发给客户')
|
||||||
|
} catch {
|
||||||
|
Message.error('复制失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (s.problematic > 0) {
|
||||||
|
list.push({
|
||||||
|
key: 'problematic',
|
||||||
|
label: '问题件',
|
||||||
|
value: `${s.problematic} 票`,
|
||||||
|
tone: 'danger',
|
||||||
|
action: '去处理',
|
||||||
|
onClick: () => {
|
||||||
|
void router.push({ path: '/agent/orders', query: { status: 'problematic' } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (s.deliveryFailed > 0) {
|
||||||
|
list.push({
|
||||||
|
key: 'deliveryFailed',
|
||||||
|
label: '派送失败',
|
||||||
|
value: `${s.deliveryFailed} 票`,
|
||||||
|
tone: 'danger',
|
||||||
|
action: '去查看',
|
||||||
|
onClick: () => {
|
||||||
|
void router.push({ path: '/agent/orders', query: { status: 'deliveryFailed' } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (s.weekNewCustomers > 0) {
|
||||||
|
list.push({
|
||||||
|
key: 'weekNew',
|
||||||
|
label: '本周新增绑定',
|
||||||
|
value: `${s.weekNewCustomers} 位`,
|
||||||
|
tone: 'primary',
|
||||||
|
action: '看客户',
|
||||||
|
onClick: () => {
|
||||||
|
void router.push('/agent/customers')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.alert-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip--warning {
|
||||||
|
background: rgba(255, 125, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip--danger {
|
||||||
|
background: rgba(245, 63, 63, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip--primary {
|
||||||
|
background: var(--color-primary-light-1, #e8f3ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip__value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-chip__action {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
208
src/views/agent/workplace/components/AgentBanner.vue
Normal file
208
src/views/agent/workplace/components/AgentBanner.vue
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="banner">
|
||||||
|
<div class="banner-row">
|
||||||
|
<div class="banner-hello">
|
||||||
|
<a-avatar :size="40" class="banner-hello__avatar">
|
||||||
|
<img :src="avatarUrl" alt="" />
|
||||||
|
</a-avatar>
|
||||||
|
<p class="banner-hello__line">
|
||||||
|
<span class="banner-hello__greet">{{ greetText }}</span>
|
||||||
|
<span class="banner-hello__name">{{ displayName }}</span>
|
||||||
|
</p>
|
||||||
|
<div class="banner-local-clock" aria-live="polite">
|
||||||
|
<span class="banner-local-clock__label">本地</span>
|
||||||
|
<span class="banner-local-clock__date">{{ dateText }}</span>
|
||||||
|
<span class="banner-local-clock__time">{{ timeText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="commission-chip"
|
||||||
|
:class="{ 'commission-chip--alert': pendingAmount > 0 }"
|
||||||
|
title="查看返佣明细"
|
||||||
|
@click="goCommission"
|
||||||
|
>
|
||||||
|
<span class="commission-chip__label">待结算返佣</span>
|
||||||
|
<span class="commission-chip__value">
|
||||||
|
<template v-if="loading">···</template>
|
||||||
|
<template v-else>¥{{ pendingAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</template>
|
||||||
|
</span>
|
||||||
|
<span class="commission-chip__cta">去查看</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||||
|
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||||
|
import type { AgentWorkplaceStats } from '@/api/agent'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stats: AgentWorkplaceStats | null
|
||||||
|
loading?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const rbacUserStore = useRbacUserStore()
|
||||||
|
const now = ref(new Date())
|
||||||
|
let timerId: number | undefined
|
||||||
|
|
||||||
|
const displayName = computed(
|
||||||
|
() => userStore.name?.trim() || rbacUserStore.userInfo?.account || '代理',
|
||||||
|
)
|
||||||
|
const avatarUrl = computed(() =>
|
||||||
|
resolveAvatarUrl(userStore.avatar || rbacUserStore.userInfo?.avatar),
|
||||||
|
)
|
||||||
|
const pendingAmount = computed(() => Number(props.stats?.pendingCommissionAmount || 0))
|
||||||
|
|
||||||
|
const greetText = computed(() => {
|
||||||
|
const h = now.value.getHours()
|
||||||
|
if (h < 11) return '早上好,'
|
||||||
|
if (h < 14) return '中午好,'
|
||||||
|
if (h < 18) return '下午好,'
|
||||||
|
return '晚上好,'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dateText = computed(() =>
|
||||||
|
new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'short',
|
||||||
|
}).format(now.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeText = computed(() =>
|
||||||
|
new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(now.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
function goCommission() {
|
||||||
|
void router.push('/agent/commission')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
timerId = window.setInterval(() => {
|
||||||
|
now.value = new Date()
|
||||||
|
}, 30_000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timerId) window.clearInterval(timerId)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.banner {
|
||||||
|
padding: 16px 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-hello {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-hello__avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-hello__line {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-hello__greet {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-hello__name {
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-local-clock {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-local-clock__time {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary-light-1, #e8f3ff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-chip--alert {
|
||||||
|
background: rgba(255, 125, 0, 0.12);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 125, 0, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-chip__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-chip__value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-chip__cta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
305
src/views/agent/workplace/components/AgentDataPanel.vue
Normal file
305
src/views/agent/workplace/components/AgentDataPanel.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stat-panel">
|
||||||
|
<div class="stat-panel__body">
|
||||||
|
<div v-if="loading" class="stat-panel__loading">
|
||||||
|
<a-spin />
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="stat-grid stat-grid--priority">
|
||||||
|
<button
|
||||||
|
v-for="item in priorityItems"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="stat-card"
|
||||||
|
:class="[
|
||||||
|
`stat-card--${item.tone}`,
|
||||||
|
{
|
||||||
|
'stat-card--clickable': Boolean(item.to),
|
||||||
|
'stat-card--alert': item.alert && item.value > 0,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@click="onStatClick(item)"
|
||||||
|
>
|
||||||
|
<div class="stat-card__main">
|
||||||
|
<div class="stat-card__meta">
|
||||||
|
<span class="stat-card__label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__body">
|
||||||
|
<span class="stat-card__value">{{ formatValue(item) }}</span>
|
||||||
|
<span v-if="item.unit" class="stat-card__unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="item.hint" class="stat-card__hint">{{ item.hint }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-grid stat-grid--secondary">
|
||||||
|
<button
|
||||||
|
v-for="item in secondaryItems"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="stat-card stat-card--compact"
|
||||||
|
:class="[`stat-card--${item.tone}`, { 'stat-card--clickable': Boolean(item.to) }]"
|
||||||
|
@click="onStatClick(item)"
|
||||||
|
>
|
||||||
|
<div class="stat-card__main">
|
||||||
|
<div class="stat-card__meta">
|
||||||
|
<span class="stat-card__label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card__body">
|
||||||
|
<span class="stat-card__value">{{ formatValue(item) }}</span>
|
||||||
|
<span v-if="item.unit" class="stat-card__unit">{{ item.unit }}</span>
|
||||||
|
<span
|
||||||
|
v-else-if="item.showTrend"
|
||||||
|
class="stat-card__trend"
|
||||||
|
:class="item.trendUp ? 'is-up' : 'is-down'"
|
||||||
|
>
|
||||||
|
{{ item.trendUp ? '↑' : '↓' }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { AgentWorkplaceStats } from '@/api/agent'
|
||||||
|
|
||||||
|
type StatTone = 'neutral' | 'primary' | 'warning' | 'danger' | 'success'
|
||||||
|
|
||||||
|
type StatItem = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
precision?: number
|
||||||
|
unit?: string
|
||||||
|
showTrend?: boolean
|
||||||
|
trendUp?: boolean
|
||||||
|
tone: StatTone
|
||||||
|
alert?: boolean
|
||||||
|
hint?: string
|
||||||
|
to?: { path: string; query?: Record<string, string> }
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stats: AgentWorkplaceStats | null
|
||||||
|
loading?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const allItems = computed<StatItem[]>(() => {
|
||||||
|
const data = props.stats
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'pendingPayment',
|
||||||
|
label: '待付款',
|
||||||
|
value: data?.pendingPayment ?? 0,
|
||||||
|
unit: '票',
|
||||||
|
tone: 'warning',
|
||||||
|
alert: true,
|
||||||
|
hint: '点击查看',
|
||||||
|
to: { path: '/agent/orders', query: { status: 'pendingPayment' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shipping',
|
||||||
|
label: '运输中',
|
||||||
|
value: data?.shipping ?? 0,
|
||||||
|
unit: '票',
|
||||||
|
tone: 'primary',
|
||||||
|
hint: '点击查看',
|
||||||
|
to: { path: '/agent/orders', query: { status: 'shipping' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'problematic',
|
||||||
|
label: '问题件',
|
||||||
|
value: data?.problematic ?? 0,
|
||||||
|
unit: '票',
|
||||||
|
tone: 'danger',
|
||||||
|
alert: true,
|
||||||
|
hint: '点击查看',
|
||||||
|
to: { path: '/agent/orders', query: { status: 'problematic' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'estimatedCommission',
|
||||||
|
label: '预计返佣',
|
||||||
|
value: data?.estimatedCommissionCount ?? 0,
|
||||||
|
unit: '笔',
|
||||||
|
tone: 'primary',
|
||||||
|
hint: '下单后可见',
|
||||||
|
to: { path: '/agent/commission', query: { status: 'estimated' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pendingCommission',
|
||||||
|
label: '待结算返佣',
|
||||||
|
value: data?.pendingCommissionCount ?? 0,
|
||||||
|
unit: '笔',
|
||||||
|
tone: 'warning',
|
||||||
|
alert: true,
|
||||||
|
hint: '点击查看',
|
||||||
|
to: { path: '/agent/commission', query: { status: 'pending' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'customers',
|
||||||
|
label: '名下客户',
|
||||||
|
value: data?.customerCount ?? 0,
|
||||||
|
unit: '位',
|
||||||
|
tone: 'primary',
|
||||||
|
to: { path: '/agent/customers' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'weekNew',
|
||||||
|
label: '本周新增',
|
||||||
|
value: data?.weekNewCustomers ?? 0,
|
||||||
|
unit: '位',
|
||||||
|
tone: 'primary',
|
||||||
|
to: { path: '/agent/customers' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'todayCreated',
|
||||||
|
label: '今日下单',
|
||||||
|
value: data?.todayCreated ?? 0,
|
||||||
|
unit: '票',
|
||||||
|
tone: 'primary',
|
||||||
|
to: { path: '/agent/orders' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'deliveryFailed',
|
||||||
|
label: '派送失败',
|
||||||
|
value: data?.deliveryFailed ?? 0,
|
||||||
|
unit: '票',
|
||||||
|
tone: 'danger',
|
||||||
|
alert: true,
|
||||||
|
to: { path: '/agent/orders', query: { status: 'deliveryFailed' } },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const priorityItems = computed(() => allItems.value.slice(0, 4))
|
||||||
|
const secondaryItems = computed(() => allItems.value.slice(4))
|
||||||
|
|
||||||
|
function formatValue(item: StatItem) {
|
||||||
|
const n = Number(item.value) || 0
|
||||||
|
if (item.precision != null && item.precision > 0) return n.toFixed(item.precision)
|
||||||
|
return Math.round(n).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStatClick(item: StatItem) {
|
||||||
|
if (!item.to) return
|
||||||
|
void router.push(item.to)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.stat-panel {
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-panel__loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid--priority {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid--secondary {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 88px;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: default;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--compact {
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--compact .stat-card__value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__trend.is-up {
|
||||||
|
color: rgb(var(--success-6));
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__trend.is-down {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--alert .stat-card__value {
|
||||||
|
color: rgb(var(--warning-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--danger.stat-card--alert .stat-card__value {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.stat-grid--priority,
|
||||||
|
.stat-grid--secondary {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
src/views/agent/workplace/components/AgentInviteCard.vue
Normal file
78
src/views/agent/workplace/components/AgentInviteCard.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<button type="button" class="invite-chip" @click="goInvite">
|
||||||
|
<span class="invite-chip__icon" aria-hidden="true">
|
||||||
|
<icon-share-internal />
|
||||||
|
</span>
|
||||||
|
<span class="invite-chip__body">
|
||||||
|
<span class="invite-chip__title">邀请获客</span>
|
||||||
|
<span class="invite-chip__desc">复制专属链接 / 二维码,客户注册即绑定</span>
|
||||||
|
</span>
|
||||||
|
<icon-right class="invite-chip__arrow" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { IconRight, IconShareInternal } from '@arco-design/web-vue/es/icon'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AgentInviteCard' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function goInvite() {
|
||||||
|
void router.push('/agent/invite')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.invite-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-chip__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-primary-light-1, #e8f3ff);
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-chip__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-chip__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-chip__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-chip__arrow {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
112
src/views/agent/workplace/components/AgentQuickOps.vue
Normal file
112
src/views/agent/workplace/components/AgentQuickOps.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<a-card
|
||||||
|
class="general-card"
|
||||||
|
title="快捷作业"
|
||||||
|
:header-style="{ paddingBottom: '0' }"
|
||||||
|
:body-style="{ padding: '12px 16px 16px' }"
|
||||||
|
>
|
||||||
|
<div class="start-business-flow">
|
||||||
|
<button
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="step.path"
|
||||||
|
type="button"
|
||||||
|
class="flow-step"
|
||||||
|
@click="go(step.path)"
|
||||||
|
>
|
||||||
|
<span class="flow-step__index">{{ index + 1 }}</span>
|
||||||
|
<span class="flow-step__icon">
|
||||||
|
<component :is="step.icon" />
|
||||||
|
</span>
|
||||||
|
<span class="flow-step__title">{{ step.title }}</span>
|
||||||
|
<icon-right class="flow-step__arrow" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
IconGift,
|
||||||
|
IconList,
|
||||||
|
IconPlus,
|
||||||
|
IconRight,
|
||||||
|
IconSearch,
|
||||||
|
} from '@arco-design/web-vue/es/icon'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AgentQuickOps' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ title: '邀请获客', path: '/agent/invite', icon: IconGift },
|
||||||
|
{ title: '运费查价', path: '/agent/priceCheck', icon: IconSearch },
|
||||||
|
{ title: '运单下单', path: '/agent/placeOrder', icon: IconPlus },
|
||||||
|
{ title: '运单列表', path: '/agent/orders', icon: IconList },
|
||||||
|
]
|
||||||
|
|
||||||
|
function go(path: string) {
|
||||||
|
void router.push(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.start-business-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary-light-1, #e8f3ff);
|
||||||
|
|
||||||
|
.flow-step__title,
|
||||||
|
.flow-step__arrow,
|
||||||
|
.flow-step__icon {
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step__index {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step__title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-step__arrow {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
241
src/views/agent/workplace/components/AgentRecentCommission.vue
Normal file
241
src/views/agent/workplace/components/AgentRecentCommission.vue
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="recent-panel">
|
||||||
|
<div class="recent-panel__head">
|
||||||
|
<span class="recent-panel__title">最近返佣</span>
|
||||||
|
<a-button type="text" size="mini" @click="goAll">全部</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recent-panel__body">
|
||||||
|
<div v-if="loading" class="recent-state">
|
||||||
|
<a-spin />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!rows.length" class="recent-state recent-state--empty">
|
||||||
|
<p class="recent-state__title">暂无返佣明细</p>
|
||||||
|
<p class="recent-state__desc">名下客户下单后会出现预计返佣,派送完成锁定为待结算</p>
|
||||||
|
</div>
|
||||||
|
<ul v-else class="commission-list">
|
||||||
|
<li
|
||||||
|
v-for="row in rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="commission-item"
|
||||||
|
@click="goAll"
|
||||||
|
>
|
||||||
|
<div class="commission-item__main">
|
||||||
|
<span class="commission-item__no">{{ row.serviceNo || `#${row.orderId}` }}</span>
|
||||||
|
<span class="commission-item__name">{{ row.customerName || '客户' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="commission-item__side">
|
||||||
|
<span class="commission-item__amount">¥{{ formatAmount(row.commissionAmount) }}</span>
|
||||||
|
<span
|
||||||
|
class="commission-item__status"
|
||||||
|
:class="{
|
||||||
|
'is-paid': row.status === 'paid',
|
||||||
|
'is-pending': row.status === 'pending',
|
||||||
|
'is-estimated': row.status === 'estimated',
|
||||||
|
'is-void': row.status === 'void',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ statusLabel(row.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { AgentPortalApi, type AgentCommissionDto } from '@/api/agent'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AgentRecentCommission' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(true)
|
||||||
|
const rows = ref<AgentCommissionDto[]>([])
|
||||||
|
|
||||||
|
function pick(row: Record<string, unknown>, camel: string, pascal: string) {
|
||||||
|
return row[camel] ?? row[pascal]
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(raw: AgentCommissionDto): AgentCommissionDto {
|
||||||
|
const r = raw as unknown as Record<string, unknown>
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
id: Number(pick(r, 'id', 'Id') ?? 0),
|
||||||
|
orderId: Number(pick(r, 'orderId', 'OrderId') ?? 0),
|
||||||
|
serviceNo: String(pick(r, 'serviceNo', 'ServiceNo') ?? ''),
|
||||||
|
customerName: String(pick(r, 'customerName', 'CustomerName') ?? ''),
|
||||||
|
commissionAmount: Number(pick(r, 'commissionAmount', 'CommissionAmount') ?? 0),
|
||||||
|
status: String(pick(r, 'status', 'Status') ?? 'pending'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(v: number) {
|
||||||
|
return Number(v || 0).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(st?: string) {
|
||||||
|
switch (st) {
|
||||||
|
case 'estimated':
|
||||||
|
return '预计返佣'
|
||||||
|
case 'pending':
|
||||||
|
return '待结算'
|
||||||
|
case 'paid':
|
||||||
|
return '已打款'
|
||||||
|
case 'void':
|
||||||
|
return '已作废'
|
||||||
|
default:
|
||||||
|
return '待结算'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goAll() {
|
||||||
|
void router.push('/agent/commission')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await AgentPortalApi.commissions({ page: 1, pageSize: 8 })
|
||||||
|
rows.value = (res.items || []).map(normalize)
|
||||||
|
} catch {
|
||||||
|
rows.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => void loadList())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.recent-panel {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-panel__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-panel__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-state {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-state__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-state__desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__main {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__no {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__side {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__amount {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__status {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__status.is-pending {
|
||||||
|
color: rgb(var(--warning-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__status.is-estimated {
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__status.is-paid {
|
||||||
|
color: rgb(var(--success-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.commission-item__status.is-void {
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
200
src/views/agent/workplace/components/AgentTrendPanel.vue
Normal file
200
src/views/agent/workplace/components/AgentTrendPanel.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<a-card
|
||||||
|
class="trend-card"
|
||||||
|
:bordered="false"
|
||||||
|
:header-style="{ display: 'none' }"
|
||||||
|
:body-style="{
|
||||||
|
padding: '12px 16px 16px',
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="trend-head">
|
||||||
|
<span class="trend-head__title">近7日趋势</span>
|
||||||
|
<a-radio-group v-model="metric" type="button" size="small">
|
||||||
|
<a-radio value="orders">下单量</a-radio>
|
||||||
|
<a-radio value="commission">返佣额</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin v-if="loading" class="trend-loading" />
|
||||||
|
<div v-else-if="isEmpty" class="trend-empty">
|
||||||
|
<p class="trend-empty__title">暂无趋势数据</p>
|
||||||
|
<p class="trend-empty__desc">邀请客户并代客下单后,这里会显示近7日变化</p>
|
||||||
|
<div class="trend-empty__actions">
|
||||||
|
<a-button type="primary" @click="go('/agent/invite')">邀请获客</a-button>
|
||||||
|
<a-button @click="go('/agent/placeOrder')">运单下单</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="trend-chart-wrap">
|
||||||
|
<Chart width="100%" height="260px" :options="chartOption" />
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import Chart from '@/components/chart/index.vue'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
import { AgentPortalApi } from '@/api/agent'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AgentTrendPanel' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const metric = ref<'orders' | 'commission'>('orders')
|
||||||
|
const loading = ref(true)
|
||||||
|
const labels = ref<string[]>([])
|
||||||
|
const orderCounts = ref<number[]>([])
|
||||||
|
const commissionAmounts = ref<number[]>([])
|
||||||
|
|
||||||
|
const isEmpty = computed(() => {
|
||||||
|
if (!labels.value.length) return true
|
||||||
|
const orders = orderCounts.value.reduce((s, n) => s + (Number(n) || 0), 0)
|
||||||
|
const amount = commissionAmounts.value.reduce((s, n) => s + (Number(n) || 0), 0)
|
||||||
|
return orders === 0 && amount === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const seriesData = computed(() =>
|
||||||
|
metric.value === 'orders' ? orderCounts.value : commissionAmounts.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const chartOption = computed(() => {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const isDark = appStore.theme === 'dark'
|
||||||
|
const primary = '#1d7281'
|
||||||
|
return {
|
||||||
|
animationDuration: 280,
|
||||||
|
grid: { left: 8, right: 8, top: 16, bottom: 24, containLabel: true },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis' as const,
|
||||||
|
confine: true,
|
||||||
|
valueFormatter: (v: number) =>
|
||||||
|
metric.value === 'orders'
|
||||||
|
? `${v} 票`
|
||||||
|
: `¥${Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
data: labels.value,
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLine: { lineStyle: { color: isDark ? 'rgba(255,255,255,0.12)' : '#E5E6EB' } },
|
||||||
|
axisLabel: { color: isDark ? 'rgba(255,255,255,0.55)' : '#86909C', fontSize: 11 },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value' as const,
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: { color: isDark ? 'rgba(255,255,255,0.08)' : '#F2F3F5', type: 'dashed' as const },
|
||||||
|
},
|
||||||
|
axisLabel: { color: isDark ? 'rgba(255,255,255,0.55)' : '#86909C', fontSize: 11 },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line' as const,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
data: seriesData.value,
|
||||||
|
name: metric.value === 'orders' ? '下单量' : '返佣额',
|
||||||
|
lineStyle: { width: 2, color: primary },
|
||||||
|
itemStyle: { color: primary },
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear' as const,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: 'rgba(29, 114, 129, 0.22)' },
|
||||||
|
{ offset: 1, color: 'rgba(29, 114, 129, 0.02)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadTrend() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const trend = await AgentPortalApi.workplaceTrend(7)
|
||||||
|
labels.value = trend.points.map((p) => p.label)
|
||||||
|
orderCounts.value = trend.points.map((p) => p.orderCount)
|
||||||
|
commissionAmounts.value = trend.points.map((p) => Number(p.commissionAmount) || 0)
|
||||||
|
} catch {
|
||||||
|
labels.value = []
|
||||||
|
orderCounts.value = []
|
||||||
|
commissionAmounts.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function go(path: string) {
|
||||||
|
void router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => void loadTrend())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.trend-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-head__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-loading,
|
||||||
|
.trend-empty,
|
||||||
|
.trend-chart-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-empty__title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-empty__desc {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-empty__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-chart-wrap {
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
208
src/views/agent/workplace/index.vue
Normal file
208
src/views/agent/workplace/index.vue
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="workplace-container workplace-container--agent">
|
||||||
|
<div class="container">
|
||||||
|
<div class="left-side">
|
||||||
|
<div class="panel panel--hero">
|
||||||
|
<AgentBanner :stats="stats" :loading="statsLoading" />
|
||||||
|
<AgentAlertBar :stats="stats" :agent-name="agentName" />
|
||||||
|
<AgentDataPanel :stats="stats" :loading="statsLoading" />
|
||||||
|
</div>
|
||||||
|
<div class="panel panel--analysis panel--analysis-fill">
|
||||||
|
<AgentTrendPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-side">
|
||||||
|
<AgentInviteCard />
|
||||||
|
<div class="panel moduler-wrap">
|
||||||
|
<AgentQuickOps />
|
||||||
|
</div>
|
||||||
|
<div class="panel panel--recent">
|
||||||
|
<AgentRecentCommission />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { AgentPortalApi, type AgentWorkplaceStats } from '@/api/agent'
|
||||||
|
import AgentAlertBar from './components/AgentAlertBar.vue'
|
||||||
|
import AgentBanner from './components/AgentBanner.vue'
|
||||||
|
import AgentDataPanel from './components/AgentDataPanel.vue'
|
||||||
|
import AgentInviteCard from './components/AgentInviteCard.vue'
|
||||||
|
import AgentQuickOps from './components/AgentQuickOps.vue'
|
||||||
|
import AgentRecentCommission from './components/AgentRecentCommission.vue'
|
||||||
|
import AgentTrendPanel from './components/AgentTrendPanel.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AgentPortalWorkplace' })
|
||||||
|
|
||||||
|
const statsLoading = ref(true)
|
||||||
|
const stats = ref<AgentWorkplaceStats | null>(null)
|
||||||
|
const agentName = ref('渠道顾问')
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
statsLoading.value = true
|
||||||
|
try {
|
||||||
|
stats.value = await AgentPortalApi.workplaceStats()
|
||||||
|
} catch {
|
||||||
|
stats.value = null
|
||||||
|
} finally {
|
||||||
|
statsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const profile = await AgentPortalApi.profile()
|
||||||
|
const raw = profile as unknown as Record<string, unknown>
|
||||||
|
agentName.value = String(raw.agentName ?? raw.AgentName ?? raw.agentCode ?? '渠道顾问')
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
void loadStats()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.workplace-container {
|
||||||
|
--wp-card-radius: 16px;
|
||||||
|
--wp-inner-radius: 12px;
|
||||||
|
--wp-card-border: none;
|
||||||
|
--wp-card-bg: #ffffff;
|
||||||
|
--wp-card-shadow:
|
||||||
|
inset 0 2px 8px rgba(15, 23, 42, 0.07),
|
||||||
|
inset 0 1px 1px rgba(15, 23, 42, 0.04);
|
||||||
|
--wp-well-bg: #ffffff;
|
||||||
|
--wp-well-shadow:
|
||||||
|
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
|
||||||
|
inset 0 1px 4px rgba(15, 23, 42, 0.05);
|
||||||
|
--wp-well-shadow-deep:
|
||||||
|
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
|
||||||
|
inset 0 2px 6px rgba(15, 23, 42, 0.07);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side {
|
||||||
|
flex: 7;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side .panel {
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side .panel--hero {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: visible;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side .panel--analysis-fill {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
flex: 3;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 380px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side .moduler-wrap {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side .panel--recent {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
overflow: auto;
|
||||||
|
border: var(--wp-card-border);
|
||||||
|
border-radius: var(--wp-card-radius);
|
||||||
|
background: var(--wp-card-bg);
|
||||||
|
box-shadow: var(--wp-card-shadow);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workplace-container :deep(.stat-card),
|
||||||
|
.workplace-container :deep(.invite-chip),
|
||||||
|
.workplace-container :deep(.commission-item) {
|
||||||
|
border: none;
|
||||||
|
background: var(--wp-well-bg);
|
||||||
|
box-shadow: var(--wp-well-shadow);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workplace-container :deep(.stat-card--clickable:hover),
|
||||||
|
.workplace-container :deep(.invite-chip:hover),
|
||||||
|
.workplace-container :deep(.commission-item:hover) {
|
||||||
|
box-shadow: var(--wp-well-shadow-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side,
|
||||||
|
.right-side {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side .panel--analysis-fill {
|
||||||
|
flex: none;
|
||||||
|
min-height: 360px;
|
||||||
|
height: min(52vh, 480px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side .panel--recent {
|
||||||
|
flex: none;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -118,6 +118,7 @@
|
|||||||
import { hasPermission } from '@/utils/rbac-permission';
|
import { hasPermission } from '@/utils/rbac-permission';
|
||||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
|
||||||
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
|
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
|
||||||
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MyOrders',
|
name: 'MyOrders',
|
||||||
@ -126,6 +127,20 @@
|
|||||||
const { t, te } = useI18n();
|
const { t, te } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
const isAgentPortal = computed(() => rbacUserStore.isAgentPortal());
|
||||||
|
const placeOrderPath = computed(() =>
|
||||||
|
isAgentPortal.value ? '/agent/placeOrder' : '/order/placeOrder',
|
||||||
|
);
|
||||||
|
const priceCheckPath = computed(() =>
|
||||||
|
isAgentPortal.value ? '/agent/priceCheck' : '/order/priceCheck',
|
||||||
|
);
|
||||||
|
/** 代理从名下客户跳入时带 customerId,列表按该客户过滤 */
|
||||||
|
const queryCustomerId = computed(() => {
|
||||||
|
const raw = route.query.customerId;
|
||||||
|
const id = Number(Array.isArray(raw) ? raw[0] : raw);
|
||||||
|
return Number.isFinite(id) && id > 0 ? id : undefined;
|
||||||
|
});
|
||||||
const isFulfillList = computed(() => route.meta.listMode === 'fulfill');
|
const isFulfillList = computed(() => route.meta.listMode === 'fulfill');
|
||||||
const listFulfillmentTypeFilter = computed(() =>
|
const listFulfillmentTypeFilter = computed(() =>
|
||||||
isFulfillList.value ? 'warehouse' : undefined,
|
isFulfillList.value ? 'warehouse' : undefined,
|
||||||
@ -1043,6 +1058,7 @@
|
|||||||
filters,
|
filters,
|
||||||
sortField,
|
sortField,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
customerId: queryCustomerId.value,
|
||||||
});
|
});
|
||||||
if (requestSeq !== listRequestSeq) {
|
if (requestSeq !== listRequestSeq) {
|
||||||
return;
|
return;
|
||||||
@ -2127,10 +2143,10 @@
|
|||||||
class="list-empty-hint"
|
class="list-empty-hint"
|
||||||
>
|
>
|
||||||
<span>{{ t('order.myOrders.empty.hint') }}</span>
|
<span>{{ t('order.myOrders.empty.hint') }}</span>
|
||||||
<a-button class="app-btn-primary-soft" @click="router.push('/order/placeOrder')">
|
<a-button class="app-btn-primary-soft" @click="router.push(placeOrderPath)">
|
||||||
{{ t('order.myOrders.empty.goPlaceOrder') }}
|
{{ t('order.myOrders.empty.goPlaceOrder') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="router.push('/order/priceCheck')">
|
<a-button @click="router.push(priceCheckPath)">
|
||||||
{{ t('order.myOrders.empty.goPriceCheck') }}
|
{{ t('order.myOrders.empty.goPriceCheck') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -90,6 +90,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
const {
|
const {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
isAgent,
|
||||||
customerId: adminCustomerId,
|
customerId: adminCustomerId,
|
||||||
customerOptions,
|
customerOptions,
|
||||||
customerLoading,
|
customerLoading,
|
||||||
@ -97,6 +98,14 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
ensureMutationCustomerId,
|
ensureMutationCustomerId,
|
||||||
resolveSelectedCustomerId,
|
resolveSelectedCustomerId,
|
||||||
} = useWarehouseAdminCustomer();
|
} = useWarehouseAdminCustomer();
|
||||||
|
const customerSelectPlaceholder = computed(() =>
|
||||||
|
isAgent.value ? '请选择名下客户' : t('order.placeOrder.selectCustomer'),
|
||||||
|
);
|
||||||
|
const orderListPath = computed(() => {
|
||||||
|
if (rbacUserStore.isAgentPortal()) return '/agent/orders';
|
||||||
|
if (rbacUserStore.isCustomerPortal()) return '/order/myOrders';
|
||||||
|
return '/order/waybillManage';
|
||||||
|
});
|
||||||
const adminCustomerSelected = computed(
|
const adminCustomerSelected = computed(
|
||||||
() => !isAdmin.value || resolveSelectedCustomerId() != null,
|
() => !isAdmin.value || resolveSelectedCustomerId() != null,
|
||||||
);
|
);
|
||||||
@ -2699,6 +2708,15 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyQueryCustomerId() {
|
||||||
|
if (!isAdmin.value) return;
|
||||||
|
const raw = route.query.customerId;
|
||||||
|
const id = Number(Array.isArray(raw) ? raw[0] : raw);
|
||||||
|
if (Number.isFinite(id) && id > 0) {
|
||||||
|
adminCustomerId.value = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyPriceCheckDraft() {
|
function applyPriceCheckDraft() {
|
||||||
const fromPriceCheck = String(route.query.from || '') === 'priceCheck';
|
const fromPriceCheck = String(route.query.from || '') === 'priceCheck';
|
||||||
let draft: Record<string, any> | null = null;
|
let draft: Record<string, any> | null = null;
|
||||||
@ -2708,6 +2726,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
} catch {
|
} catch {
|
||||||
draft = null;
|
draft = null;
|
||||||
}
|
}
|
||||||
|
applyQueryCustomerId();
|
||||||
if (!fromPriceCheck && !draft) return;
|
if (!fromPriceCheck && !draft) return;
|
||||||
|
|
||||||
activeOrderMode.value = 'single';
|
activeOrderMode.value = 'single';
|
||||||
@ -2799,6 +2818,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
applyQueryCustomerId();
|
||||||
loadSingleFormReferenceOptions();
|
loadSingleFormReferenceOptions();
|
||||||
void loadCurrentCustomerCode();
|
void loadCurrentCustomerCode();
|
||||||
loadReceiverAiPrefs();
|
loadReceiverAiPrefs();
|
||||||
@ -3534,7 +3554,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
if (!info) return;
|
if (!info) return;
|
||||||
if (info.draftSaved && info.orderId) {
|
if (info.draftSaved && info.orderId) {
|
||||||
router.push({
|
router.push({
|
||||||
path: rbacUserStore.isCustomerPortal() ? '/order/myOrders' : '/order/waybillManage',
|
path: orderListPath.value,
|
||||||
query: { status: 'draft', orderId: String(info.orderId), edit: '1' },
|
query: { status: 'draft', orderId: String(info.orderId), edit: '1' },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -3565,7 +3585,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
cancelText: t('order.placeOrder.single.failSaveAsDraftStay'),
|
cancelText: t('order.placeOrder.single.failSaveAsDraftStay'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
router.push({
|
router.push({
|
||||||
path: rbacUserStore.isCustomerPortal() ? '/order/myOrders' : '/order/waybillManage',
|
path: orderListPath.value,
|
||||||
query: {
|
query: {
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
...(orderId ? { orderId: String(orderId), edit: '1' } : {}),
|
...(orderId ? { orderId: String(orderId), edit: '1' } : {}),
|
||||||
@ -3588,7 +3608,11 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
openAskAdvisor(router, {
|
openAskAdvisor(router, {
|
||||||
orderId: info?.orderId ? String(info.orderId) : undefined,
|
orderId: info?.orderId ? String(info.orderId) : undefined,
|
||||||
waybillNo: info?.serviceNo,
|
waybillNo: info?.serviceNo,
|
||||||
from: rbacUserStore.isCustomerPortal() ? 'myOrders' : 'waybillManage',
|
from: rbacUserStore.isAgentPortal()
|
||||||
|
? 'agentOrders'
|
||||||
|
: rbacUserStore.isCustomerPortal()
|
||||||
|
? 'myOrders'
|
||||||
|
: 'waybillManage',
|
||||||
orderStatus: info?.draftSaved ? 'draft' : undefined,
|
orderStatus: info?.draftSaved ? 'draft' : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -3608,7 +3632,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
const info = createFailInfo.value;
|
const info = createFailInfo.value;
|
||||||
closeCreateFailModal();
|
closeCreateFailModal();
|
||||||
router.push({
|
router.push({
|
||||||
path: rbacUserStore.isCustomerPortal() ? '/order/myOrders' : '/order/waybillManage',
|
path: orderListPath.value,
|
||||||
query: {
|
query: {
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
...(info?.orderId ? { orderId: String(info.orderId) } : {}),
|
...(info?.orderId ? { orderId: String(info.orderId) } : {}),
|
||||||
@ -3698,11 +3722,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
okText: t('order.placeOrder.single.successGoOrders'),
|
okText: t('order.placeOrder.single.successGoOrders'),
|
||||||
cancelText: t('order.placeOrder.single.successStay'),
|
cancelText: t('order.placeOrder.single.successStay'),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
router.push({
|
router.push({ path: orderListPath.value });
|
||||||
path: rbacUserStore.isCustomerPortal()
|
|
||||||
? '/order/myOrders'
|
|
||||||
: '/order/waybillManage',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -3913,7 +3933,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
allow-search
|
allow-search
|
||||||
allow-clear
|
allow-clear
|
||||||
size="small"
|
size="small"
|
||||||
:placeholder="t('order.placeOrder.selectCustomer')"
|
:placeholder="customerSelectPlaceholder"
|
||||||
:loading="customerLoading"
|
:loading="customerLoading"
|
||||||
:options="customerOptions"
|
:options="customerOptions"
|
||||||
class="place-order-customer-select"
|
class="place-order-customer-select"
|
||||||
@ -4767,7 +4787,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
allow-search
|
allow-search
|
||||||
allow-clear
|
allow-clear
|
||||||
size="small"
|
size="small"
|
||||||
:placeholder="t('order.placeOrder.selectCustomer')"
|
:placeholder="customerSelectPlaceholder"
|
||||||
:loading="customerLoading"
|
:loading="customerLoading"
|
||||||
:options="customerOptions"
|
:options="customerOptions"
|
||||||
class="place-order-customer-select"
|
class="place-order-customer-select"
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
import { getDictListAll } from '@/api/system/dict';
|
import { getDictListAll } from '@/api/system/dict';
|
||||||
import { getWarehouseList } from '@/api/system/warehouse';
|
import { getWarehouseList } from '@/api/system/warehouse';
|
||||||
import { useCurrentWarehouseStore } from '@/store';
|
import { useCurrentWarehouseStore } from '@/store';
|
||||||
|
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer';
|
||||||
import ChannelGuidePanel from './components/ChannelGuidePanel.vue';
|
import ChannelGuidePanel from './components/ChannelGuidePanel.vue';
|
||||||
import ShippingMethodTabSelect from './components/ShippingMethodTabSelect.vue';
|
import ShippingMethodTabSelect from './components/ShippingMethodTabSelect.vue';
|
||||||
import AiRecommendCard from './components/AiRecommendCard.vue';
|
import AiRecommendCard from './components/AiRecommendCard.vue';
|
||||||
@ -33,6 +34,13 @@
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentWarehouseStore = useCurrentWarehouseStore();
|
const currentWarehouseStore = useCurrentWarehouseStore();
|
||||||
|
const {
|
||||||
|
isAgent,
|
||||||
|
customerId: agentCustomerId,
|
||||||
|
customerOptions: agentCustomerOptions,
|
||||||
|
customerLoading: agentCustomerLoading,
|
||||||
|
resolveSelectedCustomerId,
|
||||||
|
} = useWarehouseAdminCustomer();
|
||||||
|
|
||||||
/** 首屏常用国精简,避免芯片墙;其余靠搜索 */
|
/** 首屏常用国精简,避免芯片墙;其余靠搜索 */
|
||||||
const countryShortcutCodes: string[] = ['US', 'GB', 'DE', 'CA', 'FR', 'ES'];
|
const countryShortcutCodes: string[] = ['US', 'GB', 'DE', 'CA', 'FR', 'ES'];
|
||||||
@ -574,6 +582,7 @@
|
|||||||
const length = parseOptionalDimension(formData.length);
|
const length = parseOptionalDimension(formData.length);
|
||||||
const width = parseOptionalDimension(formData.width);
|
const width = parseOptionalDimension(formData.width);
|
||||||
const height = parseOptionalDimension(formData.height);
|
const height = parseOptionalDimension(formData.height);
|
||||||
|
const selectedCustomerId = isAgent.value ? resolveSelectedCustomerId() : undefined;
|
||||||
const { items, failures, queryHint: hint, availableCoupons: coupons } = await postOrderPriceCheck({
|
const { items, failures, queryHint: hint, availableCoupons: coupons } = await postOrderPriceCheck({
|
||||||
destinationCountry: formData.destinationCountry,
|
destinationCountry: formData.destinationCountry,
|
||||||
shippingMethod: formData.shippingMethod || '',
|
shippingMethod: formData.shippingMethod || '',
|
||||||
@ -587,6 +596,7 @@
|
|||||||
destinationCity: formData.destinationCity?.trim() || '',
|
destinationCity: formData.destinationCity?.trim() || '',
|
||||||
postalCode: formData.postalCode?.trim() || '',
|
postalCode: formData.postalCode?.trim() || '',
|
||||||
couponInstanceId: couponInstanceId.value,
|
couponInstanceId: couponInstanceId.value,
|
||||||
|
...(selectedCustomerId != null ? { customerId: selectedCustomerId } : {}),
|
||||||
});
|
});
|
||||||
queryHint.value = hint;
|
queryHint.value = hint;
|
||||||
availableCoupons.value = coupons ?? [];
|
availableCoupons.value = coupons ?? [];
|
||||||
@ -671,6 +681,7 @@
|
|||||||
const length = parseOptionalDimension(formData.length);
|
const length = parseOptionalDimension(formData.length);
|
||||||
const width = parseOptionalDimension(formData.width);
|
const width = parseOptionalDimension(formData.width);
|
||||||
const height = parseOptionalDimension(formData.height);
|
const height = parseOptionalDimension(formData.height);
|
||||||
|
const selectedCustomerId = isAgent.value ? resolveSelectedCustomerId() : undefined;
|
||||||
const { items, failures } = await postOrderPriceCheck({
|
const { items, failures } = await postOrderPriceCheck({
|
||||||
destinationCountry: formData.destinationCountry,
|
destinationCountry: formData.destinationCountry,
|
||||||
shippingMethod: productCode,
|
shippingMethod: productCode,
|
||||||
@ -684,6 +695,7 @@
|
|||||||
destinationCity: formData.destinationCity?.trim() || '',
|
destinationCity: formData.destinationCity?.trim() || '',
|
||||||
postalCode: formData.postalCode?.trim() || '',
|
postalCode: formData.postalCode?.trim() || '',
|
||||||
couponInstanceId: couponInstanceId.value,
|
couponInstanceId: couponInstanceId.value,
|
||||||
|
...(selectedCustomerId != null ? { customerId: selectedCustomerId } : {}),
|
||||||
});
|
});
|
||||||
const nextRows = processQueryResults(items || []);
|
const nextRows = processQueryResults(items || []);
|
||||||
const replacement =
|
const replacement =
|
||||||
@ -779,14 +791,16 @@
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore quota
|
// ignore quota
|
||||||
}
|
}
|
||||||
|
const selectedCustomerId = isAgent.value ? resolveSelectedCustomerId() : undefined;
|
||||||
router
|
router
|
||||||
.push({
|
.push({
|
||||||
name: 'PlaceOrder',
|
name: isAgent.value ? 'AgentPortalPlaceOrder' : 'PlaceOrder',
|
||||||
query: {
|
query: {
|
||||||
from: 'priceCheck',
|
from: 'priceCheck',
|
||||||
mode: 'single',
|
mode: 'single',
|
||||||
productCode: draft.productCode || undefined,
|
productCode: draft.productCode || undefined,
|
||||||
countryCode: draft.countryCode || undefined,
|
countryCode: draft.countryCode || undefined,
|
||||||
|
...(selectedCustomerId != null ? { customerId: String(selectedCustomerId) } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
@ -970,6 +984,17 @@
|
|||||||
<icon-question-circle />
|
<icon-question-circle />
|
||||||
</button>
|
</button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<a-select
|
||||||
|
v-if="isAgent"
|
||||||
|
v-model="agentCustomerId"
|
||||||
|
allow-search
|
||||||
|
allow-clear
|
||||||
|
size="small"
|
||||||
|
placeholder="代客查价(可选)"
|
||||||
|
:loading="agentCustomerLoading"
|
||||||
|
:options="agentCustomerOptions"
|
||||||
|
class="price-check-agent-customer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row form-row-primary">
|
<div class="form-row form-row-primary">
|
||||||
@ -1366,9 +1391,15 @@
|
|||||||
.form-head {
|
.form-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.price-check-agent-customer {
|
||||||
|
margin-left: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
.form-head__title {
|
.form-head__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
|
|||||||
@ -7,7 +7,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-alert
|
<a-alert
|
||||||
v-if="referrerLoading"
|
v-if="agentLoading"
|
||||||
|
type="info"
|
||||||
|
:show-icon="true"
|
||||||
|
class="referrer-alert"
|
||||||
|
>
|
||||||
|
{{ t('marketing.register.agentValidating') }}
|
||||||
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-else-if="agentValidated === true && effectiveAgentCode"
|
||||||
|
type="success"
|
||||||
|
:show-icon="true"
|
||||||
|
class="referrer-alert"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t('marketing.register.agentHint', {
|
||||||
|
name: agentNameMasked || '渠道代理',
|
||||||
|
code: effectiveAgentCode,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<template v-if="agentLocked">
|
||||||
|
<div class="agent-lock-tip">{{ t('marketing.register.agentLockedHint') }}</div>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-else-if="agentValidated === false && formState.agentCodeInput.trim()"
|
||||||
|
type="warning"
|
||||||
|
:show-icon="true"
|
||||||
|
class="referrer-alert"
|
||||||
|
>
|
||||||
|
{{ t('marketing.register.agentInvalid') }}
|
||||||
|
</a-alert>
|
||||||
|
|
||||||
|
<a-alert
|
||||||
|
v-if="!effectiveAgentCode && referrerLoading"
|
||||||
type="info"
|
type="info"
|
||||||
:show-icon="true"
|
:show-icon="true"
|
||||||
class="referrer-alert"
|
class="referrer-alert"
|
||||||
@ -15,18 +48,20 @@
|
|||||||
{{ t('marketing.register.referrerLoading') }}
|
{{ t('marketing.register.referrerLoading') }}
|
||||||
</a-alert>
|
</a-alert>
|
||||||
<a-alert
|
<a-alert
|
||||||
v-else-if="referrerInfo"
|
v-else-if="!effectiveAgentCode && referrerInfo"
|
||||||
type="success"
|
type="success"
|
||||||
:show-icon="true"
|
:show-icon="true"
|
||||||
class="referrer-alert"
|
class="referrer-alert"
|
||||||
>
|
>
|
||||||
{{ t('marketing.register.referrerHint', {
|
{{
|
||||||
name: referrerInfo.referrerName ?? referrerInfo.ReferrerName ?? referrerInfo.customerName,
|
t('marketing.register.referrerHint', {
|
||||||
code: referrerInfo.inviteCode ?? referrerInfo.InviteCode ?? inviteCode,
|
name: referrerInfo.referrerName ?? referrerInfo.ReferrerName ?? referrerInfo.customerName,
|
||||||
}) }}
|
code: referrerInfo.inviteCode ?? referrerInfo.InviteCode ?? inviteCode,
|
||||||
|
})
|
||||||
|
}}
|
||||||
</a-alert>
|
</a-alert>
|
||||||
<a-alert
|
<a-alert
|
||||||
v-else-if="referrerTimedOut"
|
v-else-if="!effectiveAgentCode && referrerTimedOut"
|
||||||
type="warning"
|
type="warning"
|
||||||
:show-icon="true"
|
:show-icon="true"
|
||||||
class="referrer-alert"
|
class="referrer-alert"
|
||||||
@ -34,13 +69,21 @@
|
|||||||
{{ t('marketing.register.referrerTimeout') }}
|
{{ t('marketing.register.referrerTimeout') }}
|
||||||
</a-alert>
|
</a-alert>
|
||||||
<a-alert
|
<a-alert
|
||||||
v-else-if="inviteCode && referrerValidated === false"
|
v-else-if="!effectiveAgentCode && inviteCode && referrerValidated === false"
|
||||||
type="warning"
|
type="warning"
|
||||||
:show-icon="true"
|
:show-icon="true"
|
||||||
class="referrer-alert"
|
class="referrer-alert"
|
||||||
>
|
>
|
||||||
{{ t('marketing.register.invalidRef') }}
|
{{ t('marketing.register.invalidRef') }}
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-if="effectiveAgentCode && inviteCode"
|
||||||
|
type="info"
|
||||||
|
:show-icon="true"
|
||||||
|
class="referrer-alert"
|
||||||
|
>
|
||||||
|
{{ t('marketing.register.agentPriorityHint') }}
|
||||||
|
</a-alert>
|
||||||
|
|
||||||
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
|
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
|
||||||
<a-form-item field="customerName" :label="t('marketing.register.customerName')">
|
<a-form-item field="customerName" :label="t('marketing.register.customerName')">
|
||||||
@ -58,6 +101,16 @@
|
|||||||
<a-form-item field="confirmPassword" :label="t('marketing.register.confirmPassword')">
|
<a-form-item field="confirmPassword" :label="t('marketing.register.confirmPassword')">
|
||||||
<a-input-password v-model="formState.confirmPassword" allow-clear />
|
<a-input-password v-model="formState.confirmPassword" allow-clear />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item field="agentCodeInput" :label="t('marketing.register.agentField')">
|
||||||
|
<a-input
|
||||||
|
v-model="formState.agentCodeInput"
|
||||||
|
allow-clear
|
||||||
|
:disabled="agentLocked"
|
||||||
|
:placeholder="t('marketing.register.agentPlaceholder')"
|
||||||
|
@blur="onAgentInputBlur"
|
||||||
|
@clear="onAgentInputClear"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-button type="primary" long :loading="submitting" @click="handleSubmit">
|
<a-button type="primary" long :loading="submitting" @click="handleSubmit">
|
||||||
{{ submitting ? t('marketing.register.submitting') : t('marketing.register.submit') }}
|
{{ submitting ? t('marketing.register.submitting') : t('marketing.register.submit') }}
|
||||||
@ -73,19 +126,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import type { FieldRule, FormInstance } from '@arco-design/web-vue'
|
import type { FieldRule, FormInstance } from '@arco-design/web-vue'
|
||||||
import { CustomerService } from '@/api/customer'
|
import { CustomerService } from '@/api/customer'
|
||||||
import { validateInviteCode } from '@/api/marketing/portal'
|
import { validateInviteCode } from '@/api/marketing/portal'
|
||||||
|
import { validateAgentCode } from '@/api/agent'
|
||||||
import { isLetterAndDigitPassword } from '@/utils/customerPassword'
|
import { isLetterAndDigitPassword } from '@/utils/customerPassword'
|
||||||
import {
|
import {
|
||||||
clearStashedAdvisorInvite,
|
clearStashedAdvisorInvite,
|
||||||
resolveAdvisorInvite,
|
resolveAdvisorInvite,
|
||||||
stashAdvisorInvite,
|
stashAdvisorInvite,
|
||||||
} from '@/utils/advisorInviteStorage'
|
} from '@/utils/advisorInviteStorage'
|
||||||
|
import {
|
||||||
|
clearStashedAgentInvite,
|
||||||
|
resolveAgentInvite,
|
||||||
|
stashAgentInvite,
|
||||||
|
} from '@/utils/agentInviteStorage'
|
||||||
import { stashInviteRef } from '@/utils/inviteRefStorage'
|
import { stashInviteRef } from '@/utils/inviteRefStorage'
|
||||||
import { isTimeoutError, withTimeout } from '@/utils/withTimeout'
|
import { isTimeoutError, withTimeout } from '@/utils/withTimeout'
|
||||||
import { preferH5Portal } from '@/utils/device'
|
import { preferH5Portal } from '@/utils/device'
|
||||||
@ -94,6 +153,7 @@ import { isAdminH5Path } from '@/utils/login-portal'
|
|||||||
defineOptions({ name: 'CustomerRegister' })
|
defineOptions({ name: 'CustomerRegister' })
|
||||||
|
|
||||||
const REFERRER_TIMEOUT_MS = 15000
|
const REFERRER_TIMEOUT_MS = 15000
|
||||||
|
const AGENT_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -119,11 +179,15 @@ function resolvePostRegisterLogin() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const inviteCode = ref('')
|
const inviteCode = ref('')
|
||||||
const advisorCode = ref('')
|
const advisorCode = ref('')
|
||||||
const agentCode = ref('')
|
const agentLocked = ref(false)
|
||||||
|
const agentValidated = ref<boolean | null>(null)
|
||||||
|
const agentNameMasked = ref('')
|
||||||
|
const agentLoading = ref(false)
|
||||||
const referrerInfo = ref<Record<string, unknown> | null>(null)
|
const referrerInfo = ref<Record<string, unknown> | null>(null)
|
||||||
const referrerValidated = ref<boolean | null>(null)
|
const referrerValidated = ref<boolean | null>(null)
|
||||||
const referrerLoading = ref(false)
|
const referrerLoading = ref(false)
|
||||||
@ -135,8 +199,13 @@ const formState = reactive({
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
|
agentCodeInput: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const effectiveAgentCode = computed(() =>
|
||||||
|
agentValidated.value === true ? formState.agentCodeInput.trim() : '',
|
||||||
|
)
|
||||||
|
|
||||||
const formRules: Record<string, FieldRule[]> = {
|
const formRules: Record<string, FieldRule[]> = {
|
||||||
customerName: [{ required: true, message: t('marketing.register.customerNameRequired') }],
|
customerName: [{ required: true, message: t('marketing.register.customerNameRequired') }],
|
||||||
phone: [{ required: true, message: t('marketing.register.phoneRequired') }],
|
phone: [{ required: true, message: t('marketing.register.phoneRequired') }],
|
||||||
@ -164,6 +233,56 @@ const formRules: Record<string, FieldRule[]> = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateAgent(code: string, locked: boolean) {
|
||||||
|
const trimmed = code.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
agentValidated.value = null
|
||||||
|
agentNameMasked.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agentLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await withTimeout(validateAgentCode(trimmed), AGENT_TIMEOUT_MS)
|
||||||
|
if (!data.valid) {
|
||||||
|
agentValidated.value = false
|
||||||
|
agentNameMasked.value = ''
|
||||||
|
if (locked) {
|
||||||
|
Message.warning(data.message || t('marketing.register.agentInvalid'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formState.agentCodeInput = data.agentCode || trimmed
|
||||||
|
agentNameMasked.value = data.agentNameMasked || ''
|
||||||
|
agentValidated.value = true
|
||||||
|
agentLocked.value = locked
|
||||||
|
stashAgentInvite(formState.agentCodeInput, locked)
|
||||||
|
} catch {
|
||||||
|
agentValidated.value = false
|
||||||
|
agentNameMasked.value = ''
|
||||||
|
} finally {
|
||||||
|
agentLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAgentInputBlur() {
|
||||||
|
if (agentLocked.value) return
|
||||||
|
const code = formState.agentCodeInput.trim()
|
||||||
|
if (!code) {
|
||||||
|
agentValidated.value = null
|
||||||
|
agentNameMasked.value = ''
|
||||||
|
clearStashedAgentInvite()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void validateAgent(code, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAgentInputClear() {
|
||||||
|
if (agentLocked.value) return
|
||||||
|
agentValidated.value = null
|
||||||
|
agentNameMasked.value = ''
|
||||||
|
clearStashedAgentInvite()
|
||||||
|
}
|
||||||
|
|
||||||
async function loadReferrer(code: string) {
|
async function loadReferrer(code: string) {
|
||||||
inviteCode.value = code
|
inviteCode.value = code
|
||||||
stashInviteRef(code)
|
stashInviteRef(code)
|
||||||
@ -209,6 +328,16 @@ async function handleSubmit() {
|
|||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typedAgent = formState.agentCodeInput.trim()
|
||||||
|
if (typedAgent && agentValidated.value !== true) {
|
||||||
|
await validateAgent(typedAgent, agentLocked.value)
|
||||||
|
if (agentValidated.value !== true) {
|
||||||
|
Message.warning(t('marketing.register.agentInvalid'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await CustomerService.create({
|
await CustomerService.create({
|
||||||
@ -216,8 +345,8 @@ async function handleSubmit() {
|
|||||||
phone: formState.phone.trim(),
|
phone: formState.phone.trim(),
|
||||||
email: formState.email.trim(),
|
email: formState.email.trim(),
|
||||||
password: formState.password,
|
password: formState.password,
|
||||||
...(agentCode.value
|
...(effectiveAgentCode.value
|
||||||
? ({ agentCode: agentCode.value } as Record<string, string>)
|
? ({ agentCode: effectiveAgentCode.value } as Record<string, string>)
|
||||||
: inviteCode.value && referrerValidated.value
|
: inviteCode.value && referrerValidated.value
|
||||||
? ({ inviteCode: inviteCode.value } as Record<string, string>)
|
? ({ inviteCode: inviteCode.value } as Record<string, string>)
|
||||||
: {}),
|
: {}),
|
||||||
@ -226,6 +355,7 @@ async function handleSubmit() {
|
|||||||
const { clearStashedInviteRef } = await import('@/utils/inviteRefStorage')
|
const { clearStashedInviteRef } = await import('@/utils/inviteRefStorage')
|
||||||
clearStashedInviteRef()
|
clearStashedInviteRef()
|
||||||
clearStashedAdvisorInvite()
|
clearStashedAdvisorInvite()
|
||||||
|
clearStashedAgentInvite()
|
||||||
Message.success(t('marketing.register.success'))
|
Message.success(t('marketing.register.success'))
|
||||||
router.replace(resolvePostRegisterLogin())
|
router.replace(resolvePostRegisterLogin())
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -240,13 +370,17 @@ function goLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const agent = String(route.query.agent ?? route.query.agentCode ?? '').trim()
|
const agent = resolveAgentInvite(route.query as Record<string, unknown>)
|
||||||
if (agent) {
|
if (agent.code) {
|
||||||
agentCode.value = agent
|
formState.agentCodeInput = agent.code
|
||||||
|
agentLocked.value = agent.locked
|
||||||
|
void validateAgent(agent.code, agent.locked)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ref = String(route.query.ref ?? route.query.invite ?? '').trim()
|
const ref = String(route.query.ref ?? route.query.invite ?? '').trim()
|
||||||
// 有代理码时不校验 C2C 邀请码(与后端互斥一致)
|
// 有代理码时不校验 C2C(与后端互斥一致);手填校验成功后也会优先 agent
|
||||||
if (ref && !agent) void loadReferrer(ref)
|
if (ref && !agent.code) void loadReferrer(ref)
|
||||||
|
|
||||||
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>)
|
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>)
|
||||||
if (advisor) {
|
if (advisor) {
|
||||||
advisorCode.value = advisor
|
advisorCode.value = advisor
|
||||||
@ -294,6 +428,12 @@ onMounted(() => {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-lock-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
.register-footer {
|
.register-footer {
|
||||||
margin: 20px 0 0;
|
margin: 20px 0 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -13,15 +13,15 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="filterState.salesman"
|
v-model="filterState.agentId"
|
||||||
:placeholder="t('user.customer.fields.salesPerson')"
|
:placeholder="t('user.customer.fields.agent')"
|
||||||
allow-clear
|
allow-clear
|
||||||
allow-search
|
allow-search
|
||||||
style="width: 160px"
|
style="width: 180px"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="option in userOptions"
|
v-for="option in agentOptions"
|
||||||
:key="`sales-${option.value}`"
|
:key="`agent-filter-${option.value}`"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
@ -349,17 +349,18 @@
|
|||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="salesPerson">
|
<a-form-item field="agentId">
|
||||||
<template #label>{{ t('user.customer.fields.salesPerson') }}</template>
|
<template #label>{{ t('user.customer.fields.agent') }}</template>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="formState.salesPerson"
|
v-model="formState.agentId"
|
||||||
:placeholder="t('user.customer.placeholder.select')"
|
:placeholder="t('user.customer.placeholder.select')"
|
||||||
allow-clear
|
allow-clear
|
||||||
allow-search
|
allow-search
|
||||||
|
:disabled="isEdit && formState.parentCustomerId != null"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
v-for="option in userOptions"
|
v-for="option in agentOptions"
|
||||||
:key="option.value"
|
:key="`agent-form-${option.value}`"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
@ -436,6 +437,7 @@ import PaginationPro from '@/components/PaginationPro/index.vue'
|
|||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import type { CustomerDto } from '@/api/customer'
|
import type { CustomerDto } from '@/api/customer'
|
||||||
import { CustomerService } from '@/api/customer'
|
import { CustomerService } from '@/api/customer'
|
||||||
|
import { AgentAdminApi, type AgentDto } from '@/api/agent'
|
||||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||||
import { RoleService } from '@/api/rbac/generated/services/RoleService'
|
import { RoleService } from '@/api/rbac/generated/services/RoleService'
|
||||||
import { UserService } from '@/api/rbac/generated/services/UserService'
|
import { UserService } from '@/api/rbac/generated/services/UserService'
|
||||||
@ -472,7 +474,7 @@ const pageSize = ref(50)
|
|||||||
|
|
||||||
const filterState = reactive({
|
const filterState = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
salesman: '' as string | undefined,
|
agentId: undefined as number | undefined,
|
||||||
customerService: '' as string | undefined,
|
customerService: '' as string | undefined,
|
||||||
})
|
})
|
||||||
const headerSearchParams = reactive<Record<string, string>>({})
|
const headerSearchParams = reactive<Record<string, string>>({})
|
||||||
@ -485,7 +487,7 @@ const CUSTOMER_SERVER_FILTER_KEYS = [
|
|||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
'wechatNickname',
|
'wechatNickname',
|
||||||
'salesPerson',
|
'agent',
|
||||||
'customerService',
|
'customerService',
|
||||||
'status',
|
'status',
|
||||||
] as const
|
] as const
|
||||||
@ -501,11 +503,15 @@ const CUSTOMER_SORTABLE_KEYS = [
|
|||||||
const processedTableData = computed(() => tableData.value)
|
const processedTableData = computed(() => tableData.value)
|
||||||
|
|
||||||
type SelectOption = { label: string; value: string | number }
|
type SelectOption = { label: string; value: string | number }
|
||||||
|
type AgentSelectOption = { label: string; value: number; agentCode: string }
|
||||||
const userOptions = ref<SelectOption[]>([])
|
const userOptions = ref<SelectOption[]>([])
|
||||||
|
const agentOptions = ref<AgentSelectOption[]>([])
|
||||||
|
const agentList = ref<AgentDto[]>([])
|
||||||
const memberLevelOptions = ref<SelectOption[]>([])
|
const memberLevelOptions = ref<SelectOption[]>([])
|
||||||
const userList = ref<any[]>([])
|
const userList = ref<any[]>([])
|
||||||
|
|
||||||
const CUSTOMER_ROLE_CODE = 'Customer'
|
const CUSTOMER_ROLE_CODE = 'Customer'
|
||||||
|
const AGENT_ROLE_CODE = 'Agent'
|
||||||
|
|
||||||
function getUserRoleCodes(u: any): string[] {
|
function getUserRoleCodes(u: any): string[] {
|
||||||
const roles = u?.roles ?? u?.Roles ?? []
|
const roles = u?.roles ?? u?.Roles ?? []
|
||||||
@ -515,14 +521,15 @@ function getUserRoleCodes(u: any): string[] {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 客户门户账号(Customer / CustomerSub_* 角色)不可作为销售或顾问 */
|
/** 客户门户 / 渠道代理账号不可作为所属顾问 */
|
||||||
function isCustomerPortalUser(u: any, excludedUserIds?: Set<string>): boolean {
|
function isExcludedAdvisorUser(u: any, excludedUserIds?: Set<string>): boolean {
|
||||||
const id = String(u?.id ?? u?.Id ?? '')
|
const id = String(u?.id ?? u?.Id ?? '')
|
||||||
if (id && excludedUserIds?.has(id)) return true
|
if (id && excludedUserIds?.has(id)) return true
|
||||||
return getUserRoleCodes(u).some(
|
return getUserRoleCodes(u).some(
|
||||||
(code) =>
|
(code) =>
|
||||||
code.localeCompare(CUSTOMER_ROLE_CODE, undefined, { sensitivity: 'accent' }) === 0 ||
|
code.localeCompare(CUSTOMER_ROLE_CODE, undefined, { sensitivity: 'accent' }) === 0 ||
|
||||||
code.startsWith('CustomerSub_')
|
code.localeCompare(AGENT_ROLE_CODE, undefined, { sensitivity: 'accent' }) === 0 ||
|
||||||
|
code.startsWith('CustomerSub_'),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,6 +619,59 @@ async function loadMemberLevelOptions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 启用状态:后端 EnableStatus.Enabled = 2 */
|
||||||
|
const AGENT_STATUS_ENABLED = 2
|
||||||
|
|
||||||
|
async function loadAgentOptions() {
|
||||||
|
try {
|
||||||
|
const pageSize = 100
|
||||||
|
const agents: AgentDto[] = []
|
||||||
|
let page = 1
|
||||||
|
while (true) {
|
||||||
|
const { items } = await AgentAdminApi.list({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
status: AGENT_STATUS_ENABLED,
|
||||||
|
})
|
||||||
|
const batch = items || []
|
||||||
|
agents.push(...batch)
|
||||||
|
if (batch.length < pageSize) break
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
agentList.value = agents
|
||||||
|
agentOptions.value = agents
|
||||||
|
.filter((a) => a?.id && a.agentCode)
|
||||||
|
.map((a) => {
|
||||||
|
const name = String(a.agentName || '').trim()
|
||||||
|
const code = String(a.agentCode || '').trim()
|
||||||
|
return {
|
||||||
|
value: Number(a.id),
|
||||||
|
agentCode: code,
|
||||||
|
label: name && name !== code ? `${name}(${code})` : code,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || '加载代理下拉失败')
|
||||||
|
agentList.value = []
|
||||||
|
agentOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAgentCodeForForm(agentId: number | null | undefined): string {
|
||||||
|
if (agentId == null || agentId <= 0) return ''
|
||||||
|
const hit = agentOptions.value.find((o) => o.value === Number(agentId))
|
||||||
|
if (hit?.agentCode) return hit.agentCode
|
||||||
|
const fromList = agentList.value.find((a) => Number(a.id) === Number(agentId))
|
||||||
|
return String(fromList?.agentCode || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAgentDisplay(row: CustomerRow | any): string {
|
||||||
|
const name = String(row?.agentName ?? '').trim()
|
||||||
|
const code = String(row?.agentCode ?? '').trim()
|
||||||
|
if (name && code && name !== code) return `${name}(${code})`
|
||||||
|
return name || code || '--'
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFormSelectOptions() {
|
async function loadFormSelectOptions() {
|
||||||
try {
|
try {
|
||||||
const pageSize = 100
|
const pageSize = 100
|
||||||
@ -643,27 +703,29 @@ async function loadFormSelectOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const excludedUserIds = new Set<string>()
|
const excludedUserIds = new Set<string>()
|
||||||
const customerRole = roles.find(
|
const excludeRoleCodes = [CUSTOMER_ROLE_CODE, AGENT_ROLE_CODE]
|
||||||
(r: any) =>
|
for (const roleCode of excludeRoleCodes) {
|
||||||
String(r?.code ?? r?.Code ?? '').localeCompare(CUSTOMER_ROLE_CODE, undefined, {
|
const role = roles.find(
|
||||||
sensitivity: 'accent',
|
(r: any) =>
|
||||||
}) === 0
|
String(r?.code ?? r?.Code ?? '').localeCompare(roleCode, undefined, {
|
||||||
)
|
sensitivity: 'accent',
|
||||||
const customerRoleId = customerRole?.id ?? customerRole?.Id
|
}) === 0,
|
||||||
if (customerRoleId) {
|
)
|
||||||
|
const roleId = role?.id ?? role?.Id
|
||||||
|
if (!roleId) continue
|
||||||
try {
|
try {
|
||||||
const customerUsers = await UserService.getRbacUserGetUsersByRole(String(customerRoleId))
|
const roleUsers = await UserService.getRbacUserGetUsersByRole(String(roleId))
|
||||||
for (const u of normalizeListPayload(customerUsers)) {
|
for (const u of normalizeListPayload(roleUsers)) {
|
||||||
const id = String(u?.id ?? u?.Id ?? '')
|
const id = String(u?.id ?? u?.Id ?? '')
|
||||||
if (id) excludedUserIds.add(id)
|
if (id) excludedUserIds.add(id)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 拉取 Customer 角色用户失败时,仍依赖用户列表上的 roles 字段过滤
|
// 按角色拉用户失败时,仍依赖用户列表上的 roles 字段过滤
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userList.value = users
|
userList.value = users
|
||||||
const staffUsers = users.filter((u) => !isCustomerPortalUser(u, excludedUserIds))
|
const staffUsers = users.filter((u) => !isExcludedAdvisorUser(u, excludedUserIds))
|
||||||
userOptions.value = staffUsers
|
userOptions.value = staffUsers
|
||||||
.map(mapUserToSelectOption)
|
.map(mapUserToSelectOption)
|
||||||
.filter((item): item is SelectOption => item != null)
|
.filter((item): item is SelectOption => item != null)
|
||||||
@ -681,11 +743,14 @@ function resolveCustomerId(record: CustomerRow | any): string | number | undefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapCustomerRow(r: any): CustomerRow {
|
function mapCustomerRow(r: any): CustomerRow {
|
||||||
|
const agentIdRaw = r.agentId ?? r.AgentId
|
||||||
return {
|
return {
|
||||||
...r,
|
...r,
|
||||||
id: r.id ?? r.Id,
|
id: r.id ?? r.Id,
|
||||||
accountBalance: r.accountBalance ?? r.balance ?? r.Balance,
|
accountBalance: r.accountBalance ?? r.balance ?? r.Balance,
|
||||||
salesPerson: r.salesPerson ?? r.salesman ?? r.Salesman,
|
agentId: agentIdRaw != null && agentIdRaw !== '' ? Number(agentIdRaw) : null,
|
||||||
|
agentCode: r.agentCode ?? r.AgentCode ?? '',
|
||||||
|
agentName: r.agentName ?? r.AgentName ?? '',
|
||||||
memberLevelId: r.memberLevelId ?? r.MemberLevelId,
|
memberLevelId: r.memberLevelId ?? r.MemberLevelId,
|
||||||
memberLevelName: r.memberLevelName ?? r.MemberLevelName,
|
memberLevelName: r.memberLevelName ?? r.MemberLevelName,
|
||||||
priceTierKey: r.priceTierKey ?? r.PriceTierKey ?? '',
|
priceTierKey: r.priceTierKey ?? r.PriceTierKey ?? '',
|
||||||
@ -701,7 +766,7 @@ async function loadList() {
|
|||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
keyword: keyword || undefined,
|
keyword: keyword || undefined,
|
||||||
salesman: adapted.salesman || filterState.salesman || undefined,
|
agentId: adapted.agentId || filterState.agentId || undefined,
|
||||||
customerService: adapted.customerService || filterState.customerService || undefined,
|
customerService: adapted.customerService || filterState.customerService || undefined,
|
||||||
status: adapted.status as any,
|
status: adapted.status as any,
|
||||||
sortField: listSortField.value || undefined,
|
sortField: listSortField.value || undefined,
|
||||||
@ -753,7 +818,7 @@ const hotColumns = computed(() =>
|
|||||||
{ key: 'memberLevelName', label: t('user.customer.fields.memberLevel'), width: 100 },
|
{ key: 'memberLevelName', label: t('user.customer.fields.memberLevel'), width: 100 },
|
||||||
{ key: 'priceTierKey', label: t('user.customer.fields.priceTierKey'), width: 100 },
|
{ key: 'priceTierKey', label: t('user.customer.fields.priceTierKey'), width: 100 },
|
||||||
{ key: 'remark', label: t('user.customer.fields.remark'), width: 100 },
|
{ key: 'remark', label: t('user.customer.fields.remark'), width: 100 },
|
||||||
{ key: 'salesPerson', label: t('user.customer.fields.salesPerson'), width: 90 },
|
{ key: 'agent', label: t('user.customer.fields.agent'), width: 120 },
|
||||||
{ key: 'customerService', label: t('user.customer.fields.customerService'), width: 90 },
|
{ key: 'customerService', label: t('user.customer.fields.customerService'), width: 90 },
|
||||||
{ key: 'createTime', label: t('user.customer.fields.createTime'), width: 150 },
|
{ key: 'createTime', label: t('user.customer.fields.createTime'), width: 150 },
|
||||||
{ key: 'createBy', label: t('user.customer.fields.createBy'), width: 90 },
|
{ key: 'createBy', label: t('user.customer.fields.createBy'), width: 90 },
|
||||||
@ -816,8 +881,8 @@ function customCellRenderer(row: number, col: number, value: any, rowData: any):
|
|||||||
if (val === undefined || val === null) return '--'
|
if (val === undefined || val === null) return '--'
|
||||||
return Number(val).toLocaleString()
|
return Number(val).toLocaleString()
|
||||||
}
|
}
|
||||||
if (colKey === 'salesPerson') {
|
if (colKey === 'agent') {
|
||||||
return formatStaffDisplay(rowData?.salesPerson ?? rowData?.salesman)
|
return formatAgentDisplay(rowData)
|
||||||
}
|
}
|
||||||
if (colKey === 'customerService') {
|
if (colKey === 'customerService') {
|
||||||
return formatStaffDisplay(rowData?.customerService)
|
return formatStaffDisplay(rowData?.customerService)
|
||||||
@ -944,8 +1009,9 @@ const formState = reactive<Partial<CustomerRow>>({
|
|||||||
memberLevelId: undefined,
|
memberLevelId: undefined,
|
||||||
priceTierKey: '普通会员',
|
priceTierKey: '普通会员',
|
||||||
remark: '',
|
remark: '',
|
||||||
salesPerson: '',
|
agentId: undefined as number | null | undefined,
|
||||||
customerService: '',
|
customerService: '',
|
||||||
|
parentCustomerId: null as number | null | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const priceTierOptions = [
|
const priceTierOptions = [
|
||||||
@ -994,8 +1060,9 @@ function handleAdd() {
|
|||||||
memberLevelId: undefined,
|
memberLevelId: undefined,
|
||||||
priceTierKey: '普通会员',
|
priceTierKey: '普通会员',
|
||||||
remark: '',
|
remark: '',
|
||||||
salesPerson: '',
|
agentId: undefined,
|
||||||
customerService: '',
|
customerService: '',
|
||||||
|
parentCustomerId: null,
|
||||||
})
|
})
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
@ -1003,6 +1070,20 @@ function handleAdd() {
|
|||||||
function handleEdit(record: CustomerRow) {
|
function handleEdit(record: CustomerRow) {
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = resolveCustomerId(record)
|
currentId.value = resolveCustomerId(record)
|
||||||
|
const agentId =
|
||||||
|
record.agentId != null && Number(record.agentId) > 0 ? Number(record.agentId) : undefined
|
||||||
|
// 已停用代理仍展示当前绑定,避免下拉空白
|
||||||
|
if (agentId && !agentOptions.value.some((o) => o.value === agentId)) {
|
||||||
|
const code = String(record.agentCode || '').trim()
|
||||||
|
agentOptions.value = [
|
||||||
|
...agentOptions.value,
|
||||||
|
{
|
||||||
|
value: agentId,
|
||||||
|
agentCode: code,
|
||||||
|
label: formatAgentDisplay(record),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
Object.assign(formState, {
|
Object.assign(formState, {
|
||||||
customerCode: record.customerCode,
|
customerCode: record.customerCode,
|
||||||
customerName: record.customerName,
|
customerName: record.customerName,
|
||||||
@ -1017,8 +1098,9 @@ function handleEdit(record: CustomerRow) {
|
|||||||
memberLevelId: record.memberLevelId ?? undefined,
|
memberLevelId: record.memberLevelId ?? undefined,
|
||||||
priceTierKey: record.priceTierKey || '普通客户',
|
priceTierKey: record.priceTierKey || '普通客户',
|
||||||
remark: record.remark,
|
remark: record.remark,
|
||||||
salesPerson: resolveStaffAccountForForm(record.salesPerson ?? record.salesman),
|
agentId,
|
||||||
customerService: resolveStaffAccountForForm(record.customerService),
|
customerService: resolveStaffAccountForForm(record.customerService),
|
||||||
|
parentCustomerId: record.parentCustomerId ?? null,
|
||||||
})
|
})
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
@ -1113,14 +1195,18 @@ async function handleModalOk(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
modalLoading.value = true
|
modalLoading.value = true
|
||||||
try {
|
try {
|
||||||
const salesPersonAccount = resolveStaffAccountForForm(formState.salesPerson)
|
|
||||||
const customerServiceAccount = resolveStaffAccountForForm(formState.customerService)
|
const customerServiceAccount = resolveStaffAccountForForm(formState.customerService)
|
||||||
|
const agentCode = resolveAgentCodeForForm(formState.agentId as number | null | undefined)
|
||||||
|
// 主客户才改绑;子客户禁用下拉,不传 agentCode 以免误解绑
|
||||||
|
const canBindAgent = !(isEdit.value && formState.parentCustomerId != null)
|
||||||
|
|
||||||
if (isEdit.value && currentId.value != null && currentId.value !== '') {
|
if (isEdit.value && currentId.value != null && currentId.value !== '') {
|
||||||
await CustomerService.update(currentId.value, {
|
await CustomerService.update(currentId.value, {
|
||||||
...formState,
|
...formState,
|
||||||
salesPerson: salesPersonAccount,
|
salesPerson: undefined,
|
||||||
|
salesman: undefined,
|
||||||
customerService: customerServiceAccount,
|
customerService: customerServiceAccount,
|
||||||
|
agentCode: canBindAgent ? agentCode : undefined,
|
||||||
})
|
})
|
||||||
Message.success(t('user.customer.message.updateSuccess'))
|
Message.success(t('user.customer.message.updateSuccess'))
|
||||||
} else {
|
} else {
|
||||||
@ -1128,8 +1214,10 @@ async function handleModalOk(): Promise<boolean> {
|
|||||||
...formState,
|
...formState,
|
||||||
customerCode: undefined,
|
customerCode: undefined,
|
||||||
password: formState.password,
|
password: formState.password,
|
||||||
salesPerson: salesPersonAccount,
|
salesPerson: undefined,
|
||||||
|
salesman: undefined,
|
||||||
customerService: customerServiceAccount,
|
customerService: customerServiceAccount,
|
||||||
|
agentCode: agentCode || undefined,
|
||||||
})
|
})
|
||||||
Message.success(t('user.customer.message.createSuccess'))
|
Message.success(t('user.customer.message.createSuccess'))
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
@ -1153,6 +1241,7 @@ const handsontableRef = ref<InstanceType<typeof HandsontablePro>>()
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadList()
|
loadList()
|
||||||
loadFormSelectOptions()
|
loadFormSelectOptions()
|
||||||
|
loadAgentOptions()
|
||||||
loadMemberLevelOptions()
|
loadMemberLevelOptions()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -144,8 +144,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">{{ t('user.customer.fields.salesPerson') }}</span>
|
<span class="info-label">{{ t('user.customer.fields.agent') }}</span>
|
||||||
<span class="info-value">{{ formatPerson(profile.salesPerson ?? profile.salesman) }}</span>
|
<span class="info-value">{{ formatAgent(profile) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">{{ t('user.customer.fields.customerService') }}</span>
|
<span class="info-label">{{ t('user.customer.fields.customerService') }}</span>
|
||||||
@ -411,6 +411,13 @@ function formatPerson(val: string | number | undefined): string {
|
|||||||
return String(val)
|
return String(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAgent(row: CustomerDto): string {
|
||||||
|
const name = String(row.agentName ?? '').trim()
|
||||||
|
const code = String(row.agentCode ?? '').trim()
|
||||||
|
if (name && code && name !== code) return `${name}(${code})`
|
||||||
|
return name || code || '--'
|
||||||
|
}
|
||||||
|
|
||||||
async function copyText(text?: string | number | null) {
|
async function copyText(text?: string | number | null) {
|
||||||
if (!text) return
|
if (!text) return
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -91,7 +91,28 @@
|
|||||||
|
|
||||||
<section class="wechat-h5-card">
|
<section class="wechat-h5-card">
|
||||||
<a-alert
|
<a-alert
|
||||||
v-if="referrerInfo"
|
v-if="agentLoading"
|
||||||
|
type="info"
|
||||||
|
:show-icon="true"
|
||||||
|
class="wechat-h5-referrer"
|
||||||
|
>
|
||||||
|
{{ t('marketing.register.agentValidating') }}
|
||||||
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-else-if="effectiveAgentCode"
|
||||||
|
type="success"
|
||||||
|
:show-icon="true"
|
||||||
|
class="wechat-h5-referrer"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t('marketing.register.agentHint', {
|
||||||
|
name: agentNameMasked || '渠道代理',
|
||||||
|
code: effectiveAgentCode,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-else-if="referrerInfo"
|
||||||
type="success"
|
type="success"
|
||||||
:show-icon="true"
|
:show-icon="true"
|
||||||
class="wechat-h5-referrer"
|
class="wechat-h5-referrer"
|
||||||
@ -111,6 +132,14 @@
|
|||||||
>
|
>
|
||||||
{{ t('wechat.register.invalidRef') }}
|
{{ t('wechat.register.invalidRef') }}
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
<a-alert
|
||||||
|
v-if="agentValidated === false && formState.agentCodeInput.trim()"
|
||||||
|
type="warning"
|
||||||
|
:show-icon="true"
|
||||||
|
class="wechat-h5-referrer"
|
||||||
|
>
|
||||||
|
{{ t('marketing.register.agentInvalid') }}
|
||||||
|
</a-alert>
|
||||||
|
|
||||||
<a-form
|
<a-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@ -140,6 +169,17 @@
|
|||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item field="agentCodeInput" :label="t('marketing.register.agentField')">
|
||||||
|
<a-input
|
||||||
|
v-model="formState.agentCodeInput"
|
||||||
|
size="large"
|
||||||
|
:disabled="agentLocked"
|
||||||
|
:placeholder="t('marketing.register.agentPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
@blur="onAgentInputBlur"
|
||||||
|
@clear="onAgentInputClear"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
<p class="wechat-h5-pwd-tip">
|
<p class="wechat-h5-pwd-tip">
|
||||||
{{ t('wechat.register.passwordTipPrefix') }}
|
{{ t('wechat.register.passwordTipPrefix') }}
|
||||||
<strong>{{ DEFAULT_CUSTOMER_PASSWORD }}</strong>
|
<strong>{{ DEFAULT_CUSTOMER_PASSWORD }}</strong>
|
||||||
@ -200,6 +240,7 @@ import {
|
|||||||
type WeChatOAuthUserInfo,
|
type WeChatOAuthUserInfo,
|
||||||
} from '@/api/wechat';
|
} from '@/api/wechat';
|
||||||
import { validateInviteCode } from '@/api/marketing/portal';
|
import { validateInviteCode } from '@/api/marketing/portal';
|
||||||
|
import { validateAgentCode } from '@/api/agent';
|
||||||
import { handleApiError } from '@/utils/rbac-error-handler';
|
import { handleApiError } from '@/utils/rbac-error-handler';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CUSTOMER_PASSWORD,
|
DEFAULT_CUSTOMER_PASSWORD,
|
||||||
@ -216,6 +257,11 @@ import {
|
|||||||
resolveAdvisorInvite,
|
resolveAdvisorInvite,
|
||||||
stashAdvisorInvite,
|
stashAdvisorInvite,
|
||||||
} from '@/utils/advisorInviteStorage';
|
} from '@/utils/advisorInviteStorage';
|
||||||
|
import {
|
||||||
|
clearStashedAgentInvite,
|
||||||
|
resolveAgentInvite,
|
||||||
|
stashAgentInvite,
|
||||||
|
} from '@/utils/agentInviteStorage';
|
||||||
import { buildInviteRegisterUrl } from '@/utils/inviteLink';
|
import { buildInviteRegisterUrl } from '@/utils/inviteLink';
|
||||||
import { isTimeoutError, withTimeout } from '@/utils/withTimeout';
|
import { isTimeoutError, withTimeout } from '@/utils/withTimeout';
|
||||||
import { isWeChatBrowser, hideWeChatBrowserNav } from '@/utils/device';
|
import { isWeChatBrowser, hideWeChatBrowserNav } from '@/utils/device';
|
||||||
@ -271,6 +317,8 @@ function goBind() {
|
|||||||
|
|
||||||
function startOAuth() {
|
function startOAuth() {
|
||||||
authModalVisible.value = false;
|
authModalVisible.value = false;
|
||||||
|
const agent = formState.agentCodeInput.trim() || agentCode.value;
|
||||||
|
if (agent) stashAgentInvite(agent, agentLocked.value);
|
||||||
void redirectToWeChatOAuth('register', inviteCode.value);
|
void redirectToWeChatOAuth('register', inviteCode.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,6 +354,10 @@ const wxUserInfo = ref<WeChatOAuthUserInfo | null>(null);
|
|||||||
const inviteCode = ref('');
|
const inviteCode = ref('');
|
||||||
const advisorCode = ref('');
|
const advisorCode = ref('');
|
||||||
const agentCode = ref('');
|
const agentCode = ref('');
|
||||||
|
const agentLocked = ref(false);
|
||||||
|
const agentValidated = ref<boolean | null>(null);
|
||||||
|
const agentNameMasked = ref('');
|
||||||
|
const agentLoading = ref(false);
|
||||||
const referrerValidated = ref<boolean | null>(null);
|
const referrerValidated = ref<boolean | null>(null);
|
||||||
const referrerInfo = ref<{ name: string; code: string } | null>(null);
|
const referrerInfo = ref<{ name: string; code: string } | null>(null);
|
||||||
let loadingSlowTimer: number | undefined;
|
let loadingSlowTimer: number | undefined;
|
||||||
@ -313,8 +365,68 @@ let loadingSlowTimer: number | undefined;
|
|||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
customerName: '',
|
customerName: '',
|
||||||
password: DEFAULT_CUSTOMER_PASSWORD,
|
password: DEFAULT_CUSTOMER_PASSWORD,
|
||||||
|
agentCodeInput: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const effectiveAgentCode = computed(() =>
|
||||||
|
agentValidated.value === true ? formState.agentCodeInput.trim() : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const AGENT_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
|
async function validateAgent(code: string, locked: boolean) {
|
||||||
|
const trimmed = code.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
agentValidated.value = null;
|
||||||
|
agentNameMasked.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agentLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await withTimeout(validateAgentCode(trimmed), AGENT_TIMEOUT_MS);
|
||||||
|
if (!data.valid) {
|
||||||
|
agentValidated.value = false;
|
||||||
|
agentNameMasked.value = '';
|
||||||
|
if (locked) {
|
||||||
|
Message.warning(data.message || t('marketing.register.agentInvalid'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formState.agentCodeInput = data.agentCode || trimmed;
|
||||||
|
agentCode.value = formState.agentCodeInput;
|
||||||
|
agentNameMasked.value = data.agentNameMasked || '';
|
||||||
|
agentValidated.value = true;
|
||||||
|
agentLocked.value = locked;
|
||||||
|
stashAgentInvite(formState.agentCodeInput, locked);
|
||||||
|
} catch {
|
||||||
|
agentValidated.value = false;
|
||||||
|
agentNameMasked.value = '';
|
||||||
|
} finally {
|
||||||
|
agentLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAgentInputBlur() {
|
||||||
|
if (agentLocked.value) return;
|
||||||
|
const code = formState.agentCodeInput.trim();
|
||||||
|
if (!code) {
|
||||||
|
agentValidated.value = null;
|
||||||
|
agentNameMasked.value = '';
|
||||||
|
agentCode.value = '';
|
||||||
|
clearStashedAgentInvite();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void validateAgent(code, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAgentInputClear() {
|
||||||
|
if (agentLocked.value) return;
|
||||||
|
agentValidated.value = null;
|
||||||
|
agentNameMasked.value = '';
|
||||||
|
agentCode.value = '';
|
||||||
|
clearStashedAgentInvite();
|
||||||
|
}
|
||||||
|
|
||||||
const avatarLetter = computed(() => {
|
const avatarLetter = computed(() => {
|
||||||
const name = wxUserInfo.value?.nickname || formState.customerName || 'W';
|
const name = wxUserInfo.value?.nickname || formState.customerName || 'W';
|
||||||
return name.trim().charAt(0).toUpperCase();
|
return name.trim().charAt(0).toUpperCase();
|
||||||
@ -420,8 +532,17 @@ async function handleSubmit() {
|
|||||||
const errors = await formRef.value?.validate();
|
const errors = await formRef.value?.validate();
|
||||||
if (errors) return;
|
if (errors) return;
|
||||||
|
|
||||||
|
const typedAgent = formState.agentCodeInput.trim();
|
||||||
|
if (typedAgent && agentValidated.value !== true) {
|
||||||
|
await validateAgent(typedAgent, agentLocked.value);
|
||||||
|
if (agentValidated.value !== true) {
|
||||||
|
Message.warning(t('marketing.register.agentInvalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canBindInvite = Boolean(
|
const canBindInvite = Boolean(
|
||||||
!agentCode.value && inviteCode.value && referrerValidated.value === true,
|
!effectiveAgentCode.value && inviteCode.value && referrerValidated.value === true,
|
||||||
);
|
);
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
@ -429,7 +550,7 @@ async function handleSubmit() {
|
|||||||
const result = await registerByWechat({
|
const result = await registerByWechat({
|
||||||
customerName: formState.customerName.trim(),
|
customerName: formState.customerName.trim(),
|
||||||
password: formState.password,
|
password: formState.password,
|
||||||
...(agentCode.value ? { agentCode: agentCode.value } : {}),
|
...(effectiveAgentCode.value ? { agentCode: effectiveAgentCode.value } : {}),
|
||||||
...(canBindInvite ? { inviteCode: inviteCode.value } : {}),
|
...(canBindInvite ? { inviteCode: inviteCode.value } : {}),
|
||||||
...(advisorCode.value ? { advisorCode: advisorCode.value } : {}),
|
...(advisorCode.value ? { advisorCode: advisorCode.value } : {}),
|
||||||
wechat: {
|
wechat: {
|
||||||
@ -441,6 +562,7 @@ async function handleSubmit() {
|
|||||||
});
|
});
|
||||||
clearStashedWeChatInviteRef();
|
clearStashedWeChatInviteRef();
|
||||||
clearStashedAdvisorInvite();
|
clearStashedAdvisorInvite();
|
||||||
|
clearStashedAgentInvite();
|
||||||
Message.success(t('wechat.register.success'));
|
Message.success(t('wechat.register.success'));
|
||||||
const account = String(result?.customerCode || '').trim();
|
const account = String(result?.customerCode || '').trim();
|
||||||
const loggedIn = account
|
const loggedIn = account
|
||||||
@ -466,12 +588,15 @@ async function handleSubmit() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
hideWeChatBrowserNav();
|
hideWeChatBrowserNav();
|
||||||
loadingHint.value = t('wechat.common.loading');
|
loadingHint.value = t('wechat.common.loading');
|
||||||
const agent = String(route.query.agent ?? route.query.agentCode ?? '').trim();
|
const agent = resolveAgentInvite(route.query as Record<string, unknown>);
|
||||||
if (agent) {
|
if (agent.code) {
|
||||||
agentCode.value = agent;
|
formState.agentCodeInput = agent.code;
|
||||||
|
agentCode.value = agent.code;
|
||||||
|
agentLocked.value = agent.locked;
|
||||||
|
void validateAgent(agent.code, agent.locked);
|
||||||
}
|
}
|
||||||
const refCode = resolveWeChatInviteRef(route.query as Record<string, unknown>);
|
const refCode = resolveWeChatInviteRef(route.query as Record<string, unknown>);
|
||||||
if (refCode && !agent) {
|
if (refCode && !agent.code) {
|
||||||
void loadReferrer(refCode);
|
void loadReferrer(refCode);
|
||||||
}
|
}
|
||||||
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>);
|
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>);
|
||||||
|
|||||||
@ -338,7 +338,16 @@ export default defineConfig(({ mode }) => {
|
|||||||
if (path.startsWith('/agent/admin') || path.startsWith('/agent/portal')) {
|
if (path.startsWith('/agent/admin') || path.startsWith('/agent/portal')) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const agentSpaPages = ['/agent', '/agent/invite', '/agent/customers', '/agent/orders'];
|
const agentSpaPages = [
|
||||||
|
'/agent',
|
||||||
|
'/agent/workplace',
|
||||||
|
'/agent/invite',
|
||||||
|
'/agent/priceCheck',
|
||||||
|
'/agent/placeOrder',
|
||||||
|
'/agent/customers',
|
||||||
|
'/agent/orders',
|
||||||
|
'/agent/commission',
|
||||||
|
];
|
||||||
if (agentSpaPages.some((p) => path === p || path === `${p}/`)) {
|
if (agentSpaPages.some((p) => path === p || path === `${p}/`)) {
|
||||||
return '/index.html';
|
return '/index.html';
|
||||||
}
|
}
|
||||||
@ -575,9 +584,13 @@ export default defineConfig(({ mode }) => {
|
|||||||
'/Plugin/AddPackage',
|
'/Plugin/AddPackage',
|
||||||
// 渠道代理门户(API 为 /agent/admin、/agent/portal,勿用前缀匹配)
|
// 渠道代理门户(API 为 /agent/admin、/agent/portal,勿用前缀匹配)
|
||||||
'/agent',
|
'/agent',
|
||||||
|
'/agent/workplace',
|
||||||
'/agent/invite',
|
'/agent/invite',
|
||||||
|
'/agent/priceCheck',
|
||||||
|
'/agent/placeOrder',
|
||||||
'/agent/customers',
|
'/agent/customers',
|
||||||
'/agent/orders',
|
'/agent/orders',
|
||||||
|
'/agent/commission',
|
||||||
'/agentAdmin/list',
|
'/agentAdmin/list',
|
||||||
'/agentAdmin/commission',
|
'/agentAdmin/commission',
|
||||||
];
|
];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user