432 lines
14 KiB
Plaintext
432 lines
14 KiB
Plaintext
<template>
|
||
<view class="finance-transaction-stats">
|
||
<!-- 头部筛选 -->
|
||
<view class="header-filters">
|
||
<view class="date-tabs">
|
||
<text
|
||
v-for="(item, index) in dateOptions"
|
||
:key="index"
|
||
class="date-tab-item"
|
||
:class="{ active: activeDateTab === index }"
|
||
@click="handleDateTabChange(index)"
|
||
>{{ item }}</text>
|
||
</view>
|
||
<view class="date-picker-wrap">
|
||
<text class="calendar-icon">D</text>
|
||
<text class="date-range-text">{{ displayDateRange }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 交易概况区块 -->
|
||
<view class="overview-card">
|
||
<view class="overview-header">
|
||
<view class="header-left">
|
||
<text class="section-title">交易概况</text>
|
||
<text class="info-tag">?</text>
|
||
</view>
|
||
<view class="header-right">
|
||
<button class="btn-query" @click="loadData">刷新数据</button>
|
||
<button class="btn-export">导出报表</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 指标网格 -->
|
||
<view class="overview-grid">
|
||
<!-- 第一行 -->
|
||
<view class="grid-row">
|
||
<view class="overview-item">
|
||
<view class="icon-box blue"><text class="icon">🕒</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">营业额</text>
|
||
<text class="item-value">¥{{ stats.revenue }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">昨日对比:</text>
|
||
<text class="trend-value">{{ stats.revenueTrend }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box green"><text class="icon">¥</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">商品支付金额</text>
|
||
<text class="item-value">¥{{ stats.payAmount }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">交易笔数:</text>
|
||
<text class="trend-value">{{ stats.orderCount }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box orange"><text class="icon">🔒</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">购买会员金额</text>
|
||
<text class="item-value">¥{{ stats.memberAmount }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">占比:</text>
|
||
<text class="trend-value">-</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box purple"><text class="icon">💰</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">充值金额</text>
|
||
<text class="item-value">¥{{ stats.rechargeAmount }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">笔数:</text>
|
||
<text class="trend-value">{{ stats.rechargeCount }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box cyan"><text class="icon">🛒</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">线下收银金额</text>
|
||
<text class="item-value">¥{{ stats.offlineAmount }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">占比:</text>
|
||
<text class="trend-value">-</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 第二行 -->
|
||
<view class="grid-row second">
|
||
<view class="overview-item">
|
||
<view class="icon-box light-green"><text class="icon">↘</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">支出金额 (提现)</text>
|
||
<text class="item-value">¥{{ stats.expenditure }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">笔数:</text>
|
||
<text class="trend-value">{{ stats.extractCount }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box gold"><text class="icon">💳</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">全站余额存量</text>
|
||
<text class="item-value">¥{{ stats.balancePay }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">用户总数:</text>
|
||
<text class="trend-value">-</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box red-purple"><text class="icon">%</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">佣金总存量</text>
|
||
<text class="item-value">¥{{ stats.commissionPay }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">待结算:</text>
|
||
<text class="trend-value">-</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item">
|
||
<view class="icon-box blue-gray"><text class="icon">📦</text></view>
|
||
<view class="item-info">
|
||
<text class="item-label">商品退款金额</text>
|
||
<text class="item-value">¥{{ stats.refundAmount }}</text>
|
||
<view class="trend-row">
|
||
<text class="trend-label">退款率:</text>
|
||
<text class="trend-value">-</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="overview-item transparent"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 概况图表区 -->
|
||
<view class="overview-chart-section">
|
||
<view class="overview-chart-box">
|
||
<view v-if="loading" class="chart-loading"><text>统计加载中...</text></view>
|
||
<EChartsView v-else-if="overviewTrendOption != null" :option="overviewTrendOption" class="main-trend-chart" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, reactive, computed } from 'vue'
|
||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||
import { fetchFinanceOverview, fetchFinanceBillSummary } from '@/services/admin/financeService.uts'
|
||
import { fetchOrderStats } from '@/services/orderService.uts'
|
||
import AkReq from '@/uni_modules/ak-req/ak-req.uts'
|
||
|
||
const dateOptions = ['最近30天', '最近7天', '本月', '本年']
|
||
const activeDateTab = ref(0)
|
||
const loading = ref(false)
|
||
|
||
const stats = reactive({
|
||
revenue: '0.00',
|
||
payAmount: '0.00',
|
||
orderCount: '0',
|
||
memberAmount: '0.00',
|
||
rechargeAmount: '0.00',
|
||
rechargeCount: '0',
|
||
offlineAmount: '0.00',
|
||
expenditure: '0.00',
|
||
extractCount: '0',
|
||
balancePay: '0.00',
|
||
commissionPay: '0.00',
|
||
refundAmount: '0.00',
|
||
revenueTrend: '-'
|
||
})
|
||
|
||
const overviewTrendOption = ref<any>(null)
|
||
|
||
const displayDateRange = computed(() : string => {
|
||
const now = new Date()
|
||
const start = getStartTime()
|
||
return `${start.substring(0, 10)} - ${now.toISOString().substring(0, 10)}`
|
||
})
|
||
|
||
function getStartTime() : string {
|
||
const now = Date.now()
|
||
let days = 30
|
||
if (activeDateTab.value == 1) days = 7
|
||
if (activeDateTab.value == 2) days = 30 // 简化处理
|
||
if (activeDateTab.value == 3) days = 365
|
||
return new Date(now - days * 24 * 60 * 60 * 1000).toISOString()
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
const endTime = new Date().toISOString()
|
||
const startTime = getStartTime()
|
||
|
||
try {
|
||
// 1. 获取财务概况
|
||
const finRes = await fetchFinanceOverview(startTime, endTime)
|
||
if (finRes != null) {
|
||
stats.rechargeAmount = finRes.recharge_amount.toFixed(2)
|
||
stats.rechargeCount = String(finRes.recharge_count)
|
||
stats.expenditure = finRes.extract_amount.toFixed(2)
|
||
stats.extractCount = String(finRes.extract_count)
|
||
stats.balancePay = finRes.total_user_balance.toFixed(2)
|
||
stats.commissionPay = finRes.total_user_brokerage.toFixed(2)
|
||
}
|
||
|
||
// 2. 获取订单统计
|
||
const orderRes = await fetchOrderStats(startTime, endTime)
|
||
if (orderRes != null) {
|
||
stats.revenue = orderRes.total_amount.toFixed(2)
|
||
stats.payAmount = orderRes.total_amount.toFixed(2)
|
||
stats.orderCount = String(orderRes.order_count)
|
||
stats.refundAmount = orderRes.refund_amount.toFixed(2)
|
||
}
|
||
|
||
// 3. 获取趋势数据驱动图表
|
||
const trendRes = await fetchFinanceBillSummary(startTime, endTime, 'day')
|
||
updateChart(trendRes)
|
||
} catch (e) {
|
||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleDateTabChange(index : number) {
|
||
activeDateTab.value = index
|
||
loadData()
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (AkReq.getToken() == null || AkReq.getToken() === '') {
|
||
return
|
||
}
|
||
loadData()
|
||
})
|
||
|
||
function toPlainObject(obj: any): any {
|
||
if (obj == null) return null
|
||
if (typeof obj !== 'object') return obj
|
||
if (Array.isArray(obj)) {
|
||
return obj.map((item: any) : any => toPlainObject(item))
|
||
}
|
||
const plain: any = {}
|
||
const keys = Object.keys(obj)
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const key = keys[i]
|
||
const value = obj[key]
|
||
if (typeof value === 'function' || key.startsWith('_') || key === 'toJSON') {
|
||
continue
|
||
}
|
||
if (value != null && typeof value === 'object') {
|
||
plain[key] = toPlainObject(value)
|
||
} else {
|
||
plain[key] = value
|
||
}
|
||
}
|
||
return plain
|
||
}
|
||
|
||
function updateChart(data : any[]) {
|
||
const dates = data.map(item => item.date_group.substring(5))
|
||
const incomes = data.map(item => item.income)
|
||
const expenses = data.map(item => item.expense)
|
||
|
||
const option = {
|
||
grid: { left: '3%', right: '4%', bottom: '10%', top: '5%', containLabel: true },
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: ['收入', '支出'], bottom: 0 },
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: dates,
|
||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||
axisLabel: { color: '#999', fontSize: 10 }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLine: { show: false },
|
||
splitLine: { lineStyle: { color: '#f5f5f5' } },
|
||
axisLabel: { color: '#999' }
|
||
},
|
||
series: [
|
||
{
|
||
name: '收入',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: incomes,
|
||
itemStyle: { color: '#1890ff' },
|
||
areaStyle: { color: 'rgba(24, 144, 255, 0.1)' }
|
||
},
|
||
{
|
||
name: '支出',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: expenses,
|
||
itemStyle: { color: '#fa8c16' },
|
||
areaStyle: { color: 'rgba(250, 140, 22, 0.1)' }
|
||
}
|
||
]
|
||
}
|
||
overviewTrendOption.value = toPlainObject(option)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.finance-transaction-stats {
|
||
padding: 0;
|
||
background-color: transparent;
|
||
min-height: auto;
|
||
}
|
||
|
||
.header-filters {
|
||
background: #fff;
|
||
padding: 12px 20px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.date-tabs {
|
||
display: flex;
|
||
flex-direction: row;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 2px;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
.date-tab-item {
|
||
padding: 4px 15px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
border-right: 1px solid #d9d9d9;
|
||
cursor: pointer;
|
||
|
||
&:last-child { border-right: none; }
|
||
&.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||
}
|
||
|
||
.date-picker-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 4px 12px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.calendar-icon { margin-right: 8px; font-size: 14px; }
|
||
.date-range-text { font-size: 14px; color: #333; }
|
||
|
||
.overview-card {
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.overview-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header-left { display: flex; flex-direction: row; align-items: center; }
|
||
.section-title { font-size: 16px; font-weight: bold; color: #333; }
|
||
.info-tag {
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: #eee; color: #999; font-size: 11px;
|
||
display: flex; align-items: center; justify-content: center; margin-left: 8px;
|
||
}
|
||
|
||
.header-right { display: flex; flex-direction: row; gap: 12px; }
|
||
.btn-query, .btn-export {
|
||
padding: 0 16px; height: 32px; line-height: 32px;
|
||
font-size: 13px; border-radius: 4px; cursor: pointer;
|
||
}
|
||
.btn-query { background-color: #1890ff; color: #fff; border: none; }
|
||
.btn-export { background: #fff; color: #1890ff; border: 1px solid #1890ff; }
|
||
|
||
.overview-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 30px;
|
||
padding-bottom: 30px;
|
||
border-bottom: 1px dashed #f0f0f0;
|
||
}
|
||
|
||
.grid-row { display: flex; flex-direction: row; justify-content: space-between; }
|
||
.overview-item { flex: 1; display: flex; flex-direction: row; align-items: center; }
|
||
.overview-item.transparent { visibility: hidden; }
|
||
|
||
.icon-box {
|
||
width: 44px; height: 44px; border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center; margin-right: 12px;
|
||
text { color: #fff; font-size: 20px; }
|
||
}
|
||
|
||
.icon-box.blue { background-color: #2f54eb; }
|
||
.icon-box.green { background-color: #52c41a; }
|
||
.icon-box.orange { background-color: #fa8c16; }
|
||
.icon-box.purple { background-color: #722ed1; }
|
||
.icon-box.cyan { background-color: #13c2c2; }
|
||
.icon-box.light-green { background-color: #a0d911; }
|
||
.icon-box.gold { background-color: #faad14; }
|
||
.icon-box.red-purple { background-color: #eb2f96; }
|
||
.icon-box.blue-gray { background-color: #4096ff; }
|
||
|
||
.item-info { display: flex; flex-direction: column; }
|
||
.item-label { font-size: 13px; color: #999; margin-bottom: 4px; }
|
||
.item-value { font-size: 22px; font-weight: bold; color: #333; margin-bottom: 4px; }
|
||
.trend-row { display: flex; flex-direction: row; font-size: 12px; color: #999; }
|
||
.trend-value { margin-left: 4px; }
|
||
|
||
.overview-chart-section { padding-top: 24px; }
|
||
.overview-chart-box { width: 100%; height: 350px; }
|
||
.main-trend-chart { width: 100%; height: 100%; }
|
||
.chart-loading { height: 100%; display: flex; align-items: center; justify-content: center; color: #999; }
|
||
</style>
|