feat(payment): separate creator income settlement
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<PaymentSectionPage
|
||||
section="revenue"
|
||||
title="收益中心"
|
||||
desc="查看收入流水、收益统计和提现记录。"
|
||||
desc="查看现金与积分收入流水、收益统计和提现记录。"
|
||||
:links="[
|
||||
{ label: 'VIP 会员', path: '/payment/vip' },
|
||||
{ label: '推广返佣', path: '/payment/affiliate' },
|
||||
@@ -11,28 +11,10 @@
|
||||
<template #before-form>
|
||||
<n-card class="aksrt-section-card" title="收益统计">
|
||||
<n-grid :cols="4" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item>
|
||||
<n-grid-item v-for="item in revenueSummary" :key="item.key">
|
||||
<div class="aksrt-revenue__stat">
|
||||
<strong>{{ revenueStats.total.toFixed(2) }}</strong>
|
||||
<p>总收益 (元)</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="aksrt-revenue__stat">
|
||||
<strong>{{ revenueStats.pending.toFixed(2) }}</strong>
|
||||
<p>待结算 (元)</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="aksrt-revenue__stat">
|
||||
<strong>{{ revenueStats.settled.toFixed(2) }}</strong>
|
||||
<p>已结算 (元)</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="aksrt-revenue__stat">
|
||||
<strong>{{ revenueStats.count }}</strong>
|
||||
<p>收益笔数</p>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<p>{{ item.label }}</p>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
@@ -47,24 +29,28 @@
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<div v-if="timeSeries.length > 0" class="aksrt-revenue__chart">
|
||||
<div class="aksrt-revenue__chart-bars">
|
||||
<div
|
||||
v-for="(item, index) in timeSeries"
|
||||
:key="index"
|
||||
class="aksrt-revenue__chart-bar-wrap"
|
||||
:title="`${item.date}: ¥${item.amount.toFixed(2)} (${item.count}笔)`"
|
||||
>
|
||||
<div v-if="hasTimeSeries" class="aksrt-revenue__charts">
|
||||
<div v-for="group in timeSeriesGroups" :key="group.key" class="aksrt-revenue__chart">
|
||||
<h4>{{ group.label }}</h4>
|
||||
<div v-if="group.items.length > 0" class="aksrt-revenue__chart-bars">
|
||||
<div
|
||||
class="aksrt-revenue__chart-bar"
|
||||
:style="{ height: getBarHeight(item.amount) + '%' }"
|
||||
/>
|
||||
<span class="aksrt-revenue__chart-label">{{ formatLabel(item.date) }}</span>
|
||||
v-for="(item, index) in group.items"
|
||||
:key="`${group.key}-${index}`"
|
||||
class="aksrt-revenue__chart-bar-wrap"
|
||||
:title="`${item.date}: ${formatAmount(item.amount, group.key)} (${item.count}笔)`"
|
||||
>
|
||||
<div
|
||||
class="aksrt-revenue__chart-bar"
|
||||
:style="{ height: getBarHeight(item.amount, group.items) + '%' }"
|
||||
/>
|
||||
<span class="aksrt-revenue__chart-label">{{ formatLabel(item.date) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else size="small" description="暂无数据" />
|
||||
<div class="aksrt-revenue__chart-summary">
|
||||
<span>期间总计: <strong>{{ formatAmount(group.total, group.key) }}</strong></span>
|
||||
<span>共 {{ group.count }} 笔</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-revenue__chart-summary">
|
||||
<span>期间总计: <strong>¥{{ periodTotal.toFixed(2) }}</strong></span>
|
||||
<span>共 {{ periodCount }} 笔</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-empty v-else description="暂无趋势数据" />
|
||||
@@ -119,23 +105,53 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import PaymentSectionPage from '@/pages/payment/PaymentSectionPage.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const revenueStats = ref({ total: 0, pending: 0, settled: 0, count: 0 })
|
||||
const emptyCurrencyStats = () => ({ total: 0, pending: 0, settled: 0, count: 0 })
|
||||
const revenueStats = ref({ cash: emptyCurrencyStats(), points: emptyCurrencyStats() })
|
||||
const withdrawalStats = ref({ total: 0, pending: 0, processing: 0, completed: 0, rejected: 0, total_paid: 0 })
|
||||
const timeSeries = ref([])
|
||||
const timeSeries = ref({ cash: [], points: [] })
|
||||
const granularity = ref('day')
|
||||
|
||||
const periodTotal = computed(() => timeSeries.value.reduce((s, i) => s + i.amount, 0))
|
||||
const periodCount = computed(() => timeSeries.value.reduce((s, i) => s + i.count, 0))
|
||||
const maxAmount = computed(() => Math.max(1, ...timeSeries.value.map(i => i.amount)))
|
||||
const revenueSummary = computed(() => [
|
||||
{ key: 'cash-total', value: `${revenueStats.value.cash.total.toFixed(2)} 元`, label: '现金总收益' },
|
||||
{ key: 'cash-pending', value: `${revenueStats.value.cash.pending.toFixed(2)} 元`, label: '现金待结算' },
|
||||
{ key: 'cash-settled', value: `${revenueStats.value.cash.settled.toFixed(2)} 元`, label: '现金已结算' },
|
||||
{ key: 'cash-count', value: revenueStats.value.cash.count, label: '现金收益笔数' },
|
||||
{ key: 'points-total', value: `${formatAmount(revenueStats.value.points.total, 'points')}`, label: '积分总收益' },
|
||||
{ key: 'points-pending', value: `${formatAmount(revenueStats.value.points.pending, 'points')}`, label: '积分待结算' },
|
||||
{ key: 'points-settled', value: `${formatAmount(revenueStats.value.points.settled, 'points')}`, label: '积分已结算' },
|
||||
{ key: 'points-count', value: revenueStats.value.points.count, label: '积分收益笔数' },
|
||||
])
|
||||
|
||||
function getBarHeight(amount) {
|
||||
return Math.max(2, (amount / maxAmount.value) * 100)
|
||||
const timeSeriesGroups = computed(() => [
|
||||
{
|
||||
key: 'cash',
|
||||
label: '现金收益',
|
||||
items: Array.isArray(timeSeries.value.cash) ? timeSeries.value.cash : [],
|
||||
},
|
||||
{
|
||||
key: 'points',
|
||||
label: '积分收益',
|
||||
items: Array.isArray(timeSeries.value.points) ? timeSeries.value.points : [],
|
||||
},
|
||||
].map((group) => ({
|
||||
...group,
|
||||
total: group.items.reduce((sum, item) => sum + Number(item.amount || 0), 0),
|
||||
count: group.items.reduce((sum, item) => sum + Number(item.count || 0), 0),
|
||||
})))
|
||||
|
||||
const hasTimeSeries = computed(() => timeSeriesGroups.value.some((group) => group.items.length > 0))
|
||||
|
||||
function formatAmount(amount, currency) {
|
||||
return currency === 'points' ? `${Number(amount || 0).toFixed(0)} 积分` : `¥${Number(amount || 0).toFixed(2)}`
|
||||
}
|
||||
|
||||
function getBarHeight(amount, items) {
|
||||
const maxAmount = Math.max(1, ...items.map((item) => Number(item.amount || 0)))
|
||||
return Math.max(2, (Number(amount || 0) / maxAmount) * 100)
|
||||
}
|
||||
|
||||
function formatLabel(date) {
|
||||
if (granularity.value === 'month') return date.slice(5)
|
||||
if (granularity.value === 'week') return date.slice(5)
|
||||
return date.slice(5)
|
||||
return String(date || '').slice(5)
|
||||
}
|
||||
|
||||
function setGranularity(nextGranularity) {
|
||||
@@ -153,10 +169,8 @@ async function loadRevenue() {
|
||||
if (res?.data) {
|
||||
if (res.data.revenue) {
|
||||
revenueStats.value = {
|
||||
total: Number(res.data.revenue.total || 0),
|
||||
pending: Number(res.data.revenue.pending || 0),
|
||||
settled: Number(res.data.revenue.settled || 0),
|
||||
count: Number(res.data.revenue.count || 0),
|
||||
cash: { ...emptyCurrencyStats(), ...(res.data.revenue.cash || {}) },
|
||||
points: { ...emptyCurrencyStats(), ...(res.data.revenue.points || {}) },
|
||||
}
|
||||
}
|
||||
if (res.data.withdrawal) {
|
||||
@@ -169,7 +183,10 @@ async function loadRevenue() {
|
||||
total_paid: Number(res.data.withdrawal.total_paid || 0),
|
||||
}
|
||||
}
|
||||
timeSeries.value = res.data.time_series || []
|
||||
timeSeries.value = {
|
||||
cash: res.data.time_series?.cash || [],
|
||||
points: res.data.time_series?.points || [],
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
@@ -200,10 +217,20 @@ onMounted(loadRevenue)
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-revenue__charts {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.aksrt-revenue__chart {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.aksrt-revenue__chart h4 {
|
||||
margin: 0 0 10px;
|
||||
color: var(--color-ink, #1a1a2e);
|
||||
}
|
||||
|
||||
.aksrt-revenue__chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -2,46 +2,47 @@ export const revenueSchema = {
|
||||
key: 'payment.revenue',
|
||||
title: '创作分成',
|
||||
icon: 'fas fa-chart-line',
|
||||
description: '配置作者分成比例、结算门槛和结算周期。',
|
||||
description: '分别配置现金收益和积分收益的会员分成比例,结算周期结束后自动入账。',
|
||||
defaults: {
|
||||
pay_income_s: false,
|
||||
payment_revenue_share_ratio: 70,
|
||||
ratio_vip_1: 0,
|
||||
ratio_vip_2: 0,
|
||||
payment_revenue_settlement_threshold: 100,
|
||||
payment_revenue_cycle_days: 30,
|
||||
payment_revenue_settlement_mode: 'automatic',
|
||||
payment_revenue_settlement_delay_hours: 24,
|
||||
payment_revenue_points_rule: '',
|
||||
payment_revenue_note: '分成规则已就绪,订单入账后自动按比例结算。',
|
||||
pay_income_desc_details: '',
|
||||
pay_income_desc_page: '',
|
||||
payment_revenue_note: '<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>',
|
||||
payment_revenue_cash_ratio_user: 60,
|
||||
payment_revenue_cash_ratio_vip_1: 70,
|
||||
payment_revenue_cash_ratio_vip_2: 80,
|
||||
payment_revenue_points_ratio_user: 0,
|
||||
payment_revenue_points_ratio_vip_1: 0,
|
||||
payment_revenue_points_ratio_vip_2: 0,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'policy',
|
||||
title: '分成策略',
|
||||
icon: 'fas fa-percentage',
|
||||
key: 'basic',
|
||||
title: '创作分成设置',
|
||||
icon: 'fas fa-cog',
|
||||
fields: [
|
||||
{ key: 'pay_income_s', label: '启用创作分成结构', type: 'switch', default: false },
|
||||
{ key: 'payment_revenue_share_ratio', label: '作者分成比例(%)', type: 'number', default: 70, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'ratio_vip_1', label: 'VIP1分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'ratio_vip_2', label: 'VIP2分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_settlement_threshold', label: '最小结算金额', type: 'number', default: 100, min: 0, max: 100000, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'pay_income_s', label: '启用创作分成', type: 'switch', default: false },
|
||||
{ key: 'payment_revenue_cycle_days', label: '结算周期(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_settlement_mode', label: '结算模式', type: 'select', default: 'automatic', options: [{ value: 'automatic', label: '自动结算' }, { value: 'manual', label: '手动结算' }], dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_settlement_delay_hours', label: '结算延迟(小时)', type: 'number', default: 24, min: 0, max: 8760, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_points_rule', label: '积分规则', type: 'textarea', default: '', dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_note', label: '结算备注', type: 'textarea', default: '分成策略和台账结构已就绪,订单入账后自动按比例结算。', dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_note', label: '结算备注文案', type: 'textarea', sanitize_type: 'html', default: '<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>', help: '支持安全 HTML,结算后会展示在前台创作中心。', dependsOn: { key: 'pay_income_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'display',
|
||||
title: '展示设置',
|
||||
icon: 'fas fa-file-alt',
|
||||
key: 'cash',
|
||||
title: '现金收益分成',
|
||||
icon: 'fas fa-wallet',
|
||||
fields: [
|
||||
{ key: 'pay_income_desc_details', label: '收益明细文案', type: 'textarea', default: '', dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'pay_income_desc_page', label: '收益说明页', type: 'input', default: '', dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_cash_ratio_user', label: '普通用户分成比例(%)', type: 'number', default: 60, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_cash_ratio_vip_1', label: '一级会员分成比例(%)', type: 'number', default: 70, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_cash_ratio_vip_2', label: '二级会员分成比例(%)', type: 'number', default: 80, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'points',
|
||||
title: '积分收益分成',
|
||||
icon: 'fas fa-coins',
|
||||
fields: [
|
||||
{ key: 'payment_revenue_points_ratio_user', label: '普通用户分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_points_ratio_vip_1', label: '一级会员分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
{ key: 'payment_revenue_points_ratio_vip_2', label: '二级会员分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user