292 lines
9.4 KiB
Vue
292 lines
9.4 KiB
Vue
<template>
|
|
<PaymentSectionPage
|
|
section="revenue"
|
|
title="创作中心"
|
|
desc="查看现金与积分收益统计及结算运营入口。"
|
|
:links="[
|
|
{ label: 'VIP 会员', path: '/payment/vip' },
|
|
{ label: '推广返佣', path: '/payment/affiliate' },
|
|
{ label: '创作分成运营', path: '/payment/income' },
|
|
]"
|
|
>
|
|
<template #before-form>
|
|
<n-card class="aksrt-section-card" title="收益统计">
|
|
<n-grid :cols="4" :x-gap="12" :y-gap="12">
|
|
<n-grid-item v-for="item in revenueSummary" :key="item.key">
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ item.value }}</strong>
|
|
<p>{{ item.label }}</p>
|
|
</div>
|
|
</n-grid-item>
|
|
</n-grid>
|
|
</n-card>
|
|
|
|
<n-card class="aksrt-section-card" title="收益趋势">
|
|
<template #header-extra>
|
|
<n-space size="small">
|
|
<n-button :type="granularity === 'day' ? 'primary' : 'default'" size="tiny" quaternary @click="setGranularity('day')">日</n-button>
|
|
<n-button :type="granularity === 'week' ? 'primary' : 'default'" size="tiny" quaternary @click="setGranularity('week')">周</n-button>
|
|
<n-button :type="granularity === 'month' ? 'primary' : 'default'" size="tiny" quaternary @click="setGranularity('month')">月</n-button>
|
|
</n-space>
|
|
</template>
|
|
|
|
<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
|
|
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>
|
|
<n-empty v-else description="暂无趋势数据" />
|
|
</n-card>
|
|
|
|
<n-card class="aksrt-section-card" title="提现统计">
|
|
<n-grid :cols="6" :x-gap="12" :y-gap="12">
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.total }}</strong>
|
|
<p>提现笔数</p>
|
|
</div>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.pending }}</strong>
|
|
<p>待处理</p>
|
|
</div>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.processing }}</strong>
|
|
<p>处理中</p>
|
|
</div>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.completed }}</strong>
|
|
<p>已完成</p>
|
|
</div>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.rejected }}</strong>
|
|
<p>已拒绝</p>
|
|
</div>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<div class="aksrt-revenue__stat">
|
|
<strong>{{ withdrawalStats.total_paid.toFixed(2) }}</strong>
|
|
<p>已提现 (元)</p>
|
|
</div>
|
|
</n-grid-item>
|
|
</n-grid>
|
|
</n-card>
|
|
</template>
|
|
</PaymentSectionPage>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import PaymentSectionPage from '@/pages/payment/PaymentSectionPage.vue'
|
|
import api from '@/api'
|
|
|
|
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({ cash: [], points: [] })
|
|
const granularity = ref('day')
|
|
|
|
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: '积分收益笔数' },
|
|
])
|
|
|
|
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) {
|
|
return String(date || '').slice(5)
|
|
}
|
|
|
|
function setGranularity(nextGranularity) {
|
|
granularity.value = nextGranularity
|
|
loadRevenue()
|
|
}
|
|
|
|
async function loadRevenue() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
time_series: '1',
|
|
granularity: granularity.value,
|
|
})
|
|
const res = await api.request(`/payment/revenue?${params}`)
|
|
if (res?.data) {
|
|
if (res.data.revenue) {
|
|
revenueStats.value = {
|
|
cash: { ...emptyCurrencyStats(), ...(res.data.revenue.cash || {}) },
|
|
points: { ...emptyCurrencyStats(), ...(res.data.revenue.points || {}) },
|
|
}
|
|
}
|
|
if (res.data.withdrawal) {
|
|
withdrawalStats.value = {
|
|
total: Number(res.data.withdrawal.total || 0),
|
|
pending: Number(res.data.withdrawal.pending || 0),
|
|
processing: Number(res.data.withdrawal.processing || 0),
|
|
completed: Number(res.data.withdrawal.completed || 0),
|
|
rejected: Number(res.data.withdrawal.rejected || 0),
|
|
total_paid: Number(res.data.withdrawal.total_paid || 0),
|
|
}
|
|
}
|
|
timeSeries.value = {
|
|
cash: res.data.time_series?.cash || [],
|
|
points: res.data.time_series?.points || [],
|
|
}
|
|
}
|
|
} catch {
|
|
// silent
|
|
}
|
|
}
|
|
|
|
onMounted(loadRevenue)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aksrt-revenue__stat {
|
|
text-align: center;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
background: var(--surface-2, #fafafa);
|
|
}
|
|
|
|
.aksrt-revenue__stat strong {
|
|
display: block;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: var(--aksrt-color-text);
|
|
}
|
|
|
|
.aksrt-revenue__stat p {
|
|
margin: 4px 0 0;
|
|
font-size: 12px;
|
|
color: var(--aksrt-color-muted);
|
|
}
|
|
|
|
.aksrt-revenue__charts {
|
|
display: grid;
|
|
gap: 24px;
|
|
}
|
|
|
|
.aksrt-revenue__chart {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.aksrt-revenue__chart h4 {
|
|
margin: 0 0 10px;
|
|
color: var(--aksrt-color-text);
|
|
}
|
|
|
|
.aksrt-revenue__chart-bars {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 2px;
|
|
height: 180px;
|
|
padding: 0 4px;
|
|
border-bottom: 1px solid var(--color-line, #e5e7eb);
|
|
}
|
|
|
|
.aksrt-revenue__chart-bar-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
height: 100%;
|
|
min-width: 0;
|
|
cursor: default;
|
|
}
|
|
|
|
.aksrt-revenue__chart-bar {
|
|
width: 100%;
|
|
max-width: 32px;
|
|
min-height: 2px;
|
|
background: var(--color-primary, #88abda);
|
|
border-radius: 3px 3px 0 0;
|
|
transition: height 0.3s ease;
|
|
}
|
|
|
|
.aksrt-revenue__chart-bar-wrap:hover .aksrt-revenue__chart-bar {
|
|
background: var(--color-primary-hover, #7099cc);
|
|
}
|
|
|
|
.aksrt-revenue__chart-label {
|
|
font-size: 10px;
|
|
color: var(--aksrt-color-muted);
|
|
margin-top: 4px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.aksrt-revenue__chart-summary {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 12px;
|
|
font-size: 12px;
|
|
color: var(--color-ink-3, #525866);
|
|
}
|
|
|
|
.aksrt-revenue__chart-summary strong {
|
|
color: var(--aksrt-color-text);
|
|
font-weight: 600;
|
|
}
|
|
</style>
|