393 lines
10 KiB
Vue
393 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
import { fetchFinanceDetails, type FinanceDetail } from '@/features/admin/api/adminFinance'
|
|
import { adminPath } from '@/shared/utils/adminPath'
|
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
|
import { orderStatusLabel } from '@/shared/utils/statusLabels'
|
|
import { formatDateTime } from '@/shared/utils/time'
|
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
|
|
|
const loading = ref(false)
|
|
const details = ref<FinanceDetail[]>([])
|
|
const currentPage = ref(1)
|
|
const currentPageSize = ref(20)
|
|
const total = ref(0)
|
|
const filters = reactive({
|
|
start_date: defaultStartDate(),
|
|
end_date: defaultEndDate(),
|
|
date_type: 'settled',
|
|
order_no: '',
|
|
user_id: '',
|
|
order_status: '',
|
|
settlement_status: '',
|
|
})
|
|
|
|
onMounted(loadDetails)
|
|
|
|
async function loadDetails() {
|
|
loading.value = true
|
|
try {
|
|
const result = await fetchFinanceDetails({
|
|
...filters,
|
|
page: currentPage.value,
|
|
page_size: currentPageSize.value,
|
|
})
|
|
details.value = result.items
|
|
total.value = result.total
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function resetFilters() {
|
|
filters.start_date = defaultStartDate()
|
|
filters.end_date = defaultEndDate()
|
|
filters.date_type = 'settled'
|
|
filters.order_no = ''
|
|
filters.user_id = ''
|
|
filters.order_status = ''
|
|
filters.settlement_status = ''
|
|
currentPage.value = 1
|
|
void loadDetails()
|
|
}
|
|
|
|
async function handlePageChange() {
|
|
await loadDetails()
|
|
}
|
|
|
|
function moneyCent(value: number) {
|
|
return formatCentWithSymbol(value)
|
|
}
|
|
|
|
function financeStatusLabel(status: string) {
|
|
const map: Record<string, string> = {
|
|
normal: '正常',
|
|
refund_pending: '退款中',
|
|
refund_failed: '退款失败',
|
|
settlement_diff: '结算差异',
|
|
}
|
|
return map[status] || status
|
|
}
|
|
|
|
function financeStatusType(status: string) {
|
|
if (status === 'normal') return 'success'
|
|
if (status === 'settlement_diff' || status === 'refund_failed') return 'danger'
|
|
if (status === 'refund_pending') return 'warning'
|
|
return 'info'
|
|
}
|
|
|
|
function settlementStatusLabel(status: string) {
|
|
const map: Record<string, string> = {
|
|
unsettled: '未结算',
|
|
settling: '结算中',
|
|
settled: '已结算',
|
|
}
|
|
return map[status] || status || '-'
|
|
}
|
|
|
|
function shortDateTime(value?: string) {
|
|
if (!value) return '-'
|
|
const text = formatDateTime(value)
|
|
return text.replace(/^\d{4}-/, '')
|
|
}
|
|
|
|
function defaultStartDate() {
|
|
const date = new Date()
|
|
date.setDate(date.getDate() - 29)
|
|
return formatInputDate(date)
|
|
}
|
|
|
|
function defaultEndDate() {
|
|
return formatInputDate(new Date())
|
|
}
|
|
|
|
function formatInputDate(date: Date) {
|
|
return date.toISOString().slice(0, 10)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="page finance-details-page">
|
|
<div class="page-header-row">
|
|
<div class="page-header">
|
|
<p class="eyebrow">Finance Details</p>
|
|
<h1>财务明细</h1>
|
|
<p>按订单核对收款、退款、平台收入、号主应得与钱包实际入账。</p>
|
|
</div>
|
|
<div class="toolbar-actions">
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
<el-button type="primary" :icon="Search" :loading="loading" @click="loadDetails">
|
|
查询
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-form class="filter-panel finance-filter-panel" label-position="top">
|
|
<el-form-item label="日期类型">
|
|
<el-select v-model="filters.date_type" class="full-control">
|
|
<el-option label="结算日期" value="settled" />
|
|
<el-option label="下单日期" value="created" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="开始日期">
|
|
<el-date-picker
|
|
v-model="filters.start_date"
|
|
class="full-control"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="结束日期">
|
|
<el-date-picker
|
|
v-model="filters.end_date"
|
|
class="full-control"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="订单号">
|
|
<el-input v-model="filters.order_no" clearable placeholder="按订单号筛选" />
|
|
</el-form-item>
|
|
<el-form-item label="用户 ID">
|
|
<el-input v-model="filters.user_id" clearable placeholder="租客或号主 ID" />
|
|
</el-form-item>
|
|
<el-form-item label="订单状态">
|
|
<el-select
|
|
v-model="filters.order_status"
|
|
clearable
|
|
placeholder="全部状态"
|
|
class="full-control"
|
|
>
|
|
<el-option label="待支付" value="pending_payment" />
|
|
<el-option label="使用中" value="renting" />
|
|
<el-option label="待结账" value="pending_checkout_confirm" />
|
|
<el-option label="已完成" value="completed" />
|
|
<el-option label="已关闭" value="closed" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="结算状态">
|
|
<el-select
|
|
v-model="filters.settlement_status"
|
|
clearable
|
|
placeholder="全部结算"
|
|
class="full-control"
|
|
>
|
|
<el-option label="未结算" value="unsettled" />
|
|
<el-option label="已结算" value="settled" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<div v-loading="loading" class="table-panel finance-details-table">
|
|
<div class="finance-grid-header">
|
|
<div>订单</div>
|
|
<div>状态</div>
|
|
<div>租客/号主</div>
|
|
<div class="amount-cell">收款</div>
|
|
<div class="amount-cell">已退</div>
|
|
<div class="amount-cell">退中</div>
|
|
<div class="amount-cell">净入</div>
|
|
<div class="amount-cell">平台</div>
|
|
<div class="amount-cell">应得</div>
|
|
<div class="amount-cell">入账</div>
|
|
<div class="amount-cell">差异</div>
|
|
</div>
|
|
|
|
<div v-if="details.length > 0" class="finance-grid-body">
|
|
<div v-for="row in details" :key="row.order_id" class="finance-grid-row">
|
|
<div class="order-cell">
|
|
<RouterLink class="order-no" :to="adminPath(`orders/${row.order_id}`)">
|
|
{{ row.order_no }}
|
|
</RouterLink>
|
|
<span class="cell-muted">结算 {{ shortDateTime(row.settled_at) }}</span>
|
|
</div>
|
|
|
|
<div class="status-stack">
|
|
<span>{{ orderStatusLabel(row.order_status) }}</span>
|
|
<small>{{ settlementStatusLabel(row.settlement_status) }}</small>
|
|
<el-tag size="small" :type="financeStatusType(row.finance_status)">
|
|
{{ financeStatusLabel(row.finance_status) }}
|
|
</el-tag>
|
|
</div>
|
|
|
|
<div class="party-cell">
|
|
<span>租 {{ row.renter_phone || row.renter_nickname || row.renter_id }}</span>
|
|
<span>主 {{ row.owner_phone || row.owner_nickname || row.owner_id }}</span>
|
|
</div>
|
|
|
|
<span class="amount-text amount-cell">{{ moneyCent(row.paid_amount_cent) }}</span>
|
|
<span class="amount-text amount-cell muted-amount">
|
|
{{ moneyCent(row.refunded_amount_cent) }}
|
|
</span>
|
|
<span class="amount-text amount-cell muted-amount">
|
|
{{ moneyCent(row.refunding_amount_cent) }}
|
|
</span>
|
|
<span class="amount-text amount-cell">{{ moneyCent(row.channel_net_amount_cent) }}</span>
|
|
<span class="amount-text amount-cell">{{
|
|
moneyCent(row.checkout_platform_fee_cent)
|
|
}}</span>
|
|
<span class="amount-text amount-cell">{{
|
|
moneyCent(row.checkout_owner_income_cent)
|
|
}}</span>
|
|
<span class="amount-text amount-cell">{{
|
|
moneyCent(row.owner_wallet_income_amount_cent)
|
|
}}</span>
|
|
<span
|
|
class="amount-text amount-cell"
|
|
:class="{ 'amount-danger': Math.abs(row.settlement_diff_amount_cent) >= 5 }"
|
|
>
|
|
{{ moneyCent(row.settlement_diff_amount_cent) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<el-empty v-else :image-size="80" description="暂无财务明细" />
|
|
</div>
|
|
|
|
<AdminTablePagination
|
|
v-if="total > 0"
|
|
v-model:current-page="currentPage"
|
|
v-model:page-size="currentPageSize"
|
|
:total="total"
|
|
:loading="loading"
|
|
@page-change="handlePageChange"
|
|
/>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.finance-details-page {
|
|
--finance-muted: #7f8aa3;
|
|
}
|
|
|
|
.finance-details-page :deep(.page-header-row) {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.finance-details-page :deep(.page-header h1) {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.finance-filter-panel {
|
|
grid-template-columns: 7% 10% 10% 29% 23% 10% 11%;
|
|
gap: 10px;
|
|
margin-bottom: 16px;
|
|
padding: 14px 16px;
|
|
}
|
|
|
|
.finance-filter-panel :deep(.el-form-item__label) {
|
|
margin-bottom: 4px;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.finance-filter-panel :deep(.el-input__wrapper),
|
|
.finance-filter-panel :deep(.el-select__wrapper) {
|
|
min-height: 32px;
|
|
}
|
|
|
|
.finance-details-table {
|
|
padding: 0;
|
|
}
|
|
|
|
.finance-grid-header,
|
|
.finance-grid-row {
|
|
display: grid;
|
|
grid-template-columns: 15% 9% 16% repeat(8, 7.5%);
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.finance-grid-header {
|
|
border-bottom: 1px solid #edf1f7;
|
|
color: #7f8aa3;
|
|
font-size: 12px;
|
|
font-weight: 650;
|
|
letter-spacing: 0;
|
|
line-height: 1.35;
|
|
text-transform: none;
|
|
}
|
|
|
|
.finance-grid-header > div,
|
|
.finance-grid-row > div,
|
|
.finance-grid-row > span {
|
|
min-width: 0;
|
|
padding: 8px 7px;
|
|
}
|
|
|
|
.finance-grid-row {
|
|
border-bottom: 1px solid #edf1f7;
|
|
}
|
|
|
|
.finance-grid-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.order-cell,
|
|
.status-stack,
|
|
.party-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.order-no {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
color: #364152;
|
|
font-weight: 650;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cell-muted,
|
|
.status-stack small,
|
|
.party-cell span {
|
|
color: var(--finance-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.status-stack > span {
|
|
color: #364152;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.party-cell span {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.amount-text {
|
|
display: block;
|
|
overflow: hidden;
|
|
color: #344054;
|
|
font-size: 12px;
|
|
font-weight: 650;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.amount-cell {
|
|
text-align: right;
|
|
}
|
|
|
|
.muted-amount {
|
|
color: #667085;
|
|
}
|
|
|
|
.amount-danger {
|
|
color: #dc2626;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@media (max-width: 1440px) {
|
|
.finance-filter-panel {
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
}
|
|
</style>
|