Files
hfb_sys/frontend/src/features/admin/views/AdminFinanceDashboardView.vue
T

747 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import {
Calendar,
Coin,
CreditCard,
DocumentChecked,
Money,
Refresh,
Search,
ShoppingCart,
Timer,
TrendCharts,
Van,
Wallet,
WarningFilled,
} from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import {
fetchFinanceDashboard,
SETTLEMENT_DIFF_THRESHOLD_CENT,
type FinanceDashboard,
} from '@/features/admin/api/adminFinance'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { formatDateTime, formatInputDate } from '@/shared/utils/time'
const loading = ref(false)
const dashboard = ref<FinanceDashboard | null>(null)
const activePreset = ref<'today' | '7d' | '30d' | 'this_month' | 'custom'>('7d')
const filters = reactive({
start_date: defaultStartDate(),
end_date: defaultEndDate(),
})
const dailyItems = computed(() => dashboard.value?.daily_items ?? [])
onMounted(loadDashboard)
async function loadDashboard() {
loading.value = true
try {
dashboard.value = await fetchFinanceDashboard(filters)
} finally {
loading.value = false
}
}
function handleDateChange() {
activePreset.value = 'custom'
loadDashboard()
}
function selectPreset(preset: 'today' | '7d' | '30d' | 'this_month') {
activePreset.value = preset
const now = new Date()
const todayStr = formatInputDate(now)
if (preset === 'today') {
filters.start_date = todayStr
filters.end_date = todayStr
} else if (preset === '7d') {
const d = new Date()
d.setDate(d.getDate() - 6)
filters.start_date = formatInputDate(d)
filters.end_date = todayStr
} else if (preset === '30d') {
const d = new Date()
d.setDate(d.getDate() - 29)
filters.start_date = formatInputDate(d)
filters.end_date = todayStr
} else if (preset === 'this_month') {
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1)
filters.start_date = formatInputDate(firstDay)
filters.end_date = todayStr
}
loadDashboard()
}
function moneyCent(value: number | undefined | null) {
return formatCentWithSymbol(value || 0)
}
function defaultStartDate() {
const date = new Date()
date.setDate(date.getDate() - 6)
return formatInputDate(date)
}
function defaultEndDate() {
return formatInputDate(new Date())
}
function diffType(value: number | undefined | null) {
return Math.abs(Number(value || 0)) >= SETTLEMENT_DIFF_THRESHOLD_CENT ? 'danger' : 'success'
}
</script>
<template>
<div class="finance-dashboard-page">
<!-- 顶部导航与控制栏 -->
<header class="dashboard-header">
<div class="header-main">
<div class="title-group">
<div class="eyebrow-tag">
<el-icon><TrendCharts /></el-icon>
<span>FINANCE OVERVIEW</span>
</div>
<h1 class="page-title">财务仪表盘</h1>
<p class="page-subtitle">
聚合分析全站资金流水、平台利润收益、号主结算账单及各项出款待办。
</p>
</div>
<div class="header-meta" v-if="dashboard">
<span class="meta-item">
<el-icon><Timer /></el-icon>
数据生成:{{ formatDateTime(dashboard.generated_at) }}
</span>
</div>
</div>
<!-- 筛选工具栏 -->
<div class="filter-toolbar">
<div class="preset-buttons">
<button
type="button"
class="preset-btn"
:class="{ active: activePreset === 'today' }"
@click="selectPreset('today')"
>
今日
</button>
<button
type="button"
class="preset-btn"
:class="{ active: activePreset === '7d' }"
@click="selectPreset('7d')"
>
近 7 天
</button>
<button
type="button"
class="preset-btn"
:class="{ active: activePreset === '30d' }"
@click="selectPreset('30d')"
>
近 30 天
</button>
<button
type="button"
class="preset-btn"
:class="{ active: activePreset === 'this_month' }"
@click="selectPreset('this_month')"
>
本月
</button>
</div>
<div class="date-picker-group">
<el-date-picker
v-model="filters.start_date"
type="date"
value-format="YYYY-MM-DD"
placeholder="开始日期"
:clearable="false"
class="date-input"
@change="handleDateChange"
/>
<span class="date-separator">至</span>
<el-date-picker
v-model="filters.end_date"
type="date"
value-format="YYYY-MM-DD"
placeholder="结束日期"
:clearable="false"
class="date-input"
@change="handleDateChange"
/>
</div>
<div class="action-buttons">
<el-button type="primary" :icon="Search" :loading="loading" @click="loadDashboard">
查询
</el-button>
<el-button :icon="Refresh" :loading="loading" @click="loadDashboard"> 刷新 </el-button>
</div>
</div>
</header>
<!-- 核心 KPI 看板 (Hero Cards) -->
<section v-if="dashboard" class="hero-kpi-grid">
<!-- 平台总流水 -->
<div class="hero-card hero-card--flow">
<div class="hero-card-header">
<div class="hero-icon-box">
<el-icon><Money /></el-icon>
</div>
<span class="hero-label">平台总流水</span>
</div>
<div class="hero-value">{{ moneyCent(dashboard.summary.total_flow_amount_cent) }}</div>
<div class="hero-footer">
<span class="hero-pill success">
{{ dashboard.summary.successful_pay_count }} 笔收款
</span>
<span class="hero-note">含所有成功支付金额</span>
</div>
</div>
<!-- 平台利润 -->
<div class="hero-card hero-card--profit">
<div class="hero-card-header">
<div class="hero-icon-box">
<el-icon><TrendCharts /></el-icon>
</div>
<span class="hero-label">平台净利润</span>
</div>
<div class="hero-value">{{ moneyCent(dashboard.summary.platform_income_amount_cent) }}</div>
<div class="hero-footer">
<span class="hero-pill emerald">
普通 {{ dashboard.summary.settled_order_count }} 笔 ·
{{ moneyCent(dashboard.summary.normal_order_profit_amount_cent) }}
</span>
<span class="hero-pill teal">
提号 {{ dashboard.summary.pickup_completed_count }} 笔 ·
{{ moneyCent(dashboard.summary.pickup_profit_amount_cent) }}
</span>
</div>
</div>
<!-- 渠道净流入 -->
<div class="hero-card hero-card--net">
<div class="hero-card-header">
<div class="hero-icon-box">
<el-icon><Wallet /></el-icon>
</div>
<span class="hero-label">渠道净流入</span>
</div>
<div class="hero-value">{{ moneyCent(dashboard.summary.channel_net_amount_cent) }}</div>
<div class="hero-footer">
<span class="hero-note">成功收款 − 成功退款</span>
</div>
</div>
<!-- 全部实际出款 -->
<div v-if="dashboard.disbursement_summary" class="hero-card hero-card--outflow">
<div class="hero-card-header">
<div class="hero-icon-box">
<el-icon><CreditCard /></el-icon>
</div>
<span class="hero-label">实际出款合计</span>
</div>
<div class="hero-value">
{{ moneyCent(dashboard.disbursement_summary.paid_amount_cent) }}
</div>
<div class="hero-footer">
<span class="hero-pill purple">
{{ dashboard.disbursement_summary.paid_count }} 笔已打款
</span>
<span class="hero-note">全口径资金出款</span>
</div>
</div>
<!-- 结算差异与异常 -->
<div class="hero-card hero-card--risk">
<div class="hero-card-header">
<div class="hero-icon-box">
<el-icon><WarningFilled /></el-icon>
</div>
<span class="hero-label">结算差异监控</span>
</div>
<div
class="hero-value diff-value"
:class="diffType(dashboard.summary.settlement_diff_amount_cent)"
>
{{ moneyCent(dashboard.summary.settlement_diff_amount_cent) }}
</div>
<div class="hero-footer">
<span
class="hero-pill"
:class="dashboard.summary.financial_exception_count > 0 ? 'danger' : 'neutral'"
>
{{ dashboard.summary.financial_exception_count }} 笔异常订单
</span>
<span class="hero-pill warning">
待结算 {{ dashboard.summary.pending_settle_order_count }} 单
</span>
</div>
</div>
</section>
<!-- 模块化内容区块 -->
<div v-if="dashboard" class="dashboard-sections">
<!-- 平台收支与号主结算 -->
<div class="section-container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-badge primary">
<el-icon><Coin /></el-icon>
</div>
<div>
<h2 class="section-title">平台收支与号主结算</h2>
<p class="section-desc">退款监控、号主应得结算口径及平台预期收益</p>
</div>
</div>
</div>
<div class="sub-metric-grid">
<div class="sub-card">
<div class="sub-card-label">总退款金额</div>
<div class="sub-card-value danger-text">
{{ moneyCent(dashboard.summary.total_refund_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>全额 {{ dashboard.summary.full_refund_count }} 笔</span>
<span class="dot-sep">·</span>
<span>部分 {{ dashboard.summary.partial_refund_count }} 笔</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">退款处理中</div>
<div class="sub-card-value warning-text">
{{ moneyCent(dashboard.summary.pending_refund_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.summary.pending_refund_count }} 笔待渠道回执</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">预计后续利润</div>
<div class="sub-card-value primary-text">
{{ moneyCent(dashboard.summary.estimated_income_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.summary.pending_settle_order_count }} 个未完结待结算订单</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">号主应得金额</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.summary.owner_should_income_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>结账单核算口径</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">号主实际结算总额</div>
<div class="sub-card-value success-text">
{{ moneyCent(dashboard.summary.owner_effective_settlement_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>钱包已入账 + 平台代管应付</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">区间待线下结算</div>
<div class="sub-card-value warning-text">
{{ moneyCent(dashboard.summary.offline_settlement_pending_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.summary.offline_settlement_pending_count }} 笔平台代管待结算</span>
</div>
</div>
</div>
</div>
<!-- 资金出款与待办审批 -->
<div v-if="dashboard.disbursement_summary" class="section-container">
<div class="section-header">
<div class="section-title-wrap">
<div class="section-badge teal">
<el-icon><CreditCard /></el-icon>
</div>
<div>
<h2 class="section-title">资金出款与待办流程</h2>
<p class="section-desc">已打款统计按所选区间计算,待处理出款为当前全局待办</p>
</div>
</div>
</div>
<div class="disbursement-layout">
<!-- 已出款项 -->
<div class="disbursement-subgroup">
<div class="subgroup-title">
<span class="status-indicator success-dot"></span>
<span>区间已完成出款</span>
</div>
<div class="sub-metric-grid cols-2">
<div class="sub-card">
<div class="sub-card-label">平台代管打款</div>
<div class="sub-card-value">
{{
moneyCent(dashboard.disbursement_summary.offline_settlement_paid_amount_cent)
}}
</div>
<div class="sub-card-sub">
<span
>{{
dashboard.disbursement_summary.offline_settlement_paid_count
}}
笔线下打款</span
>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">用户提现打款</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.disbursement_summary.withdrawal_paid_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.disbursement_summary.withdrawal_paid_count }} 笔</span>
<span class="dot-sep">·</span>
<span
>扣费
{{ moneyCent(dashboard.disbursement_summary.withdrawal_fee_amount_cent) }}</span
>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">其他线下出款</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.disbursement_summary.manual_paid_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.disbursement_summary.manual_paid_count }} 笔手工记账</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">日常运营开支</div>
<div class="sub-card-value orange-text">
{{ moneyCent(dashboard.operating_expense_summary.amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.operating_expense_summary.count }} 笔有效记账</span>
</div>
</div>
</div>
</div>
<!-- 待办审批项 -->
<div class="disbursement-subgroup pending-group">
<div class="subgroup-title">
<span class="status-indicator warning-dot"></span>
<span>待办审批与待打款</span>
</div>
<div class="sub-metric-grid cols-2">
<div class="sub-card highlight-pending">
<div class="sub-card-label">当前待出款总额</div>
<div class="sub-card-value warning-text">
{{ moneyCent(dashboard.disbursement_summary.pending_payment_amount_cent) }}
</div>
<div class="sub-card-sub">
<span
>{{
dashboard.disbursement_summary.pending_payment_count
}}
笔已进入打款阶段</span
>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">代管待打款</div>
<div class="sub-card-value">
{{
moneyCent(dashboard.disbursement_summary.offline_settlement_pending_amount_cent)
}}
</div>
<div class="sub-card-sub">
<span
>{{
dashboard.disbursement_summary.offline_settlement_pending_count
}}
笔代管待打款</span
>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">提现待打款</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.disbursement_summary.withdrawal_pending_amount_cent) }}
</div>
<div class="sub-card-sub">
<span
>{{
dashboard.disbursement_summary.withdrawal_pending_count
}}
笔已审核待放款</span
>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">提现待审核</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.disbursement_summary.withdrawal_review_amount_cent) }}
</div>
<div class="sub-card-sub">
<span
>{{ dashboard.disbursement_summary.withdrawal_review_count }} 笔提现待初审</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 专项业务分部:撞车商城与线下提号 -->
<div class="segments-row">
<!-- 撞车商城 -->
<div
v-if="dashboard.mohong_summary"
class="section-container segment-container mohong-segment"
>
<div class="section-header">
<div class="section-title-wrap">
<div class="section-badge blue">
<el-icon><ShoppingCart /></el-icon>
</div>
<div>
<h2 class="section-title">撞车商城</h2>
<p class="section-desc">商城交易流水独立核算,不计入平台利润</p>
</div>
</div>
</div>
<div class="sub-metric-grid cols-3">
<div class="sub-card">
<div class="sub-card-label">商城成交额</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.mohong_summary.flow_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.mohong_summary.paid_order_count }} 笔成功订单</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">商城退款</div>
<div class="sub-card-value danger-text">
{{ moneyCent(dashboard.mohong_summary.refund_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>已完成退款</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">商城净流入</div>
<div class="sub-card-value success-text">
{{ moneyCent(dashboard.mohong_summary.net_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>成交额 − 退款</span>
</div>
</div>
</div>
</div>
<!-- 线下提号 -->
<div
v-if="dashboard.pickup_summary"
class="section-container segment-container pickup-segment"
>
<div class="section-header">
<div class="section-title-wrap">
<div class="section-badge purple">
<el-icon><Van /></el-icon>
</div>
<div>
<h2 class="section-title">线下提号</h2>
<p class="section-desc">电商平台线下提号结算与手工利润汇总</p>
</div>
</div>
</div>
<div class="sub-metric-grid cols-3">
<div class="sub-card">
<div class="sub-card-label">线下提号结算</div>
<div class="sub-card-value">
{{ moneyCent(dashboard.pickup_summary.settled_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>{{ dashboard.pickup_summary.completed_count }} 笔已完成结算</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">线下提号利润</div>
<div class="sub-card-value emerald-text">
{{ moneyCent(dashboard.pickup_summary.profit_amount_cent) }}
</div>
<div class="sub-card-sub">
<span>电商平台手工录入</span>
</div>
</div>
<div class="sub-card">
<div class="sub-card-label">进行中提号</div>
<div class="sub-card-value warning-text">
{{ dashboard.pickup_summary.in_progress_count }}
</div>
<div class="sub-card-sub">
<span>当前进行中笔数</span>
</div>
</div>
</div>
</div>
</div>
<!-- 每日财务明细表格 -->
<div class="section-container table-container">
<div class="section-header table-section-header">
<div class="section-title-wrap">
<div class="section-badge indigo">
<el-icon><DocumentChecked /></el-icon>
</div>
<div>
<h2 class="section-title">每日财务明细</h2>
<p class="section-desc">展示所选时间范围内每一天的资金与订单明细数据</p>
</div>
</div>
<div class="table-actions">
<span class="record-badge">共 {{ dailyItems.length }} 天明细</span>
</div>
</div>
<div class="table-wrapper">
<el-table
v-loading="loading"
class="custom-finance-table"
:data="dailyItems"
stripe
highlight-current-row
empty-text="所选区间内暂无财务数据"
>
<el-table-column prop="date" label="日期" width="130" fixed="left">
<template #default="{ row }">
<div class="date-cell">
<el-icon><Calendar /></el-icon>
<span>{{ row.date }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="平台利润" min-width="140">
<template #default="{ row }">
<span class="amount-cell profit-cell">
{{ moneyCent(row.platform_income_amount_cent) }}
</span>
</template>
</el-table-column>
<el-table-column label="总流水" min-width="140">
<template #default="{ row }">
<span class="amount-cell flow-cell">
{{ moneyCent(row.total_flow_amount_cent) }}
</span>
</template>
</el-table-column>
<el-table-column label="总退款" min-width="130">
<template #default="{ row }">
<span
class="amount-cell"
:class="{ 'refund-cell': (row.total_refund_amount_cent || 0) > 0 }"
>
{{ moneyCent(row.total_refund_amount_cent) }}
</span>
</template>
</el-table-column>
<el-table-column label="实际出款" min-width="130">
<template #default="{ row }">
<span class="amount-cell outflow-cell">
{{ moneyCent(row.disbursement_paid_amount_cent) }}
</span>
</template>
</el-table-column>
<el-table-column label="运营开支" min-width="120">
<template #default="{ row }">
<span class="amount-cell expense-cell">
{{ moneyCent(row.operating_expense_amount_cent) }}
</span>
</template>
</el-table-column>
<el-table-column label="哈夫币结算" width="110" align="center">
<template #default="{ row }">
<span class="count-pill">{{ row.settled_order_count }}</span>
</template>
</el-table-column>
<el-table-column label="线下提号结算" width="120" align="center">
<template #default="{ row }">
<span class="count-pill">{{ row.pickup_completed_count }}</span>
</template>
</el-table-column>
<el-table-column label="已结算成交数" width="120" align="center">
<template #default="{ row }">
<span class="count-pill">{{ row.sales_count }}</span>
</template>
</el-table-column>
<el-table-column label="已付款订单数" width="120" align="center">
<template #default="{ row }">
<span class="count-pill primary-pill">{{ row.paid_order_count }}</span>
</template>
</el-table-column>
<el-table-column label="退款 / 取消" min-width="160">
<template #default="{ row }">
<div class="refund-cancel-tag-group">
<span v-if="row.normal_full_refund_count > 0" class="mini-tag refund-tag">
全退 {{ row.normal_full_refund_count }}
</span>
<span v-else class="mini-tag muted-tag">全退 0</span>
<span v-if="row.pickup_cancelled_count > 0" class="mini-tag cancel-tag">
取消提号 {{ row.pickup_cancelled_count }}
</span>
<span v-else class="mini-tag muted-tag">取消 0</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</template>
<style scoped src="./AdminFinanceDashboardView.css"></style>