feat: deepen report metrics
This commit is contained in:
@@ -3,9 +3,11 @@ import SearchField from '@/components/SearchField.vue';
|
||||
import SearchPanel from '@/components/SearchPanel.vue';
|
||||
import { FinanceService } from '@/service/FinanceService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
|
||||
const withdrawals = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -37,6 +39,24 @@ const statusOptions = [
|
||||
{ label: 'failed', value: 'failed' }
|
||||
];
|
||||
|
||||
function getQueryValue(value) {
|
||||
if (Array.isArray(value)) return value[0];
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
function parseNumber(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseDate(value) {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
const approveDialogVisible = ref(false);
|
||||
const approveLoading = ref(false);
|
||||
const approveOrder = ref(null);
|
||||
@@ -109,7 +129,7 @@ function onSearch() {
|
||||
loadWithdrawals();
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
function resetFilters() {
|
||||
orderID.value = null;
|
||||
tenantID.value = null;
|
||||
tenantCode.value = '';
|
||||
@@ -127,6 +147,44 @@ function onReset() {
|
||||
sortOrder.value = -1;
|
||||
page.value = 1;
|
||||
rows.value = 10;
|
||||
}
|
||||
|
||||
function applyRouteQuery(query) {
|
||||
resetFilters();
|
||||
|
||||
const idValue = getQueryValue(query?.id);
|
||||
const tenantValue = getQueryValue(query?.tenant_id);
|
||||
const userValue = getQueryValue(query?.user_id);
|
||||
if (idValue) orderID.value = parseNumber(idValue);
|
||||
if (tenantValue) tenantID.value = parseNumber(tenantValue);
|
||||
if (userValue) userID.value = parseNumber(userValue);
|
||||
|
||||
const statusValue = getQueryValue(query?.status);
|
||||
const tenantCodeValue = getQueryValue(query?.tenant_code);
|
||||
const tenantNameValue = getQueryValue(query?.tenant_name);
|
||||
const usernameValue = getQueryValue(query?.username);
|
||||
if (statusValue !== null) status.value = String(statusValue);
|
||||
if (tenantCodeValue !== null) tenantCode.value = String(tenantCodeValue);
|
||||
if (tenantNameValue !== null) tenantName.value = String(tenantNameValue);
|
||||
if (usernameValue !== null) username.value = String(usernameValue);
|
||||
|
||||
const createdFromValue = getQueryValue(query?.created_at_from);
|
||||
const createdToValue = getQueryValue(query?.created_at_to);
|
||||
const paidFromValue = getQueryValue(query?.paid_at_from);
|
||||
const paidToValue = getQueryValue(query?.paid_at_to);
|
||||
if (createdFromValue) createdAtFrom.value = parseDate(createdFromValue);
|
||||
if (createdToValue) createdAtTo.value = parseDate(createdToValue);
|
||||
if (paidFromValue) paidAtFrom.value = parseDate(paidFromValue);
|
||||
if (paidToValue) paidAtTo.value = parseDate(paidToValue);
|
||||
|
||||
const amountMinValue = getQueryValue(query?.amount_paid_min);
|
||||
const amountMaxValue = getQueryValue(query?.amount_paid_max);
|
||||
if (amountMinValue) amountPaidMin.value = parseNumber(amountMinValue);
|
||||
if (amountMaxValue) amountPaidMax.value = parseNumber(amountMaxValue);
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
resetFilters();
|
||||
loadWithdrawals();
|
||||
}
|
||||
|
||||
@@ -185,7 +243,14 @@ async function confirmReject() {
|
||||
}
|
||||
}
|
||||
|
||||
loadWithdrawals();
|
||||
watch(
|
||||
() => route.query,
|
||||
(query) => {
|
||||
applyRouteQuery(query);
|
||||
loadWithdrawals();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -24,6 +24,12 @@ const orderChartData = ref(null);
|
||||
const amountChartData = ref(null);
|
||||
const orderChartOptions = ref(null);
|
||||
const amountChartOptions = ref(null);
|
||||
const withdrawOrderChartData = ref(null);
|
||||
const withdrawAmountChartData = ref(null);
|
||||
const withdrawOrderChartOptions = ref(null);
|
||||
const withdrawAmountChartOptions = ref(null);
|
||||
const interactionChartData = ref(null);
|
||||
const interactionChartOptions = ref(null);
|
||||
|
||||
const granularityOptions = [{ label: '按天', value: 'day' }];
|
||||
|
||||
@@ -63,9 +69,40 @@ function goToOrders(status, usePaidAt) {
|
||||
router.push({ name: 'superadmin-orders', query: buildOrdersQuery({ status, usePaidAt }) });
|
||||
}
|
||||
|
||||
function goToContents() {
|
||||
function buildWithdrawalsQuery({ status, usePaidAt } = {}) {
|
||||
const query = {};
|
||||
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||
if (status) query.status = status;
|
||||
const start = formatDateParam(startAt.value);
|
||||
const end = formatDateParam(endAt.value);
|
||||
if (usePaidAt) {
|
||||
if (start) query.paid_at_from = start;
|
||||
if (end) query.paid_at_to = end;
|
||||
} else {
|
||||
if (start) query.created_at_from = start;
|
||||
if (end) query.created_at_to = end;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
function goToWithdrawals(status, usePaidAt) {
|
||||
router.push({ name: 'superadmin-finance', query: buildWithdrawalsQuery({ status, usePaidAt }) });
|
||||
}
|
||||
|
||||
function buildContentsQuery({ useCreatedAt } = {}) {
|
||||
const query = {};
|
||||
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||
if (useCreatedAt) {
|
||||
const start = formatDateParam(startAt.value);
|
||||
const end = formatDateParam(endAt.value);
|
||||
if (start) query.created_at_from = start;
|
||||
if (end) query.created_at_to = end;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
function goToContents(extraQuery = {}) {
|
||||
const query = { ...buildContentsQuery(), ...extraQuery };
|
||||
router.push({ name: 'superadmin-contents', query });
|
||||
}
|
||||
|
||||
@@ -127,6 +164,129 @@ function buildAmountChartData(items) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildWithdrawOrderChartData(items) {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const labels = items.map((item) => item.date);
|
||||
const applyOrders = items.map((item) => Number(item.withdrawal_apply_orders ?? 0));
|
||||
const paidOrders = items.map((item) => Number(item.withdrawal_paid_orders ?? 0));
|
||||
const failedOrders = items.map((item) => Number(item.withdrawal_failed_orders ?? 0));
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '提现申请',
|
||||
data: applyOrders,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '提现完成',
|
||||
data: paidOrders,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '提现失败',
|
||||
data: failedOrders,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
|
||||
tension: 0.35
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildWithdrawAmountChartData(items) {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const labels = items.map((item) => item.date);
|
||||
const applyAmounts = items.map((item) => Number(item.withdrawal_apply_amount ?? 0));
|
||||
const paidAmounts = items.map((item) => Number(item.withdrawal_paid_amount ?? 0));
|
||||
const failedAmounts = items.map((item) => Number(item.withdrawal_failed_amount ?? 0));
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '申请金额',
|
||||
data: applyAmounts,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '完成金额',
|
||||
data: paidAmounts,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '失败金额',
|
||||
data: failedAmounts,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
|
||||
tension: 0.35
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildInteractionChartData(items) {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const labels = items.map((item) => item.date);
|
||||
const contentCreated = items.map((item) => Number(item.content_created ?? 0));
|
||||
const likeActions = items.map((item) => Number(item.like_actions ?? 0));
|
||||
const favoriteActions = items.map((item) => Number(item.favorite_actions ?? 0));
|
||||
const commentCount = items.map((item) => Number(item.comment_count ?? 0));
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '新增内容',
|
||||
data: contentCreated,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '新增点赞',
|
||||
data: likeActions,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '新增收藏',
|
||||
data: favoriteActions,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-orange-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-orange-500'),
|
||||
tension: 0.35
|
||||
},
|
||||
{
|
||||
label: '新增评论',
|
||||
data: commentCount,
|
||||
fill: true,
|
||||
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
|
||||
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
|
||||
tension: 0.35
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildChartOptions({ amountMode } = {}) {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||
@@ -186,6 +346,12 @@ function updateCharts() {
|
||||
amountChartData.value = buildAmountChartData(items);
|
||||
orderChartOptions.value = buildChartOptions({ amountMode: false });
|
||||
amountChartOptions.value = buildChartOptions({ amountMode: true });
|
||||
withdrawOrderChartData.value = buildWithdrawOrderChartData(items);
|
||||
withdrawAmountChartData.value = buildWithdrawAmountChartData(items);
|
||||
withdrawOrderChartOptions.value = buildChartOptions({ amountMode: false });
|
||||
withdrawAmountChartOptions.value = buildChartOptions({ amountMode: true });
|
||||
interactionChartData.value = buildInteractionChartData(items);
|
||||
interactionChartOptions.value = buildChartOptions({ amountMode: false });
|
||||
}
|
||||
|
||||
const summaryItems = computed(() => {
|
||||
@@ -202,6 +368,31 @@ const summaryItems = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const withdrawSummaryItems = computed(() => {
|
||||
const summary = overview.value?.summary;
|
||||
if (!summary) return [];
|
||||
|
||||
return [
|
||||
{ key: 'withdraw-apply', label: '提现申请:', value: summary.withdrawal_apply_orders ?? 0, icon: 'pi-inbox', onClick: () => goToWithdrawals('created', false) },
|
||||
{ key: 'withdraw-paid', label: '提现完成:', value: summary.withdrawal_paid_orders ?? 0, icon: 'pi-check-circle', onClick: () => goToWithdrawals('paid', true) },
|
||||
{ key: 'withdraw-failed', label: '提现失败:', value: summary.withdrawal_failed_orders ?? 0, icon: 'pi-times-circle', onClick: () => goToWithdrawals('failed', false) }
|
||||
];
|
||||
});
|
||||
|
||||
const contentSummaryItems = computed(() => {
|
||||
const summary = overview.value?.summary;
|
||||
if (!summary) return [];
|
||||
|
||||
const rangeQuery = buildContentsQuery({ useCreatedAt: true });
|
||||
return [
|
||||
{ key: 'content-count', label: '内容总量:', value: summary.content_count ?? 0, icon: 'pi-book', onClick: () => goToContents() },
|
||||
{ key: 'content-created', label: '新增内容:', value: summary.content_created ?? 0, icon: 'pi-plus', onClick: () => goToContents(rangeQuery) },
|
||||
{ key: 'like-actions', label: '新增点赞:', value: summary.like_actions ?? 0, icon: 'pi-thumbs-up', onClick: () => goToContents(rangeQuery) },
|
||||
{ key: 'favorite-actions', label: '新增收藏:', value: summary.favorite_actions ?? 0, icon: 'pi-star', onClick: () => goToContents(rangeQuery) },
|
||||
{ key: 'comment-count', label: '新增评论:', value: summary.comment_count ?? 0, icon: 'pi-comments', onClick: () => goToContents(rangeQuery) }
|
||||
];
|
||||
});
|
||||
|
||||
async function loadOverview() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -287,6 +478,8 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||
</div>
|
||||
|
||||
<StatisticsStrip v-if="summaryItems.length" :items="summaryItems" containerClass="card mb-4" />
|
||||
<StatisticsStrip v-if="withdrawSummaryItems.length" :items="withdrawSummaryItems" containerClass="card mb-4" />
|
||||
<StatisticsStrip v-if="contentSummaryItems.length" :items="contentSummaryItems" containerClass="card mb-4" />
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -313,6 +506,41 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h4 class="m-0">提现趋势</h4>
|
||||
<span class="text-muted-color">申请 / 完成 / 失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">订单趋势</span>
|
||||
<span class="text-muted-color text-sm">申请 vs 完成 vs 失败</span>
|
||||
</div>
|
||||
<Chart type="line" :data="withdrawOrderChartData" :options="withdrawOrderChartOptions" class="h-72" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">金额趋势</span>
|
||||
<span class="text-muted-color text-sm">单位:元</span>
|
||||
</div>
|
||||
<Chart type="line" :data="withdrawAmountChartData" :options="withdrawAmountChartOptions" class="h-72" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h4 class="m-0">内容互动趋势</h4>
|
||||
<span class="text-muted-color">新增内容 / 点赞 / 收藏 / 评论</span>
|
||||
</div>
|
||||
</div>
|
||||
<Chart type="line" :data="interactionChartData" :options="interactionChartOptions" class="h-72" />
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">趋势明细</h4>
|
||||
@@ -331,6 +559,28 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||
{{ formatCnyFromYuan(data.refund_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="withdrawal_apply_orders" header="提现申请" style="min-width: 10rem" />
|
||||
<Column field="withdrawal_apply_amount" header="申请金额" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCnyFromYuan(data.withdrawal_apply_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="withdrawal_paid_orders" header="提现完成" style="min-width: 10rem" />
|
||||
<Column field="withdrawal_paid_amount" header="完成金额" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCnyFromYuan(data.withdrawal_paid_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="withdrawal_failed_orders" header="提现失败" style="min-width: 10rem" />
|
||||
<Column field="withdrawal_failed_amount" header="失败金额" style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatCnyFromYuan(data.withdrawal_failed_amount) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="content_created" header="新增内容" style="min-width: 10rem" />
|
||||
<Column field="like_actions" header="新增点赞" style="min-width: 10rem" />
|
||||
<Column field="favorite_actions" header="新增收藏" style="min-width: 10rem" />
|
||||
<Column field="comment_count" header="新增评论" style="min-width: 10rem" />
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user