fix(admin): 主站分销管理按最新分成与释放口径改展示

规则页按模式展示收号/卖出/服务费,通道费标明主站承担;订单改为释放状态筛选;结算区分自动与人工批次。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-10 17:36:49 +08:00
co-authored by Cursor
parent 647fdc0b4a
commit e621722218
10 changed files with 677 additions and 138 deletions
@@ -42,7 +42,11 @@ export interface DistributionOrder {
status: string
refund_status: string
settlement_status: string
distributor_settlement_status?: string
release_at?: string
distribution_mode: string
gross_paid_amount_cent: number
refund_amount_cent: number
net_paid_amount_cent: number
payment_fee_cent: number
service_fee_cent: number
@@ -194,6 +198,8 @@ export async function updateDistributionRuleStatus(id: number, status: string) {
export async function fetchDistributionOrders(params: {
kind?: string
status?: string
mode?: string
financialStatus?: string
siteID?: number
page?: number
pageSize?: number
@@ -204,6 +210,8 @@ export async function fetchDistributionOrders(params: {
params: {
kind: params.kind || undefined,
status: params.status || undefined,
mode: params.mode || undefined,
financial_status: params.financialStatus || undefined,
site_id: params.siteID || undefined,
page: params.page || 1,
page_size: params.pageSize || 20,
@@ -0,0 +1,112 @@
import type { DistributionRule } from '@/features/admin/api/adminDistribution'
export const DISTRIBUTION_MODES = [
{
value: 'two_party_distributor_receive',
label: '两方 · 分站收号',
hint: '主站下单,货在分站',
},
{
value: 'two_party_platform_receive',
label: '两方 · 分站卖出',
hint: '分站卖主站货',
},
{
value: 'three_party',
label: '三方分账',
hint: '分站卖另一分站的货',
},
] as const
const MODE_LABELS: Record<string, string> = Object.fromEntries(
DISTRIBUTION_MODES.map(item => [item.value, item.label])
)
const PARTY_ROLE_LABELS: Record<string, string> = {
distributor_receive: '分站收号',
platform_sell: '主站卖出',
platform_receive: '主站收号',
distributor_sell: '分站卖出',
distributor_a: '收号站',
distributor_b: '卖出站',
}
export function distributionModeLabel(mode?: string) {
if (!mode) return '-'
return MODE_LABELS[mode] || mode
}
export function distributionPartyRoleLabel(role?: string) {
if (!role) return '-'
return PARTY_ROLE_LABELS[role] || role
}
export function formatRatioPercent(value?: number) {
const n = Number(value || 0)
return `${n.toFixed(n % 1 === 0 ? 0 : 1)}%`
}
export function formatPaymentFeeBps(bps?: number) {
return `${Number(bps || 0) / 100}%`
}
export function ruleSplitParts(rule: DistributionRule) {
switch (rule.mode) {
case 'two_party_distributor_receive':
return [
{ label: '分站收号', value: formatRatioPercent(rule.distributor_receive_ratio) },
{ label: '主站卖出', value: formatRatioPercent(rule.platform_sell_ratio) },
]
case 'two_party_platform_receive':
return [
{ label: '主站收号', value: formatRatioPercent(rule.platform_receive_ratio) },
{ label: '分站卖出', value: formatRatioPercent(rule.distributor_sell_ratio) },
]
case 'three_party':
return [
{ label: '收号站', value: formatRatioPercent(rule.distributor_a_ratio) },
{ label: '卖出站', value: formatRatioPercent(rule.distributor_b_ratio) },
]
default:
return []
}
}
export function formatServiceFee(
rule: Pick<DistributionRule, 'service_fee_value' | 'service_fee_unit'>
) {
if (rule.service_fee_unit === 'fixed_amount') {
return `${Number(rule.service_fee_value || 0).toFixed(2)} 元/单`
}
return formatRatioPercent(rule.service_fee_value)
}
export function settlementBatchKind(batchNo?: string) {
if (batchNo?.startsWith('DA')) {
return { label: '自动释放', type: 'success' as const }
}
if (batchNo?.startsWith('DS')) {
return { label: '人工批次', type: 'warning' as const }
}
return { label: '结算批次', type: 'info' as const }
}
export function distributorReleaseLabel(row: {
distributor_settlement_status?: string
release_at?: string
}) {
if (row.distributor_settlement_status === 'settled') {
return { label: '已释放入账', type: 'success' as const }
}
if (row.distributor_settlement_status === 'pending' && row.release_at) {
const date = new Date(row.release_at)
const text = Number.isNaN(date.getTime())
? '待释放'
: `待释放 ${date.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })}`
return { label: text, type: 'warning' as const }
}
if (row.distributor_settlement_status === 'pending') {
return { label: '待释放', type: 'info' as const }
}
return { label: '未进入释放', type: 'info' as const }
}
@@ -7,8 +7,14 @@ import {
type DistributionOrder,
type DistributionSite,
} from '@/features/admin/api/adminDistribution'
import {
DISTRIBUTION_MODES,
distributionModeLabel,
distributorReleaseLabel,
} from '@/features/admin/utils/distributionLabels'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { orderStatusLabel, refundStatusLabel } from '@/shared/utils/statusLabels'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
@@ -18,18 +24,13 @@ const total = ref(0)
const query = reactive({
siteID: undefined as number | undefined,
kind: '' as string,
status: '' as string,
mode: '',
status: '',
financialStatus: '',
page: 1,
pageSize: 20,
})
const modeMap: Record<string, string> = {
two_party_distributor_receive: '收号',
two_party_platform_receive: '售出',
three_party: '三方',
}
async function loadSites() {
try {
sites.value = await fetchDistributionSites()
@@ -43,8 +44,9 @@ async function loadOrders() {
try {
const res = await fetchDistributionOrders({
siteID: query.siteID,
kind: query.kind || undefined,
mode: query.mode || undefined,
status: query.status || undefined,
financialStatus: query.financialStatus || undefined,
page: query.page,
pageSize: query.pageSize,
})
@@ -64,15 +66,16 @@ function handleSearch() {
function handleReset() {
query.siteID = undefined
query.kind = ''
query.mode = ''
query.status = ''
query.financialStatus = ''
query.page = 1
loadOrders()
}
function getSiteName(siteId: number) {
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name}` : `站点 #${siteId}`
return site ? site.site_name : `站点 #${siteId}`
}
onMounted(async () => {
@@ -85,9 +88,9 @@ onMounted(async () => {
<div class="distribution-orders-view">
<div class="page-header">
<div>
<h2 class="page-title">分销订单监控</h2>
<h2 class="page-title">分销订单</h2>
<p class="page-subtitle">
全览各合作分销站点的收号与售出订单明细、渠道手续费、单笔服务费及利润分成流水。
查看分销订单的利润基数、主站承担的通道费,以及冷却期释放状态。不要把订单履约结算和分销入账混为一谈。
</p>
</div>
</div>
@@ -104,17 +107,33 @@ onMounted(async () => {
/>
</el-select>
</el-form-item>
<el-form-item label="交易类型">
<el-select v-model="query.kind" clearable placeholder="全部模式" style="width: 140px">
<el-option label="分销收号" value="receive" />
<el-option label="分销售出" value="sold" />
<el-option label="全部" value="all" />
<el-form-item label="分销模式">
<el-select v-model="query.mode" clearable placeholder="全部模式" style="width: 180px">
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="结算状态">
<el-select v-model="query.status" clearable placeholder="全部状态" style="width: 140px">
<el-option label="待结算" value="pending" />
<el-option label="已结算" value="settled" />
<el-form-item label="订单状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width: 140px">
<el-option label="已完成" value="completed" />
<el-option label="使用中" value="renting" />
<el-option label="申诉中" value="disputing" />
<el-option label="已关闭" value="closed" />
</el-select>
</el-form-item>
<el-form-item label="释放状态">
<el-select
v-model="query.financialStatus"
clearable
placeholder="全部"
style="width: 140px"
>
<el-option label="待释放" value="pending" />
<el-option label="已释放入账" value="settled" />
</el-select>
</el-form-item>
<el-form-item>
@@ -127,53 +146,62 @@ onMounted(async () => {
<el-card shadow="never" style="margin-top: 16px">
<el-table v-loading="loading" :data="orders" stripe style="width: 100%">
<el-table-column label="订单号" prop="order_no" min-width="160" />
<el-table-column label="分销站点" min-width="140">
<el-table-column label="分销站点" min-width="130">
<template #default="{ row }">
<el-tag size="small">{{ getSiteName(row.site_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="模式" width="90" align="center">
<el-table-column label="模式" min-width="140">
<template #default="{ row }">
<el-tag type="info" size="small">
{{ modeMap[row.distribution_mode] || row.distribution_mode }}
</el-tag>
{{ distributionModeLabel(row.distribution_mode) }}
</template>
</el-table-column>
<el-table-column label="订单实付" min-width="110" align="right">
<el-table-column label="净付款" min-width="100" align="right">
<template #default="{ row }">
<strong>{{ formatCent(row.net_paid_amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="渠道手续费" min-width="100" align="right">
<el-table-column label="分站加价" min-width="100" align="right">
<template #default="{ row }">
{{ formatCent(row.payment_fee_cent) }}
{{ formatCent(row.markup_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="固定服务费" min-width="100" align="right">
<el-table-column label="利润基数" min-width="100" align="right">
<template #default="{ row }">
{{ formatCent(row.split_base_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="通道费" min-width="110" align="right">
<template #default="{ row }">
<div>{{ formatCent(row.payment_fee_cent) }}</div>
<small class="muted">主站承担</small>
</template>
</el-table-column>
<el-table-column label="服务费" min-width="90" align="right">
<template #default="{ row }">
{{ formatCent(row.service_fee_cent) }}
</template>
</el-table-column>
<el-table-column label="税费" min-width="90" align="right">
<template #default="{ row }">
{{ formatCent(row.tax_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="订单状态" width="100" align="center">
<el-table-column label="订单状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'completed' ? 'success' : 'warning'" size="small">
{{ row.status }}
{{ orderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="结算状态" width="100" align="center">
<el-table-column label="退款" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.settlement_status === 'settled' ? 'success' : 'info'" size="small">
{{ row.settlement_status === 'settled' ? '已结算' : '待结算' }}
<span>{{ refundStatusLabel(row.refund_status) }}</span>
</template>
</el-table-column>
<el-table-column label="分销入账" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="distributorReleaseLabel(row).type" size="small">
{{ distributorReleaseLabel(row).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="160">
<el-table-column label="下单时间" min-width="160">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
@@ -215,6 +243,7 @@ onMounted(async () => {
font-size: 13px;
color: #6b7280;
margin: 0;
max-width: 720px;
}
.filter-card {
@@ -226,4 +255,9 @@ onMounted(async () => {
justify-content: flex-end;
margin-top: 16px;
}
.muted {
color: #9ca3af;
font-size: 12px;
}
</style>
@@ -1,26 +1,49 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
fetchDistributionRules,
fetchDistributionSites,
updateDistributionRuleStatus,
createDistributionRule,
type DistributionRule,
type DistributionSite,
type CreateRulePayload,
} from '@/features/admin/api/adminDistribution'
import {
DISTRIBUTION_MODES,
distributionModeLabel,
formatPaymentFeeBps,
formatServiceFee,
ruleSplitParts,
} from '@/features/admin/utils/distributionLabels'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
const sites = ref<DistributionSite[]>([])
const selectedSiteId = ref<number | undefined>(undefined)
const selectedMode = ref('')
const rules = ref<DistributionRule[]>([])
const modeMap: Record<string, string> = {
two_party_distributor_receive: '两方-分销收号',
two_party_platform_receive: '两方-分销卖出',
three_party: '三方分销',
}
const createVisible = ref(false)
const creating = ref(false)
const createForm = reactive({
site_id: undefined as number | undefined,
mode: 'two_party_distributor_receive',
receive_ratio: 35,
sell_ratio: 40,
service_fee_value: 25,
payment_fee_bps: 50,
tax_enabled: false,
tax_rate_bps: 100,
activate: true,
})
const createModeMeta = computed(
() => DISTRIBUTION_MODES.find(item => item.value === createForm.mode) ?? DISTRIBUTION_MODES[0]
)
async function loadSites() {
try {
@@ -33,7 +56,10 @@ async function loadSites() {
async function loadRules() {
loading.value = true
try {
rules.value = await fetchDistributionRules(selectedSiteId.value)
rules.value = await fetchDistributionRules(
selectedSiteId.value,
selectedMode.value || undefined
)
} catch (err) {
ElMessage.error(readError(err, '获取分销规则列表失败'))
} finally {
@@ -41,6 +67,18 @@ async function loadRules() {
}
}
function getSiteName(siteId?: number) {
if (!siteId) return '全局默认'
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name} (${site.site_code})` : `站点 #${siteId}`
}
function statusMeta(status: string) {
if (status === 'active') return { label: '生效中', type: 'success' as const }
if (status === 'draft') return { label: '草稿', type: 'info' as const }
return { label: '已停用', type: 'danger' as const }
}
async function handleToggleStatus(row: DistributionRule) {
const next = row.status === 'active' ? 'disabled' : 'active'
try {
@@ -52,10 +90,69 @@ async function handleToggleStatus(row: DistributionRule) {
}
}
function getSiteName(siteId?: number) {
if (!siteId) return '全局默认'
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name} (${site.site_code})` : `站点 #${siteId}`
function resetCreateForm() {
createForm.site_id = selectedSiteId.value
createForm.mode = selectedMode.value || 'two_party_distributor_receive'
createForm.receive_ratio = 35
createForm.sell_ratio = 40
createForm.service_fee_value = 25
createForm.payment_fee_bps = 50
createForm.tax_enabled = false
createForm.tax_rate_bps = 100
createForm.activate = true
}
function handleOpenCreate() {
resetCreateForm()
createVisible.value = true
}
function buildCreatePayload(): CreateRulePayload {
const payload: CreateRulePayload = {
site_id: createForm.site_id || undefined,
mode: createForm.mode,
service_fee_value: Number(createForm.service_fee_value),
service_fee_unit: 'percent',
payment_fee_bps: Number(createForm.payment_fee_bps),
tax_enabled: createForm.tax_enabled,
tax_rate_bps: createForm.tax_enabled ? Number(createForm.tax_rate_bps) : 100,
}
if (createForm.mode === 'two_party_distributor_receive') {
payload.distributor_receive_ratio = Number(createForm.receive_ratio)
payload.platform_sell_ratio = Number(createForm.sell_ratio)
} else if (createForm.mode === 'two_party_platform_receive') {
payload.platform_receive_ratio = Number(createForm.receive_ratio)
payload.distributor_sell_ratio = Number(createForm.sell_ratio)
} else {
payload.distributor_a_ratio = Number(createForm.receive_ratio)
payload.distributor_b_ratio = Number(createForm.sell_ratio)
}
return payload
}
async function handleCreate() {
const total =
Number(createForm.receive_ratio) +
Number(createForm.sell_ratio) +
Number(createForm.service_fee_value)
if (Math.abs(total - 100) > 0.05) {
ElMessage.warning(`收号 + 卖出 + 服务费应为 100%,当前为 ${total}%`)
return
}
creating.value = true
try {
const created = await createDistributionRule(buildCreatePayload())
if (createForm.activate) {
await updateDistributionRuleStatus(created.id, 'active')
}
ElMessage.success(createForm.activate ? '规则已创建并启用' : '规则草稿已创建')
createVisible.value = false
await loadRules()
} catch (err) {
ElMessage.error(readError(err, '创建分销规则失败'))
} finally {
creating.value = false
}
}
onMounted(async () => {
@@ -70,15 +167,15 @@ onMounted(async () => {
<div>
<h2 class="page-title">分销规则配置</h2>
<p class="page-subtitle">
配置与维护各分销站点的佣金分成比例、单笔服务费、支付通道手续费与税费规则。
按模式配置利润分成。利润 = 净付款 − 号主 − 加价;通道费由主站承担,只记账不进分成。
</p>
</div>
<div class="header-filters">
<el-select
v-model="selectedSiteId"
clearable
placeholder="按分销站点筛选"
style="width: 240px"
placeholder="全部站点(含全局)"
style="width: 220px"
@change="loadRules"
>
<el-option
@@ -88,44 +185,75 @@ onMounted(async () => {
:value="site.id"
/>
</el-select>
<el-select
v-model="selectedMode"
clearable
placeholder="全部分销模式"
style="width: 200px"
@change="loadRules"
>
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate">新建规则</el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
show-icon
class="rules-alert"
title="默认 35% 收号 / 40% 卖出 / 25% 服务费。站点专属规则优先于全局默认;通道费默认 0.5%,从主站服务费与角色分成中承担。"
/>
<el-card shadow="never">
<el-table v-loading="loading" :data="rules" stripe style="width: 100%">
<el-table-column label="适用站点" min-width="160">
<el-table-column label="适用站点" min-width="170">
<template #default="{ row }">
<el-tag :type="row.site_id ? 'primary' : 'info'" size="small">
{{ getSiteName(row.site_id) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="分销模式" prop="mode" min-width="140">
<el-table-column label="分销模式" min-width="160">
<template #default="{ row }">
{{ modeMap[row.mode] || row.mode }}
<div class="mode-cell">
<strong>{{ distributionModeLabel(row.mode) }}</strong>
<small>v{{ row.version }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="版本号" prop="version" width="80" align="center">
<template #default="{ row }"> v{{ row.version }} </template>
<el-table-column label="利润分成" min-width="280">
<template #default="{ row }">
<div class="split-cell">
<span v-for="part in ruleSplitParts(row)" :key="part.label" class="split-chip">
{{ part.label }} <strong>{{ part.value }}</strong>
</span>
<span class="split-chip service"
>服务费 <strong>{{ formatServiceFee(row) }}</strong></span
>
</div>
</template>
</el-table-column>
<el-table-column label="分销商收号分成" min-width="130" align="right">
<template #default="{ row }"> {{ row.distributor_receive_ratio }}% </template>
<el-table-column label="通道费" min-width="140">
<template #default="{ row }">
<div class="fee-cell">
<span>{{ formatPaymentFeeBps(row.payment_fee_bps) }}</span>
<small>主站承担</small>
</div>
</template>
</el-table-column>
<el-table-column label="平台卖号分成" min-width="120" align="right">
<template #default="{ row }"> {{ row.platform_sell_ratio }}% </template>
</el-table-column>
<el-table-column label="服务费" min-width="110" align="right">
<template #default="{ row }"> {{ row.service_fee_value }}% </template>
</el-table-column>
<el-table-column label="支付手续费" min-width="110" align="right">
<template #default="{ row }"> {{ row.payment_fee_bps / 100 }}% </template>
</el-table-column>
<el-table-column label="税费率" min-width="100" align="center">
<el-table-column label="税费" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.tax_enabled" type="warning" size="small">
{{ row.tax_rate_bps / 100 }}%
</el-tag>
<el-tag v-else type="info" size="small">免税</el-tag>
<el-tag v-else type="info" size="small">关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="生效时间" min-width="160">
@@ -135,8 +263,8 @@ onMounted(async () => {
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'" size="small">
{{ row.status === 'active' ? '生效中' : '已停用' }}
<el-tag :type="statusMeta(row.status).type" size="small">
{{ statusMeta(row.status).label }}
</el-tag>
</template>
</el-table-column>
@@ -153,6 +281,101 @@ onMounted(async () => {
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="createVisible" title="新建分销规则" width="560px" destroy-on-close>
<el-alert
:closable="false"
type="info"
show-icon
:title="`${createModeMeta?.label}:${createModeMeta?.hint}。分成三项合计应为 100%。`"
style="margin-bottom: 16px"
/>
<el-form :model="createForm" label-width="120px">
<el-form-item label="适用站点">
<el-select
v-model="createForm.site_id"
clearable
placeholder="全局默认"
style="width: 100%"
>
<el-option
v-for="site in sites"
:key="site.id"
:label="`${site.site_name} (${site.site_code})`"
:value="site.id"
/>
</el-select>
</el-form-item>
<el-form-item label="分销模式">
<el-select v-model="createForm.mode" style="width: 100%">
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="`${item.label}(${item.hint})`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '收号站分成' : '收号分成'">
<el-input-number
v-model="createForm.receive_ratio"
:min="0"
:max="100"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '卖出站分成' : '卖出分成'">
<el-input-number
v-model="createForm.sell_ratio"
:min="0"
:max="100"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="平台服务费">
<el-input-number
v-model="createForm.service_fee_value"
:min="0"
:max="100"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="通道费率 BPS">
<el-input-number
v-model="createForm.payment_fee_bps"
:min="0"
:max="10000"
:step="1"
style="width: 100%"
/>
<div class="form-hint">
当前 {{ formatPaymentFeeBps(createForm.payment_fee_bps) }},由主站承担,不从利润里扣。
</div>
</el-form-item>
<el-form-item label="征收税费">
<el-switch v-model="createForm.tax_enabled" />
</el-form-item>
<el-form-item v-if="createForm.tax_enabled" label="税费率 BPS">
<el-input-number
v-model="createForm.tax_rate_bps"
:min="0"
:max="10000"
:step="1"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="立即启用">
<el-switch v-model="createForm.activate" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreate">保存规则</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -165,7 +388,8 @@ onMounted(async () => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
margin-bottom: 16px;
gap: 16px;
}
.page-title {
@@ -180,4 +404,65 @@ onMounted(async () => {
color: #6b7280;
margin: 0;
}
.header-filters {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.rules-alert {
margin-bottom: 16px;
}
.mode-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.mode-cell small {
color: #9ca3af;
font-size: 12px;
}
.split-cell {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.split-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
background: #f3f4f6;
color: #374151;
font-size: 12px;
}
.split-chip.service {
background: #eff6ff;
color: #1d4ed8;
}
.fee-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.fee-cell small {
color: #9ca3af;
font-size: 12px;
}
.form-hint {
margin-top: 4px;
color: #6b7280;
font-size: 12px;
line-height: 1.4;
}
</style>
@@ -12,6 +12,10 @@ import {
type DistributionSettlementItem,
type DistributionSite,
} from '@/features/admin/api/adminDistribution'
import {
distributionPartyRoleLabel,
settlementBatchKind,
} from '@/features/admin/utils/distributionLabels'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
@@ -33,6 +37,8 @@ const itemsLoading = ref(false)
const currentBatch = ref<DistributionSettlementBatch | null>(null)
const items = ref<DistributionSettlementItem[]>([])
const selectedSite = () => sites.value.find(site => site.id === selectedSiteId.value)
async function loadSites() {
try {
const list = await fetchDistributionSites()
@@ -73,7 +79,7 @@ async function handleCreateBatch() {
creating.value = true
try {
await createDistributionSettlementBatch(createForm.site_id, start, end)
ElMessage.success('结算批次生成成功')
ElMessage.success('人工结算批次已生成,可立即释放入账')
createDialogVisible.value = false
await loadBatches()
} catch (err) {
@@ -86,12 +92,12 @@ async function handleCreateBatch() {
async function handleSettle(row: DistributionSettlementBatch) {
try {
await ElMessageBox.confirm(
`确定将结算批次 ${row.batch_no} 标记为已打款结算吗?结算总额为 ${formatCent(row.total_amount_cent)}。`,
'确认结算',
{ type: 'warning', confirmButtonText: '确认标记已结算', cancelButtonText: '取消' }
`确认将批次 ${row.batch_no} 的 ${formatCent(row.total_amount_cent)} 立即转入站点可提现余额?人工批次会绕过冷却期。`,
'立即释放入账',
{ type: 'warning', confirmButtonText: '确认入账', cancelButtonText: '取消' }
)
await settleDistributionBatch(row.id)
ElMessage.success('批次已成功标记为已结算')
ElMessage.success('批次已释放到站点可提现余额')
await loadBatches()
} catch {
// cancelled or error handled
@@ -111,6 +117,13 @@ async function handleViewItems(row: DistributionSettlementBatch) {
}
}
function batchStatus(row: DistributionSettlementBatch) {
if (row.status === 'settled') return { label: '已释放入账', type: 'success' as const }
if (row.status === 'pending' || row.status === 'draft')
return { label: '待入账', type: 'warning' as const }
return { label: row.status, type: 'info' as const }
}
onMounted(() => {
loadSites()
})
@@ -120,9 +133,9 @@ onMounted(() => {
<div class="distribution-settlements-view">
<div class="page-header">
<div>
<h2 class="page-title">分销结算管理</h2>
<h2 class="page-title">分销结算对账</h2>
<p class="page-subtitle">
按周期汇总各合作分销站点的佣金分成批次,并执行最终结算出款确认。
冷却期满后系统按站点按天自动出对账单并入账。人工批次用于立即结算,不经过冷却期。
</p>
</div>
<div class="header-actions">
@@ -139,38 +152,48 @@ onMounted(() => {
:value="site.id"
/>
</el-select>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate"> 生成结算批次 </el-button>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate"> 生成人工批次 </el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
show-icon
class="settlement-alert"
:title="`当前站点冷却期 ${selectedSite()?.settlement_hold_days ?? 7} 天。DA 开头为自动释放对账单,DS 开头为人工立即结算。`"
/>
<el-card shadow="never">
<el-table v-loading="loading" :data="batches" stripe style="width: 100%">
<el-table-column label="结算批次号" prop="batch_no" min-width="180" />
<el-table-column label="周期开始时间" min-width="160">
<el-table-column label="批次号" prop="batch_no" min-width="200">
<template #default="{ row }">
{{ formatDateTime(row.period_start) }}
<div class="batch-no-cell">
<code>{{ row.batch_no }}</code>
<el-tag :type="settlementBatchKind(row.batch_no).type" size="small">
{{ settlementBatchKind(row.batch_no).label }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="周期结束时间" min-width="160">
<el-table-column label="对账周期" min-width="220">
<template #default="{ row }">
{{ formatDateTime(row.period_end) }}
{{ formatDateTime(row.period_start) }} ~ {{ formatDateTime(row.period_end) }}
</template>
</el-table-column>
<el-table-column label="应结总金额" min-width="140" align="right">
<el-table-column label="入账金额" min-width="130" align="right">
<template #default="{ row }">
<strong style="color: #67c23a; font-size: 15px">
{{ formatCent(row.total_amount_cent) }}
</strong>
<strong class="amount">{{ formatCent(row.total_amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="批次状态" width="120" align="center">
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'settled' ? 'success' : 'warning'" size="small">
{{ row.status === 'settled' ? '已打款' : '待结算' }}
<el-tag :type="batchStatus(row).type" size="small">
{{ batchStatus(row).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewItems(row)">查看明细</el-button>
<el-button
@@ -179,20 +202,26 @@ onMounted(() => {
type="success"
@click="handleSettle(row)"
>
打款结算
立即释放入账
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 生成结算批次对话框 -->
<el-dialog
v-model="createDialogVisible"
title="生成分销结算批次"
width="500px"
title="生成人工结算批次"
width="520px"
destroy-on-close
>
<el-alert
type="warning"
:closable="false"
show-icon
title="人工批次会把周期内尚未入账的分站份额立即结算,不走冷却期。"
style="margin-bottom: 16px"
/>
<el-form label-width="100px">
<el-form-item label="分销站点">
<el-select v-model="createForm.site_id" style="width: 100%">
@@ -219,29 +248,32 @@ onMounted(() => {
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreateBatch">
立即结算汇总
生成批次
</el-button>
</template>
</el-dialog>
<!-- 结算条目明细对话框 -->
<el-dialog
v-model="itemsDialogVisible"
:title="`结算批次明细 (${currentBatch?.batch_no})`"
width="650px"
:title="`批次明细(${currentBatch?.batch_no})`"
width="680px"
>
<el-table v-loading="itemsLoading" :data="items" size="small" border>
<el-table-column label="订单 ID" prop="order_id" width="100" />
<el-table-column label="分成角色" prop="party_role" min-width="150" />
<el-table-column label="应分金额" min-width="120" align="right">
<el-table-column label="分成角色" min-width="140">
<template #default="{ row }">
{{ distributionPartyRoleLabel(row.party_role) }}
</template>
</el-table-column>
<el-table-column label="入账金额" min-width="120" align="right">
<template #default="{ row }">
<strong>{{ formatCent(row.amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="100" align="center">
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'settled' ? 'success' : 'info'" size="small">
{{ row.status }}
{{ row.status === 'settled' ? '已入账' : '待入账' }}
</el-tag>
</template>
</el-table-column>
@@ -262,7 +294,8 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
margin-bottom: 16px;
gap: 16px;
}
.page-title {
@@ -276,10 +309,31 @@ onMounted(() => {
font-size: 13px;
color: #6b7280;
margin: 0;
max-width: 640px;
}
.header-actions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.settlement-alert {
margin-bottom: 16px;
}
.batch-no-cell {
display: flex;
align-items: center;
gap: 8px;
}
.batch-no-cell code {
font-size: 13px;
}
.amount {
color: #16a34a;
font-size: 15px;
}
</style>
@@ -66,6 +66,7 @@ const onboardForm = reactive({
distributor_receive_ratio: 35,
platform_sell_ratio: 40,
service_fee_value: 25,
settlement_hold_days: 7,
display_account_enabled: true,
tax_enabled: false,
tax_rate_bps: 100,
@@ -107,6 +108,7 @@ function handleOpenOnboard() {
onboardForm.distributor_receive_ratio = 35
onboardForm.platform_sell_ratio = 40
onboardForm.service_fee_value = 25
onboardForm.settlement_hold_days = 7
onboardForm.display_account_enabled = true
onboardForm.tax_enabled = false
onboardForm.tax_rate_bps = 100
@@ -130,6 +132,7 @@ async function handleOnboardSubmit() {
distributor_receive_ratio: Number(onboardForm.distributor_receive_ratio),
platform_sell_ratio: Number(onboardForm.platform_sell_ratio),
service_fee_value: Number(onboardForm.service_fee_value),
settlement_hold_days: Number(onboardForm.settlement_hold_days),
display_account_enabled: onboardForm.display_account_enabled,
tax_enabled: onboardForm.tax_enabled,
tax_rate_bps: onboardForm.tax_enabled ? onboardForm.tax_rate_bps : 0,
@@ -186,7 +189,8 @@ onMounted(() => {
<div>
<h2 class="page-title">分销站点管理</h2>
<p class="page-subtitle">
统一管理全平台合作分销站点,支持一键开通分销商、初始化管理员账号及专属佣金费率。
开通分销站、配置结算冷却期。利润按 35% 收号 / 40% 卖出 / 25%
服务费切分,通道费由主站承担。
</p>
</div>
<div class="header-actions">
@@ -321,10 +325,17 @@ onMounted(() => {
</el-col>
</el-row>
<div class="form-section-title">3. 佣金比例与费率初始化</div>
<div class="form-section-title">3. 分站收号规则与冷却期</div>
<el-alert
type="info"
:closable="false"
show-icon
title="此处只生成「两方 · 分站收号」草稿。利润 = 净付款 − 号主 − 加价;通道费 0.5% 由主站承担。另外两种模式请到规则页按全局默认启用或单独配置。"
style="margin-bottom: 12px"
/>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="收号分成比例 (%)">
<el-form-item label="分站收号分成 (%)">
<el-input-number
v-model="onboardForm.distributor_receive_ratio"
:min="0"
@@ -335,7 +346,7 @@ onMounted(() => {
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="平台卖号分成 (%)">
<el-form-item label="主站卖出分成 (%)">
<el-input-number
v-model="onboardForm.platform_sell_ratio"
:min="0"
@@ -358,15 +369,30 @@ onMounted(() => {
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结算冷却期 (天)">
<el-input-number
v-model="onboardForm.settlement_hold_days"
:min="0"
:max="365"
:step="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="展示主站账号">
<el-switch v-model="onboardForm.display_account_enabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否征税">
<el-switch v-model="onboardForm.tax_enabled" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否征税">
<el-switch v-model="onboardForm.tax_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onboardDialogVisible = false">取消</el-button>
@@ -400,9 +426,12 @@ onMounted(() => {
<code>http://{{ windowHost }}:5190/login</code>
</el-descriptions-item>
<el-descriptions-item label="初始规则">
收号分成: {{ onboardResult.rule.distributor_receive_ratio }}% · 卖号分成:
{{ onboardResult.rule.platform_sell_ratio }}% · 服务费:
{{ onboardResult.rule.service_fee_value }}%
分站收号 {{ onboardResult.rule.distributor_receive_ratio }}% · 主站卖出
{{ onboardResult.rule.platform_sell_ratio }}% · 服务费
{{ onboardResult.rule.service_fee_value }}%(草稿,通道费主站承担)
</el-descriptions-item>
<el-descriptions-item label="结算冷却期">
{{ onboardResult.site.settlement_hold_days }} 天
</el-descriptions-item>
</el-descriptions>
</div>
+1 -1
View File
@@ -158,7 +158,7 @@ const allNavGroups: NavGroup[] = [
permission: 'distribution:order_view',
},
{
label: '分销结算',
label: '结算对账',
to: adminPath('distribution/settlements'),
icon: Coin,
permission: 'distribution:settlement_view',