feat(payment): separate creator income settlement
This commit is contained in:
1 parent
cd93ea1656
commit
e20418e909
17 files changed
+451
-445
No files matched your search
@@ -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 } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -710,8 +710,47 @@
|
||||
}
|
||||
|
||||
/* ==================== 作者收益中心 ==================== */
|
||||
.creator-income { display:flex; flex-direction:column; gap:16px; }.creator-income__hero { padding:24px 26px; border-radius:var(--radius-lg); color:#fff; background:linear-gradient(135deg,var(--color-primary),#7c3aed); }.creator-income__hero p { margin:0 0 6px; opacity:.82; font-size:13px; }.creator-income__hero h2 { margin:0; color:inherit; }.creator-income__tabs { display:flex; gap:8px; flex-wrap:wrap; }.creator-income__tab { padding:7px 12px; border-radius:var(--radius-pill); color:var(--color-ink-3); text-decoration:none; }.creator-income__tab.is-active,.creator-income__tab:hover { color:var(--color-primary); background:var(--color-primary-soft); }.creator-income__cards { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }.creator-income-card { padding:18px; border:1px solid var(--color-line); border-radius:var(--radius-lg); background:var(--surface-1); }.creator-income-card h3 { margin:0 0 14px; }.creator-income-card__metrics { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; }.creator-income-card__metrics strong,.creator-income-card__metrics small { display:block; }.creator-income-card__metrics strong { color:var(--color-ink); font-size:18px; }.creator-income-card__metrics small { margin-top:4px; color:var(--color-ink-4); font-size:11px; }.creator-income__info,.creator-income__details { padding:20px; }.creator-income__info h3,.creator-income__toolbar h3 { margin:0; }.creator-income__info div,.creator-income__toolbar p { margin:6px 0 0; color:var(--color-ink-4); font-size:13px; }.creator-income__toolbar { display:flex; justify-content:space-between; align-items:flex-end; gap:12px; margin-bottom:14px; }.creator-income__table-wrap { overflow-x:auto; }.creator-income__table { width:100%; border-collapse:collapse; font-size:12px; }.creator-income__table th,.creator-income__table td { padding:10px 8px; border-bottom:1px solid var(--color-line); text-align:left; white-space:nowrap; }.creator-income-status { padding:3px 7px; border-radius:var(--radius-pill); color:var(--color-primary); background:var(--color-primary-soft); }.creator-income-status--cancelled,.creator-income-status--reversed { color:var(--color-ink-4); background:var(--surface-2); }.creator-income-status--pending_recovery { color:var(--color-error); background:rgba(220,38,38,.08); }.creator-income__empty { padding:40px 20px; text-align:center; color:var(--color-ink-4); }.creator-income__empty i { font-size:28px; color:var(--color-primary); } .creator-income__details select { margin-left:6px; }
|
||||
@media (max-width:767px) { .creator-income__cards { grid-template-columns:1fr; }.creator-income-card__metrics { grid-template-columns:repeat(2,minmax(0,1fr)); }.creator-income__toolbar { align-items:flex-start; flex-direction:column; } }
|
||||
.creator-income { display: flex; flex-direction: column; gap: 16px; }
|
||||
.creator-income__cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.creator-income-card { padding: 18px; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--surface-1); }
|
||||
.creator-income-card h2 { margin: 0 0 14px; font-size: 17px; }
|
||||
.creator-income-card--cny h2 { color: var(--color-primary); }
|
||||
.creator-income-card--points h2 { color: #16816c; }
|
||||
.creator-income-card__metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
|
||||
.creator-income-card__metrics strong,
|
||||
.creator-income-card__metrics small { display: block; }
|
||||
.creator-income-card__metrics strong { color: var(--color-ink); font-size: 18px; font-variant-numeric: tabular-nums; }
|
||||
.creator-income-card__metrics small { margin-top: 4px; color: var(--color-ink-4); font-size: 11px; }
|
||||
.creator-income__info { padding: 20px; }
|
||||
.creator-income__info h2,
|
||||
.creator-income__toolbar h2 { margin: 0; font-size: 16px; }
|
||||
.creator-income__info div,
|
||||
.creator-income__toolbar p { margin: 6px 0 0; color: var(--color-ink-4); font-size: 13px; line-height: 1.7; }
|
||||
.creator-income__details { padding: 4px 0 0; }
|
||||
.creator-income__toolbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.creator-income__list { border-top: 1px solid var(--color-line); }
|
||||
.creator-income-row { display: grid; grid-template-columns: minmax(110px, .8fr) minmax(0, 1fr) auto; align-items: center; gap: 14px; min-width: 0; padding: 14px 0; border-bottom: 1px solid var(--color-line); }
|
||||
.creator-income-row__currency { color: var(--color-ink-3); font-size: 13px; }
|
||||
.creator-income-row__amount { min-width: 0; overflow: hidden; color: #16816c; font-size: 15px; font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.creator-income-row__status { justify-self: end; padding: 3px 8px; border-radius: var(--radius-pill); color: var(--color-ink-4); background: var(--surface-2); font-size: 11px; white-space: nowrap; }
|
||||
.creator-income-row__status--settled { color: #16816c; background: rgba(22, 129, 108, .1); }
|
||||
.creator-income-row__status--pending { color: #a86d28; background: rgba(168, 109, 40, .12); }
|
||||
.creator-income__pagination { display: flex; justify-content: center; gap: 6px; padding-top: 16px; }
|
||||
.creator-income__pagination a { display: grid; width: 30px; height: 30px; place-items: center; border-radius: 6px; color: var(--color-ink-3); background: var(--surface-2); font-size: 12px; text-decoration: none; }
|
||||
.creator-income__pagination a.is-active,
|
||||
.creator-income__pagination a:hover { color: #fff; background: var(--color-primary); }
|
||||
.creator-income__empty { padding: 40px 20px; color: var(--color-ink-4); text-align: center; }
|
||||
.creator-income__empty i { display: block; margin-bottom: 8px; color: var(--color-primary); font-size: 28px; }
|
||||
@media (max-width: 767px) {
|
||||
.creator-income__cards { grid-template-columns: 1fr; }
|
||||
.creator-income-card__metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.creator-income-row { grid-template-columns: minmax(82px, .8fr) minmax(0, 1fr) auto; gap: 8px; }
|
||||
.creator-income-row__currency,
|
||||
.creator-income-row__amount { font-size: 13px; }
|
||||
.creator-income-row__status { padding-inline: 6px; }
|
||||
}
|
||||
|
||||
/* ==================== 独立创作中心工作台 ==================== */
|
||||
.creator-workspace { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
var url = restUrl.replace(/\/$/, '') + '/user-center/panel/' + encodeURIComponent(name);
|
||||
if (name === 'creator') {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
['income_currency', 'income_status', 'income_page'].forEach(function(key) {
|
||||
['income_page'].forEach(function(key) {
|
||||
if (params.get(key)) url += (url.indexOf('?') === -1 ? '?' : '&') + key + '=' + encodeURIComponent(params.get(key));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user