feat: add tenant finance report overview
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
## 1) 总体结论
|
## 1) 总体结论
|
||||||
|
|
||||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、钱包流水与异常排查、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置、评论治理。
|
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、钱包流水与异常排查、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置、评论治理。
|
||||||
- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺批量处置扩展)、创作者治理(缺提现审核联动与结算账户审批流)。
|
- **部分落地**:内容治理(缺批量处置扩展)、创作者治理(缺提现审核联动与结算账户审批流)。
|
||||||
- **未落地**:暂无。
|
- **未落地**:暂无。
|
||||||
|
|
||||||
## 2) 按页面完成度(对照 2.x)
|
## 2) 按页面完成度(对照 2.x)
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
- 备注:成员加入审核/邀请由创作者页统一处理。
|
- 备注:成员加入审核/邀请由创作者页统一处理。
|
||||||
|
|
||||||
### 2.4 租户详情 `/superadmin/tenants/:tenantID`
|
### 2.4 租户详情 `/superadmin/tenants/:tenantID`
|
||||||
- 状态:**部分完成**
|
- 状态:**已完成**
|
||||||
- 已有:租户信息、状态/续期、成员列表、内容与订单查询。
|
- 已有:租户信息、状态/续期、成员列表、内容与订单查询、租户级财务/报表聚合入口。
|
||||||
- 缺口:租户级财务与报表聚合入口(成员审核/邀请由超管入口完成)。
|
- 缺口:无显著功能缺口。
|
||||||
|
|
||||||
### 2.5 用户管理 `/superadmin/users`
|
### 2.5 用户管理 `/superadmin/users`
|
||||||
- 状态:**已完成**
|
- 状态:**已完成**
|
||||||
@@ -92,6 +92,6 @@
|
|||||||
|
|
||||||
## 4) 建议的下一步(按优先级)
|
## 4) 建议的下一步(按优先级)
|
||||||
|
|
||||||
1. **租户详情聚合**:补齐租户财务与报表聚合入口。
|
1. **订单运营补强**:问题订单标记、支付对账辅助能力。
|
||||||
2. **订单运营补强**:问题订单标记、支付对账辅助能力。
|
2. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。
|
||||||
3. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。
|
3. **创作者治理补强**:结算账户审批流、提现审核联动流程。
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { useLayout } from '@/layout/composables/layout';
|
|||||||
import { ReportService } from '@/service/ReportService';
|
import { ReportService } from '@/service/ReportService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { getPrimary, getSurface, isDarkTheme } = useLayout();
|
const { getPrimary, getSurface, isDarkTheme } = useLayout();
|
||||||
|
|
||||||
@@ -33,6 +34,24 @@ const interactionChartOptions = ref(null);
|
|||||||
|
|
||||||
const granularityOptions = [{ label: '按天', value: 'day' }];
|
const granularityOptions = [{ label: '按天', value: 'day' }];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
function formatPercent(value) {
|
function formatPercent(value) {
|
||||||
const rate = Number(value);
|
const rate = Number(value);
|
||||||
if (!Number.isFinite(rate)) return '-';
|
if (!Number.isFinite(rate)) return '-';
|
||||||
@@ -446,7 +465,24 @@ async function exportReport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
(query) => {
|
||||||
|
const tenantValue = parseNumber(getQueryValue(query?.tenant_id));
|
||||||
|
tenantID.value = tenantValue || null;
|
||||||
|
|
||||||
|
const startValue = getQueryValue(query?.start_at);
|
||||||
|
const endValue = getQueryValue(query?.end_at);
|
||||||
|
startAt.value = parseDate(startValue);
|
||||||
|
endAt.value = parseDate(endValue);
|
||||||
|
|
||||||
|
const granularityValue = getQueryValue(query?.granularity);
|
||||||
|
if (granularityValue) granularity.value = String(granularityValue);
|
||||||
|
|
||||||
loadOverview();
|
loadOverview();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
watch([getPrimary, getSurface, isDarkTheme], () => {
|
watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||||
updateCharts();
|
updateCharts();
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SearchField from '@/components/SearchField.vue';
|
import SearchField from '@/components/SearchField.vue';
|
||||||
import SearchPanel from '@/components/SearchPanel.vue';
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
|
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
||||||
import { ContentService } from '@/service/ContentService';
|
import { ContentService } from '@/service/ContentService';
|
||||||
|
import { FinanceService } from '@/service/FinanceService';
|
||||||
import { OrderService } from '@/service/OrderService';
|
import { OrderService } from '@/service/OrderService';
|
||||||
|
import { ReportService } from '@/service/ReportService';
|
||||||
import { TenantService } from '@/service/TenantService';
|
import { TenantService } from '@/service/TenantService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const tenantID = computed(() => Number(route.params.tenantID));
|
const tenantID = computed(() => Number(route.params.tenantID));
|
||||||
|
|
||||||
@@ -30,6 +34,17 @@ function formatCny(amountInCents) {
|
|||||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCnyFromYuan(amountInYuan) {
|
||||||
|
const amount = Number(amountInYuan);
|
||||||
|
if (!Number.isFinite(amount)) return '-';
|
||||||
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(rate) {
|
||||||
|
if (!Number.isFinite(rate)) return '-';
|
||||||
|
return `${(rate * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusSeverity(status) {
|
function getStatusSeverity(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'verified':
|
case 'verified':
|
||||||
@@ -86,6 +101,61 @@ function getContentVisibilitySeverity(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateParam(value) {
|
||||||
|
if (!value) return undefined;
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToReports() {
|
||||||
|
const query = {};
|
||||||
|
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||||
|
const start = formatDateParam(reportStartAt.value);
|
||||||
|
const end = formatDateParam(reportEndAt.value);
|
||||||
|
if (start) query.start_at = start;
|
||||||
|
if (end) query.end_at = end;
|
||||||
|
router.push({ name: 'superadmin-reports', query });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToFinance() {
|
||||||
|
const query = {};
|
||||||
|
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||||
|
router.push({ name: 'superadmin-finance', query });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToOrders(status, usePaidAt) {
|
||||||
|
const query = {};
|
||||||
|
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||||
|
if (status) query.status = status;
|
||||||
|
const start = formatDateParam(reportStartAt.value);
|
||||||
|
const end = formatDateParam(reportEndAt.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;
|
||||||
|
}
|
||||||
|
router.push({ name: 'superadmin-orders', query });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToWithdrawals(status, usePaidAt) {
|
||||||
|
const query = {};
|
||||||
|
if (tenantID.value) query.tenant_id = tenantID.value;
|
||||||
|
if (status) query.status = status;
|
||||||
|
const start = formatDateParam(reportStartAt.value);
|
||||||
|
const end = formatDateParam(reportEndAt.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;
|
||||||
|
}
|
||||||
|
router.push({ name: 'superadmin-finance', query });
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTenant() {
|
async function loadTenant() {
|
||||||
const id = tenantID.value;
|
const id = tenantID.value;
|
||||||
if (!id || Number.isNaN(id)) return;
|
if (!id || Number.isNaN(id)) return;
|
||||||
@@ -375,6 +445,21 @@ const orderStatusOptions = [
|
|||||||
{ label: 'failed', value: 'failed' }
|
{ label: 'failed', value: 'failed' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const reportLoading = ref(false);
|
||||||
|
const reportOverview = ref(null);
|
||||||
|
const reportStartAt = ref(null);
|
||||||
|
const reportEndAt = ref(null);
|
||||||
|
const reportRangeDays = ref(7);
|
||||||
|
const reportRangeOptions = [
|
||||||
|
{ label: '近7天', value: 7 },
|
||||||
|
{ label: '近30天', value: 30 },
|
||||||
|
{ label: '近90天', value: 90 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const ledgerLoading = ref(false);
|
||||||
|
const ledgers = ref([]);
|
||||||
|
const ledgerRows = ref(6);
|
||||||
|
|
||||||
async function loadOrders() {
|
async function loadOrders() {
|
||||||
const id = tenantID.value;
|
const id = tenantID.value;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -438,6 +523,95 @@ function onOrdersSort(event) {
|
|||||||
loadOrders();
|
loadOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureReportRange() {
|
||||||
|
if (reportStartAt.value || reportEndAt.value) return;
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date(end);
|
||||||
|
start.setDate(end.getDate() - Math.max(reportRangeDays.value, 1) + 1);
|
||||||
|
reportStartAt.value = start;
|
||||||
|
reportEndAt.value = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReportOverview() {
|
||||||
|
const id = tenantID.value;
|
||||||
|
if (!id) return;
|
||||||
|
ensureReportRange();
|
||||||
|
|
||||||
|
reportLoading.value = true;
|
||||||
|
try {
|
||||||
|
reportOverview.value = await ReportService.getOverview({
|
||||||
|
tenant_id: id,
|
||||||
|
start_at: reportStartAt.value || undefined,
|
||||||
|
end_at: reportEndAt.value || undefined,
|
||||||
|
granularity: 'day'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载租户报表概览', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
reportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyReportRange(days) {
|
||||||
|
reportRangeDays.value = days;
|
||||||
|
reportStartAt.value = null;
|
||||||
|
reportEndAt.value = null;
|
||||||
|
loadReportOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetReportRange() {
|
||||||
|
reportRangeDays.value = 7;
|
||||||
|
reportStartAt.value = null;
|
||||||
|
reportEndAt.value = null;
|
||||||
|
loadReportOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportSummaryItems = computed(() => {
|
||||||
|
const summary = reportOverview.value?.summary;
|
||||||
|
if (!summary) return [];
|
||||||
|
return [
|
||||||
|
{ key: 'paid-orders', label: '已支付订单:', value: summary.paid_orders ?? 0, icon: 'pi-shopping-cart', onClick: () => goToOrders('paid', true) },
|
||||||
|
{ key: 'paid-amount', label: '已支付金额:', value: formatCnyFromYuan(summary.paid_amount), icon: 'pi-wallet', onClick: () => goToOrders('paid', true) },
|
||||||
|
{ key: 'refund-orders', label: '退款订单:', value: summary.refund_orders ?? 0, icon: 'pi-undo', onClick: () => goToOrders('refunded', false) },
|
||||||
|
{ key: 'refund-amount', label: '退款金额:', value: formatCnyFromYuan(summary.refund_amount), icon: 'pi-replay', onClick: () => goToOrders('refunded', false) },
|
||||||
|
{ key: 'conversion', label: '转化率:', value: formatPercent(summary.conversion_rate), icon: 'pi-percentage' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const withdrawSummaryItems = computed(() => {
|
||||||
|
const summary = reportOverview.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-apply-amount', label: '申请金额:', value: formatCnyFromYuan(summary.withdrawal_apply_amount), icon: 'pi-wallet', onClick: () => goToWithdrawals('created', false) },
|
||||||
|
{ key: 'withdraw-paid', label: '提现完成:', value: summary.withdrawal_paid_orders ?? 0, icon: 'pi-check-circle', onClick: () => goToWithdrawals('paid', true) },
|
||||||
|
{ key: 'withdraw-paid-amount', label: '完成金额:', value: formatCnyFromYuan(summary.withdrawal_paid_amount), icon: 'pi-check', onClick: () => goToWithdrawals('paid', true) },
|
||||||
|
{ key: 'withdraw-failed', label: '提现失败:', value: summary.withdrawal_failed_orders ?? 0, icon: 'pi-times-circle', onClick: () => goToWithdrawals('failed', false) },
|
||||||
|
{ key: 'withdraw-failed-amount', label: '失败金额:', value: formatCnyFromYuan(summary.withdrawal_failed_amount), icon: 'pi-exclamation-circle', onClick: () => goToWithdrawals('failed', false) }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadLedgers() {
|
||||||
|
const id = tenantID.value;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
ledgerLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await FinanceService.listLedgers({
|
||||||
|
page: 1,
|
||||||
|
limit: ledgerRows.value,
|
||||||
|
tenant_id: id,
|
||||||
|
sortField: 'created_at',
|
||||||
|
sortOrder: -1
|
||||||
|
});
|
||||||
|
ledgers.value = result.items;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载租户资金流水', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
ledgerLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => tenantID.value,
|
() => tenantID.value,
|
||||||
() => {
|
() => {
|
||||||
@@ -447,14 +621,30 @@ watch(
|
|||||||
contentsRows.value = 10;
|
contentsRows.value = 10;
|
||||||
ordersPage.value = 1;
|
ordersPage.value = 1;
|
||||||
ordersRows.value = 10;
|
ordersRows.value = 10;
|
||||||
|
reportStartAt.value = null;
|
||||||
|
reportEndAt.value = null;
|
||||||
loadTenant();
|
loadTenant();
|
||||||
loadTenantUsers();
|
loadTenantUsers();
|
||||||
loadContents();
|
loadContents();
|
||||||
loadOrders();
|
loadOrders();
|
||||||
|
if (tabValue.value === 'finance') {
|
||||||
|
loadReportOverview();
|
||||||
|
loadLedgers();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabValue.value,
|
||||||
|
(value) => {
|
||||||
|
if (value === 'finance') {
|
||||||
|
loadReportOverview();
|
||||||
|
loadLedgers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ensureStatusOptionsLoaded().catch(() => {});
|
ensureStatusOptionsLoaded().catch(() => {});
|
||||||
});
|
});
|
||||||
@@ -526,6 +716,7 @@ onMounted(() => {
|
|||||||
<Tab value="users">成员</Tab>
|
<Tab value="users">成员</Tab>
|
||||||
<Tab value="contents">内容</Tab>
|
<Tab value="contents">内容</Tab>
|
||||||
<Tab value="orders">订单</Tab>
|
<Tab value="orders">订单</Tab>
|
||||||
|
<Tab value="finance">财务报表</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel value="users">
|
<TabPanel value="users">
|
||||||
@@ -830,6 +1021,89 @@ onMounted(() => {
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value="finance">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium">财务与报表</span>
|
||||||
|
<span class="text-muted-color">租户级汇总与资金流水</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button label="查看报表" icon="pi pi-chart-line" severity="secondary" @click="goToReports" />
|
||||||
|
<Button label="查看财务" icon="pi pi-wallet" severity="secondary" @click="goToFinance" />
|
||||||
|
<Button
|
||||||
|
label="刷新"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
severity="secondary"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
loadReportOverview();
|
||||||
|
loadLedgers();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:disabled="reportLoading || ledgerLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="reportLoading" @search="loadReportOverview" @reset="resetReportRange">
|
||||||
|
<SearchField label="快速区间">
|
||||||
|
<Select v-model="reportRangeDays" :options="reportRangeOptions" optionLabel="label" optionValue="value" placeholder="选择区间" class="w-full" @update:modelValue="applyReportRange" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="开始时间">
|
||||||
|
<DatePicker v-model="reportStartAt" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="结束时间">
|
||||||
|
<DatePicker v-model="reportEndAt" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<StatisticsStrip v-if="reportSummaryItems.length" :items="reportSummaryItems" containerClass="card" />
|
||||||
|
<StatisticsStrip v-if="withdrawSummaryItems.length" :items="withdrawSummaryItems" containerClass="card" />
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium">最近资金流水</span>
|
||||||
|
<span class="text-muted-color">展示最近 {{ ledgerRows }} 条</span>
|
||||||
|
</div>
|
||||||
|
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadLedgers" :disabled="ledgerLoading" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable :value="ledgers" :loading="ledgerLoading" scrollable scrollHeight="420px" responsiveLayout="scroll">
|
||||||
|
<Column field="created_at" header="时间" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data?.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="type" header="类型" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ data?.type_description || data?.type || '-' }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="amount" header="金额" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatCny(data?.amount) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="余额变化" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ formatCny(data?.balance_before) }} → {{ formatCny(data?.balance_after) }}</span>
|
||||||
|
<span class="text-xs text-muted-color">冻结 {{ formatCny(data?.frozen_before) }} → {{ formatCny(data?.frozen_after) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="remark" header="备注" style="min-width: 18rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-sm">{{ data?.remark || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="order_id" header="订单ID" style="min-width: 10rem" />
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user