优化财务管理整个ui
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { Document, DocumentChecked, Refresh, Search, View } from '@element-plus/icons-vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ 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(),
|
||||
@@ -65,8 +66,8 @@ async function handlePageChange() {
|
||||
await loadDetails()
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return formatCentWithSymbol(value)
|
||||
function moneyCent(value: number | undefined | null) {
|
||||
return formatCentWithSymbol(value || 0)
|
||||
}
|
||||
|
||||
function financeStatusLabel(status: string) {
|
||||
@@ -80,7 +81,7 @@ function financeStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function financeStatusType(status: string) {
|
||||
function financeStatusType(status: string): 'success' | 'danger' | 'warning' | 'info' {
|
||||
if (status === 'normal') return 'success'
|
||||
if (status === 'settlement_diff' || status === 'refund_failed') return 'danger'
|
||||
if (status === 'refund_pending' || status === 'offline_settlement_pending') return 'warning'
|
||||
@@ -113,12 +114,6 @@ function offlineSettlementStatusLabel(status: string) {
|
||||
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)
|
||||
@@ -131,317 +126,600 @@ function defaultEndDate() {
|
||||
</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="offline_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-item label="结算模式">
|
||||
<el-select
|
||||
v-model="filters.settlement_mode"
|
||||
clearable
|
||||
placeholder="全部模式"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="号主钱包" value="owner_wallet" />
|
||||
<el-option label="平台代管" value="platform_managed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="线下状态">
|
||||
<el-select
|
||||
v-model="filters.offline_settlement_status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="待线下结算" value="pending" />
|
||||
<el-option label="已线下结算" value="settled" />
|
||||
<el-option label="无需线下结算" value="none" />
|
||||
</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 class="finance-page-container">
|
||||
<!-- 页面头部卡片 -->
|
||||
<header class="page-header-card">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<div class="eyebrow-tag">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>FINANCE DETAILS</span>
|
||||
</div>
|
||||
<h1 class="page-title">财务明细</h1>
|
||||
<p class="page-subtitle">
|
||||
按订单维度精细核对收款、退款、平台利润扣费、号主应得与实际入账差异。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="status-stack">
|
||||
<span>{{ orderStatusLabel(row.order_status) }}</span>
|
||||
<small
|
||||
>{{ settlementStatusLabel(row.settlement_status) }} ·
|
||||
{{ settlementModeLabel(row) }}</small
|
||||
>
|
||||
<small v-if="isPlatformManaged(row)">
|
||||
{{ offlineSettlementStatusLabel(row.offline_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_effective_settlement_amount_cent)
|
||||
}}</span>
|
||||
<span
|
||||
class="amount-text amount-cell"
|
||||
:class="{
|
||||
'amount-danger':
|
||||
Math.abs(row.settlement_diff_amount_cent) >= SETTLEMENT_DIFF_THRESHOLD_CENT,
|
||||
}"
|
||||
>
|
||||
{{ moneyCent(row.settlement_diff_amount_cent) }}
|
||||
</span>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadDetails">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-empty v-else :image-size="80" description="暂无财务明细" />
|
||||
</div>
|
||||
<!-- 筛选面板卡片 -->
|
||||
<section class="section-card filter-card">
|
||||
<el-form class="filter-grid" label-position="top" @keyup.enter="loadDetails">
|
||||
<el-form-item label="日期类型">
|
||||
<el-select v-model="filters.date_type" class="full-control">
|
||||
<el-option label="结算日期" value="settled" />
|
||||
<el-option label="线下打款日期" value="offline_settled" />
|
||||
<el-option label="下单日期" value="created" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker
|
||||
v-model="filters.start_date"
|
||||
class="full-control"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
/>
|
||||
</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"
|
||||
:clearable="false"
|
||||
/>
|
||||
</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-item label="结算模式">
|
||||
<el-select
|
||||
v-model="filters.settlement_mode"
|
||||
clearable
|
||||
placeholder="全部模式"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="号主钱包" value="owner_wallet" />
|
||||
<el-option label="平台代管" value="platform_managed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="线下状态">
|
||||
<el-select
|
||||
v-model="filters.offline_settlement_status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="待线下结算" value="pending" />
|
||||
<el-option label="已线下结算" value="settled" />
|
||||
<el-option label="无需线下结算" value="none" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<!-- 财务明细数据表格 -->
|
||||
<section class="section-card table-section-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-header-title">
|
||||
<el-icon class="header-icon"><DocumentChecked /></el-icon>
|
||||
<span>订单财务流水台账</span>
|
||||
</div>
|
||||
<span class="count-badge">共 {{ total }} 条订单明细</span>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
class="custom-enterprise-table"
|
||||
:data="details"
|
||||
stripe
|
||||
empty-text="暂无财务明细记录"
|
||||
>
|
||||
<!-- 订单标识 -->
|
||||
<el-table-column label="订单信息" min-width="190" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="order-cell">
|
||||
<RouterLink class="order-no-link" :to="adminPath(`orders/${row.order_id}`)">
|
||||
{{ row.order_no }}
|
||||
</RouterLink>
|
||||
<small class="time-sub">结算 {{ formatDateTime(row.settled_at) || '-' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 状态复合列 -->
|
||||
<el-table-column label="订单与结算状态" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<div class="status-stack">
|
||||
<div class="status-row">
|
||||
<span class="order-status-badge">{{ orderStatusLabel(row.order_status) }}</span>
|
||||
<el-tag size="small" :type="financeStatusType(row.finance_status)" effect="plain">
|
||||
{{ financeStatusLabel(row.finance_status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<small class="settle-desc">
|
||||
{{ settlementStatusLabel(row.settlement_status) }} ·
|
||||
{{ settlementModeLabel(row) }}
|
||||
</small>
|
||||
<small v-if="isPlatformManaged(row)" class="offline-desc">
|
||||
{{ offlineSettlementStatusLabel(row.offline_settlement_status) }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 交易双方 -->
|
||||
<el-table-column label="交易双方" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="parties-cell">
|
||||
<div class="party-line">
|
||||
<span class="role-badge renter">租</span>
|
||||
<span class="party-info">{{
|
||||
row.renter_phone || row.renter_nickname || `UID ${row.renter_id}`
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="party-line">
|
||||
<span class="role-badge owner">主</span>
|
||||
<span class="party-info">{{
|
||||
row.owner_phone || row.owner_nickname || `UID ${row.owner_id}`
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 收款 -->
|
||||
<el-table-column label="收款金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val flow-amount">{{ moneyCent(row.paid_amount_cent) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 退款情况 -->
|
||||
<el-table-column label="已退 / 退中" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="refund-stack">
|
||||
<span v-if="row.refunded_amount_cent > 0" class="refund-text">
|
||||
退 {{ moneyCent(row.refunded_amount_cent) }}
|
||||
</span>
|
||||
<span v-else class="zero-text">0.0</span>
|
||||
|
||||
<span v-if="row.refunding_amount_cent > 0" class="refunding-text">
|
||||
中 {{ moneyCent(row.refunding_amount_cent) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 渠道净流入 -->
|
||||
<el-table-column label="渠道净流入" min-width="125" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val net-amount">{{
|
||||
moneyCent(row.channel_net_amount_cent)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 平台利润 -->
|
||||
<el-table-column label="平台收益" min-width="125" align="right">
|
||||
<template #default="{ row }">
|
||||
<strong class="amount-val profit-amount">
|
||||
{{ moneyCent(row.checkout_platform_fee_cent) }}
|
||||
</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 号主应得 -->
|
||||
<el-table-column label="号主应得" min-width="125" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val">{{ moneyCent(row.checkout_owner_income_cent) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 实际入账 / 线下 -->
|
||||
<el-table-column label="实际结算" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val success-val">
|
||||
{{ moneyCent(row.owner_effective_settlement_amount_cent) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 结算差异 -->
|
||||
<el-table-column label="结算差异" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="Math.abs(row.settlement_diff_amount_cent) >= SETTLEMENT_DIFF_THRESHOLD_CENT"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="diff-tag"
|
||||
>
|
||||
{{ moneyCent(row.settlement_diff_amount_cent) }}
|
||||
</el-tag>
|
||||
<span v-else class="amount-val zero-diff">
|
||||
{{ moneyCent(row.settlement_diff_amount_cent) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<RouterLink :to="adminPath(`orders/${row.order_id}`)" class="action-link">
|
||||
<el-icon><View /></el-icon>
|
||||
<span>查看</span>
|
||||
</RouterLink>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<AdminTablePagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.finance-details-page {
|
||||
--finance-muted: #7f8aa3;
|
||||
.finance-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 4px 32px;
|
||||
}
|
||||
|
||||
.finance-details-page :deep(.page-header-row) {
|
||||
margin-bottom: 18px;
|
||||
/* ================= 头部卡片 ================= */
|
||||
.page-header-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.finance-details-page :deep(.page-header h1) {
|
||||
font-size: 28px;
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.finance-filter-panel {
|
||||
grid-template-columns: repeat(9, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.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%);
|
||||
.eyebrow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: #4f46e5;
|
||||
background: #eef2ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ================= 筛选面板 ================= */
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.full-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.finance-grid-header {
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
color: #7f8aa3;
|
||||
/* ================= 表格卡片 ================= */
|
||||
.table-section-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #4f46e5;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.35;
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
color: #4f46e5;
|
||||
background: #eef2ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.finance-grid-header > div,
|
||||
.finance-grid-row > div,
|
||||
.finance-grid-row > span {
|
||||
min-width: 0;
|
||||
padding: 8px 7px;
|
||||
.table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.finance-grid-row {
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
|
||||
background-color: #f8fafc !important;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.finance-grid-row:last-child {
|
||||
border-bottom: 0;
|
||||
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.order-cell,
|
||||
.status-stack,
|
||||
.party-cell {
|
||||
.order-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;
|
||||
.order-no-link {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cell-muted,
|
||||
.status-stack small,
|
||||
.party-cell span {
|
||||
color: var(--finance-muted);
|
||||
.order-no-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.time-sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.order-status-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-stack > span {
|
||||
color: #364152;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.party-cell span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.settle-desc {
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #344054;
|
||||
.offline-desc {
|
||||
font-size: 11px;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.parties-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.party-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.role-badge.renter {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.role-badge.owner {
|
||||
background: #f5f3ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.party-info {
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
text-align: right;
|
||||
.amount-val {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.muted-amount {
|
||||
color: #667085;
|
||||
.flow-amount {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.amount-danger {
|
||||
color: #dc2626;
|
||||
.net-amount {
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.profit-amount {
|
||||
color: #059669;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.success-val {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.refund-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.refund-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.refunding-text {
|
||||
font-size: 11px;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.zero-text {
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.diff-tag {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.finance-filter-panel {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.zero-diff {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12.5px;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -308,75 +308,91 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Payment Configs</p>
|
||||
<h1>支付配置管理</h1>
|
||||
<p>管理多个支付商户配置,支付宝和微信可分别启用默认渠道。</p>
|
||||
</div>
|
||||
<div class="finance-page-container">
|
||||
<header class="page-header-card">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<div class="eyebrow-tag">
|
||||
<el-icon><CreditCard /></el-icon>
|
||||
<span>PAYMENT CONFIGS</span>
|
||||
</div>
|
||||
<h1 class="page-title">支付配置管理</h1>
|
||||
<p class="page-subtitle">
|
||||
管理平台多渠道支付商户配置,支持支付宝和微信独立指定默认与备用支付通道。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-select
|
||||
v-model="filterProvider"
|
||||
placeholder="支付服务商"
|
||||
style="width: 150px"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in providerOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterPayWay"
|
||||
placeholder="支付方式"
|
||||
style="width: 120px"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in payWayOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterStatus"
|
||||
placeholder="状态"
|
||||
style="width: 120px"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in statusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterEnvironment"
|
||||
placeholder="环境"
|
||||
style="width: 120px"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in environmentOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadConfigs">刷新</el-button>
|
||||
<el-button :icon="Upload" :loading="importing" @click="handleImportBackup"
|
||||
>导入备份</el-button
|
||||
>
|
||||
<el-button :icon="Download" :loading="exporting" @click="handleExportBackup"
|
||||
>导出备份</el-button
|
||||
>
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新增配置</el-button>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Upload" :loading="importing" @click="handleImportBackup">
|
||||
导入备份
|
||||
</el-button>
|
||||
<el-button :icon="Download" :loading="exporting" @click="handleExportBackup">
|
||||
导出备份
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Plus" class="action-btn-primary" @click="handleCreate">
|
||||
新增配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="section-card filter-card">
|
||||
<div class="filter-bar-flex">
|
||||
<el-select
|
||||
v-model="filterProvider"
|
||||
placeholder="支付服务商"
|
||||
class="filter-select"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in providerOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterPayWay"
|
||||
placeholder="支付方式"
|
||||
class="filter-select"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in payWayOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterStatus"
|
||||
placeholder="状态"
|
||||
class="filter-select"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in statusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filterEnvironment"
|
||||
placeholder="环境"
|
||||
class="filter-select"
|
||||
@change="loadConfigs"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in environmentOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadConfigs">刷新</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<input
|
||||
ref="backupFileInput"
|
||||
@@ -386,119 +402,131 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
@change="handleImportFile"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
:data="filteredConfigs"
|
||||
v-loading="loading"
|
||||
class="payment-config-table"
|
||||
:row-class-name="tableRowClassName"
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="name" label="配置名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="provider" label="服务商" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatProvider(row.provider) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pay_way" label="支付方式" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getPayWayClass(row.pay_way)" size="small">
|
||||
{{ formatPayWay(row.pay_way) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="merchant_id" label="商户号" min-width="150" />
|
||||
<el-table-column prop="environment" label="环境" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getEnvironmentClass(row.environment)" size="small">
|
||||
{{ formatEnvironment(row.environment) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getStatusClass(row.status)" size="small">
|
||||
{{ formatStatus(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_default" label="默认渠道" width="118" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="isDefaultConfig(row)" class="default-badge">
|
||||
<el-icon><StarFilled /></el-icon>
|
||||
默认渠道
|
||||
</span>
|
||||
<span v-else class="standby-badge">备用</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付记录" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="usage-cell">
|
||||
<div>成功: {{ row.total_transactions }} 笔</div>
|
||||
<div :class="{ 'usage-reference-locked': (row.reference_count || 0) > 0 }">
|
||||
引用: {{ row.reference_count || 0 }} 条
|
||||
</div>
|
||||
<div>金额: ¥{{ formatAmount(row.total_amount_cent) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="last_used_at"
|
||||
label="最后成功支付"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="226" class-name="operation-column">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
class="action-button view"
|
||||
size="small"
|
||||
:icon="View"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="action-button edit"
|
||||
size="small"
|
||||
:icon="Edit"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-tooltip :content="deleteDisabledReason(row)" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
class="action-button delete"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
:disabled="!canDeleteConfig(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<section class="section-card table-section-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-header-title">
|
||||
<el-icon class="header-icon"><CreditCard /></el-icon>
|
||||
<span>支付通道配置列表</span>
|
||||
</div>
|
||||
<span class="count-badge">共 {{ total }} 个配置</span>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadConfigs"
|
||||
@size-change="loadConfigs"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="filteredConfigs"
|
||||
v-loading="loading"
|
||||
class="payment-config-table custom-enterprise-table"
|
||||
:row-class-name="tableRowClassName"
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="name" label="配置名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="provider" label="服务商" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatProvider(row.provider) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pay_way" label="支付方式" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getPayWayClass(row.pay_way)" size="small">
|
||||
{{ formatPayWay(row.pay_way) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="merchant_id" label="商户号" min-width="150" />
|
||||
<el-table-column prop="environment" label="环境" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getEnvironmentClass(row.environment)" size="small">
|
||||
{{ formatEnvironment(row.environment) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="config-tag" :class="getStatusClass(row.status)" size="small">
|
||||
{{ formatStatus(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_default" label="默认渠道" width="118" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="isDefaultConfig(row)" class="default-badge">
|
||||
<el-icon><StarFilled /></el-icon>
|
||||
默认渠道
|
||||
</span>
|
||||
<span v-else class="standby-badge">备用</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付记录" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="usage-cell">
|
||||
<div>成功: {{ row.total_transactions }} 笔</div>
|
||||
<div :class="{ 'usage-reference-locked': (row.reference_count || 0) > 0 }">
|
||||
引用: {{ row.reference_count || 0 }} 条
|
||||
</div>
|
||||
<div>金额: ¥{{ formatAmount(row.total_amount_cent) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="last_used_at"
|
||||
label="最后成功支付"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="226" class-name="operation-column">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
class="action-button view"
|
||||
size="small"
|
||||
:icon="View"
|
||||
@click="handleView(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="action-button edit"
|
||||
size="small"
|
||||
:icon="Edit"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-tooltip :content="deleteDisabledReason(row)" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
class="action-button delete"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
:disabled="!canDeleteConfig(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadConfigs"
|
||||
@size-change="loadConfigs"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PaymentConfigDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -507,46 +535,137 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
@close="handleDialogClose"
|
||||
@saved="handleSaved"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 24px;
|
||||
.finance-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 4px 32px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
.page-header-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.page-header .eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #6b7280;
|
||||
.eyebrow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
}
|
||||
|
||||
.filter-bar-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.table-section-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.backup-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Document, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
CreditCard,
|
||||
DocumentChecked,
|
||||
Money,
|
||||
Refresh,
|
||||
Search,
|
||||
TopRight,
|
||||
View,
|
||||
Wallet,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
@@ -15,6 +24,7 @@ const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const detailVisible = ref(false)
|
||||
const activePayment = ref<AdminPayment | null>(null)
|
||||
|
||||
const filters = reactive({
|
||||
user_id: '',
|
||||
order_id: '',
|
||||
@@ -70,11 +80,11 @@ async function handlePageChange() {
|
||||
await loadPayments()
|
||||
}
|
||||
|
||||
function moneyCent(value: number) {
|
||||
return formatCentWithSymbol(value)
|
||||
function moneyCent(value: number | undefined | null) {
|
||||
return formatCentWithSymbol(value || 0)
|
||||
}
|
||||
|
||||
function paymentStatusType(status: string) {
|
||||
function paymentStatusType(status: string): 'success' | 'danger' | 'warning' | 'info' {
|
||||
if (['paid', 'refunded'].includes(status)) return 'success'
|
||||
if (status === 'failed') return 'danger'
|
||||
if (['paying', 'refunding'].includes(status)) return 'warning'
|
||||
@@ -126,199 +136,643 @@ function jsonText(value: unknown) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Payment Ledger</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="loadPayments"
|
||||
>查询</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>总条数</span>
|
||||
<strong>{{ total }} 笔</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>本页支付额</span>
|
||||
<strong>{{ moneyCent(payAmount) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>本页退款额</span>
|
||||
<strong>{{ moneyCent(refundAmount) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form class="filter-panel" label-position="top">
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="按用户筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单 ID">
|
||||
<el-input v-model="filters.order_id" clearable placeholder="按订单筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="filters.order_no" clearable placeholder="按订单号筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="filters.biz_type" clearable placeholder="全部业务" class="full-control">
|
||||
<el-option label="订单支付" value="order_pay" />
|
||||
<el-option label="撞车支付" value="mohong_pay" />
|
||||
<el-option label="结账退款" value="checkout_refund" />
|
||||
<el-option label="仲裁退款" value="arbitration_refund" />
|
||||
<el-option label="人工退款" value="admin_refund" />
|
||||
<el-option label="取消退款" value="cancel_refund" />
|
||||
<el-option label="撞车退款" value="mohong_refund" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" class="full-control">
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="支付中" value="paying" />
|
||||
<el-option label="退款中" value="refunding" />
|
||||
<el-option label="已退款" value="refunded" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道">
|
||||
<el-select v-model="filters.provider" clearable placeholder="全部渠道" class="full-control">
|
||||
<el-option label="拉卡拉" value="lakala" />
|
||||
<el-option label="乐刷" value="leshua" />
|
||||
<el-option label="顺成" value="shuncheng" />
|
||||
<el-option label="Mock" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="payments">
|
||||
<el-table-column prop="payment_no" label="支付单号" min-width="220" />
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<RouterLink v-if="row.order_id" :to="adminPath(`orders/${row.order_id}`)">{{
|
||||
row.order_no || row.order_id
|
||||
}}</RouterLink>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" min-width="130">
|
||||
<template #default="{ row }">{{ row.user_phone || `用户 ${row.user_id}` }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="业务" width="120">
|
||||
<template #default="{ row }">{{ bizTypeLabel(row.biz_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" width="90">
|
||||
<template #default="{ row }">{{ row.provider || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="110">
|
||||
<template #default="{ row }">{{ moneyCent(row.amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="paymentStatusType(row.status)">{{
|
||||
paymentStatusLabel(row.status)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status === 'failed' && row.error_message"
|
||||
>{{ row.error_code }} {{ row.error_message }}</span
|
||||
>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="third_order_id" label="平台单号" min-width="230" />
|
||||
<el-table-column prop="provider_order_id" label="渠道单号" min-width="220" />
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :icon="Document" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="支付流水详情" width="820px">
|
||||
<div v-if="activePayment" class="payment-detail">
|
||||
<div class="detail-grid">
|
||||
<p>
|
||||
<span>业务类型</span><strong>{{ bizTypeLabel(activePayment.biz_type) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>状态</span><strong>{{ paymentStatusLabel(activePayment.status) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>金额</span><strong>{{ moneyCent(activePayment.amount_cent) }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>渠道</span><strong>{{ activePayment.provider || '-' }}</strong>
|
||||
<div class="finance-page-container">
|
||||
<!-- 头部卡片 -->
|
||||
<header class="page-header-card">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<div class="eyebrow-tag">
|
||||
<el-icon><CreditCard /></el-icon>
|
||||
<span>PAYMENT LEDGER</span>
|
||||
</div>
|
||||
<h1 class="page-title">支付流水</h1>
|
||||
<p class="page-subtitle">
|
||||
聚合监控第三方通道支付与退款交互、渠道交易号、状态及失败异常报文。
|
||||
</p>
|
||||
</div>
|
||||
<h3>请求参数</h3>
|
||||
<pre>{{ jsonText(activePayment.raw_request) }}</pre>
|
||||
<h3>响应结果</h3>
|
||||
<pre>{{ jsonText(activePayment.raw_response) }}</pre>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadPayments">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 汇总指标卡片 -->
|
||||
<section class="summary-metric-grid">
|
||||
<div class="metric-card metric-card--pay">
|
||||
<div class="metric-icon-wrap emerald">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页支付成功额</span>
|
||||
<strong class="metric-val emerald-val">{{ moneyCent(payAmount) }}</strong>
|
||||
<span class="metric-sub-pill success">支付流水</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--refund">
|
||||
<div class="metric-icon-wrap rose">
|
||||
<el-icon><TopRight /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页退款发生额</span>
|
||||
<strong class="metric-val rose-val">{{ moneyCent(refundAmount) }}</strong>
|
||||
<span class="metric-sub-pill danger">退款流水</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--total">
|
||||
<div class="metric-icon-wrap indigo">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">渠道流水记录</span>
|
||||
<strong class="metric-val">{{ total }} 笔</strong>
|
||||
<span class="metric-sub-text">第三方通道总记录数</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 筛选卡片 -->
|
||||
<section class="section-card filter-card">
|
||||
<el-form class="filter-grid" label-position="top" @keyup.enter="loadPayments">
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="按用户 ID 检索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单 ID">
|
||||
<el-input v-model="filters.order_id" clearable placeholder="按订单 ID 检索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="filters.order_no" clearable placeholder="按订单号检索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select
|
||||
v-model="filters.biz_type"
|
||||
clearable
|
||||
placeholder="全部业务类型"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="订单支付" value="order_pay" />
|
||||
<el-option label="撞车支付" value="mohong_pay" />
|
||||
<el-option label="结账退款" value="checkout_refund" />
|
||||
<el-option label="仲裁退款" value="arbitration_refund" />
|
||||
<el-option label="人工退款" value="admin_refund" />
|
||||
<el-option label="取消退款" value="cancel_refund" />
|
||||
<el-option label="撞车退款" value="mohong_refund" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="流水状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" class="full-control">
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="支付中" value="paying" />
|
||||
<el-option label="退款中" value="refunding" />
|
||||
<el-option label="已退款" value="refunded" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付渠道">
|
||||
<el-select
|
||||
v-model="filters.provider"
|
||||
clearable
|
||||
placeholder="全部渠道"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="拉卡拉" value="lakala" />
|
||||
<el-option label="乐刷" value="leshua" />
|
||||
<el-option label="顺成" value="shuncheng" />
|
||||
<el-option label="Mock" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<!-- 支付流水表格 -->
|
||||
<section class="section-card table-section-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-header-title">
|
||||
<el-icon class="header-icon"><DocumentChecked /></el-icon>
|
||||
<span>支付与退款渠道明细</span>
|
||||
</div>
|
||||
<span class="count-badge">共 {{ total }} 条记录</span>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
class="custom-enterprise-table"
|
||||
:data="payments"
|
||||
stripe
|
||||
empty-text="暂无支付流水记录"
|
||||
>
|
||||
<el-table-column label="支付流水号" min-width="210" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<span class="mono-code">{{ row.payment_no }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="渠道 / 方式" width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="provider-cell">
|
||||
<el-tag size="small" effect="plain" class="provider-tag">{{ row.provider }}</el-tag>
|
||||
<small class="pay-way">{{ row.pay_way || '-' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="业务类型" width="130">
|
||||
<template #default="{ row }">
|
||||
<span class="biz-type-text">{{ bizTypeLabel(row.biz_type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交易金额" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<strong
|
||||
class="amount-val"
|
||||
:class="row.biz_type.includes('refund') ? 'refund-val' : 'pay-val'"
|
||||
>
|
||||
{{ moneyCent(row.amount_cent) }}
|
||||
</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="paymentStatusType(row.status)" class="status-pill">
|
||||
{{ paymentStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="关联订单 / 用户" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<div class="order-user-cell">
|
||||
<RouterLink
|
||||
v-if="row.order_id"
|
||||
class="order-link"
|
||||
:to="adminPath(`orders/${row.order_id}`)"
|
||||
>
|
||||
{{ row.order_no || `订单 #${row.order_id}` }}
|
||||
</RouterLink>
|
||||
<small class="user-id">用户 ID: {{ row.user_id }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="渠道交易单号" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<span class="channel-no">{{ row.channel_trade_no || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="异常与失败信息" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.error_message" class="error-msg">{{ row.error_message }}</span>
|
||||
<span v-else class="muted-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间" min-width="165">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="View" @click="openDetail(row)">
|
||||
报文
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- 报文详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="支付流水报文详情" width="680px" destroy-on-close>
|
||||
<div v-if="activePayment" class="payload-details">
|
||||
<div class="payload-block">
|
||||
<div class="payload-title">请求报文 Payload</div>
|
||||
<pre class="json-box"><code>{{ jsonText(activePayment.raw_request) }}</code></pre>
|
||||
</div>
|
||||
<div class="payload-block">
|
||||
<div class="payload-title">响应报文 Payload</div>
|
||||
<pre class="json-box"><code>{{ jsonText(activePayment.raw_response) }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.payment-detail h3 {
|
||||
margin: 18px 0 8px;
|
||||
font-size: 15px;
|
||||
.finance-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 4px 32px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
/* ================= 头部卡片 ================= */
|
||||
.page-header-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.eyebrow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-grid p {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
/* ================= 核心指标卡片 ================= */
|
||||
.summary-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-grid span {
|
||||
display: block;
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.metric-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.metric-card--pay::before {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.metric-card--refund::before {
|
||||
background: linear-gradient(90deg, #f43f5e, #fb7185);
|
||||
}
|
||||
|
||||
.metric-card--total::before {
|
||||
background: linear-gradient(90deg, #6366f1, #818cf8);
|
||||
}
|
||||
|
||||
.metric-icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.emerald {
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.rose {
|
||||
background: #fff1f2;
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.indigo {
|
||||
background: #eef2ff;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.emerald-val {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.rose-val {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.metric-sub-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.metric-sub-pill.success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.metric-sub-pill.danger {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.metric-sub-text {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ================= 筛选面板 ================= */
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.detail-grid strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
.full-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
/* ================= 表格卡片 ================= */
|
||||
.table-section-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #059669;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
|
||||
background-color: #f8fafc !important;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.mono-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.provider-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.provider-tag {
|
||||
width: fit-content;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pay-way {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.biz-type-text {
|
||||
font-size: 12.5px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.amount-val {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pay-val {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.refund-val {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.order-user-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.order-link {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.order-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.channel-no {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* ================= 报文弹窗 ================= */
|
||||
.payload-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.payload-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.json-box {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
BottomLeft,
|
||||
Coin,
|
||||
DocumentChecked,
|
||||
Refresh,
|
||||
Search,
|
||||
TopRight,
|
||||
Wallet,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/features/admin/api/adminWallet'
|
||||
@@ -65,11 +73,11 @@ async function handlePageChange() {
|
||||
await loadLedger()
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return formatCentWithSymbol(value)
|
||||
function money(value: number | undefined | null) {
|
||||
return formatCentWithSymbol(value || 0)
|
||||
}
|
||||
|
||||
function directionType(direction: string) {
|
||||
function directionType(direction: string): 'success' | 'danger' | 'warning' | 'info' {
|
||||
if (direction === 'in') return 'success'
|
||||
if (direction === 'out') return 'danger'
|
||||
return 'warning'
|
||||
@@ -81,95 +89,544 @@ function directionLabel(direction: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Wallet Ledger</p>
|
||||
<h1>资金流水</h1>
|
||||
<p>查看订单金额、押金、冻结、解冻、退款和结算流水。</p>
|
||||
<div class="finance-page-container">
|
||||
<!-- 头部卡片 -->
|
||||
<header class="page-header-card">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<div class="eyebrow-tag">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
<span>WALLET LEDGER</span>
|
||||
</div>
|
||||
<h1 class="page-title">资金流水</h1>
|
||||
<p class="page-subtitle">
|
||||
全站用户钱包账户的订单金额、押金冻结、结算解冻及扣划流水台账。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadLedger">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="loadLedger"
|
||||
>查询</el-button
|
||||
</header>
|
||||
|
||||
<!-- 汇总指标卡片 -->
|
||||
<section class="summary-metric-grid">
|
||||
<div class="metric-card metric-card--in">
|
||||
<div class="metric-icon-wrap emerald">
|
||||
<el-icon><BottomLeft /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页入账合计</span>
|
||||
<strong class="metric-val emerald-val">{{ money(inAmount) }}</strong>
|
||||
<span class="metric-sub-pill success">资金流入</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--out">
|
||||
<div class="metric-icon-wrap rose">
|
||||
<el-icon><TopRight /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页出账合计</span>
|
||||
<strong class="metric-val rose-val">{{ money(outAmount) }}</strong>
|
||||
<span class="metric-sub-pill danger">资金流出</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--total">
|
||||
<div class="metric-icon-wrap indigo">
|
||||
<el-icon><Coin /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">总流水记录</span>
|
||||
<strong class="metric-val">{{ total }} 笔</strong>
|
||||
<span class="metric-sub-text">钱包资金变动明细</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 筛选面板 -->
|
||||
<section class="section-card filter-card">
|
||||
<el-form class="filter-grid" label-position="top" @keyup.enter="loadLedger">
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="按用户 ID 检索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单 ID">
|
||||
<el-input v-model="filters.order_id" clearable placeholder="按订单 ID 检索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select
|
||||
v-model="filters.biz_type"
|
||||
clearable
|
||||
placeholder="全部业务类型"
|
||||
class="full-control"
|
||||
>
|
||||
<el-option label="订单冻结" value="order_freeze" />
|
||||
<el-option label="订单取消释放" value="order_cancel_release" />
|
||||
<el-option label="订单结算" value="order_settlement" />
|
||||
<el-option label="押金退回" value="deposit_refund" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<!-- 资金流水表格 -->
|
||||
<section class="section-card table-section-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-header-title">
|
||||
<el-icon class="header-icon"><DocumentChecked /></el-icon>
|
||||
<span>钱包变动流水台账</span>
|
||||
</div>
|
||||
<span class="count-badge">共 {{ total }} 条记录</span>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
class="custom-enterprise-table"
|
||||
:data="ledger"
|
||||
stripe
|
||||
empty-text="暂无资金流水记录"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table-column label="流水号" min-width="210" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<span class="mono-code">{{ row.ledger_no }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>总条数</span>
|
||||
<strong>{{ total }} 笔</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>入账合计</span>
|
||||
<strong>{{ money(inAmount) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>出账合计</span>
|
||||
<strong>{{ money(outAmount) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<el-table-column label="关联用户" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<span class="user-title">{{ row.user_phone || `用户 ID: ${row.user_id}` }}</span>
|
||||
<small v-if="row.user_phone" class="user-sub">UID: {{ row.user_id }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-form class="filter-panel" label-position="top">
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input v-model="filters.user_id" clearable placeholder="按用户筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订单 ID">
|
||||
<el-input v-model="filters.order_id" clearable placeholder="按订单筛选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="filters.biz_type" clearable placeholder="全部业务" class="full-control">
|
||||
<el-option label="订单冻结" value="order_freeze" />
|
||||
<el-option label="订单取消释放" value="order_cancel_release" />
|
||||
<el-option label="订单结算" value="order_settlement" />
|
||||
<el-option label="押金退回" value="deposit_refund" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table-column label="关联订单" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<RouterLink
|
||||
v-if="row.order_id"
|
||||
class="order-link"
|
||||
:to="adminPath(`orders/${row.order_id}`)"
|
||||
>
|
||||
{{ row.order_no || `订单 #${row.order_id}` }}
|
||||
</RouterLink>
|
||||
<span v-else class="muted-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="ledger">
|
||||
<el-table-column prop="ledger_no" label="流水号" min-width="230" />
|
||||
<el-table-column label="用户" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ row.user_phone || `用户 ${row.user_id}` }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<RouterLink v-if="row.order_id" :to="adminPath(`orders/${row.order_id}`)">{{
|
||||
row.order_no || row.order_id
|
||||
}}</RouterLink>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="biz_type" label="业务类型" width="150" />
|
||||
<el-table-column label="方向" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="directionType(row.direction)">{{ directionLabel(row.direction) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="110">
|
||||
<template #default="{ row }">{{ money(Number(row.amount_cent)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额类型" width="110">
|
||||
<template #default="{ row }">{{ balanceTypeLabel(row.balance_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变化后余额" width="130">
|
||||
<template #default="{ row }">{{ money(Number(row.balance_after_cent)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column label="业务类型" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain" class="biz-tag">{{ row.biz_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
<el-table-column label="资金流向" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="directionType(row.direction)" class="direction-tag">
|
||||
{{ directionLabel(row.direction) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="变动金额" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val" :class="row.direction === 'in' ? 'in-val' : 'out-val'">
|
||||
{{ row.direction === 'in' ? '+' : '-' }}{{ money(Number(row.amount_cent)) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="余额账户" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="balance-type">{{ balanceTypeLabel(row.balance_type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="变动后余额" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-val">{{ money(Number(row.balance_after_cent)) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="记录时间" min-width="165">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.finance-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 4px 32px;
|
||||
}
|
||||
|
||||
/* ================= 头部卡片 ================= */
|
||||
.page-header-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.eyebrow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: #0284c7;
|
||||
background: #f0f9ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ================= 核心指标卡片 ================= */
|
||||
.summary-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.metric-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.metric-card--in::before {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.metric-card--out::before {
|
||||
background: linear-gradient(90deg, #f43f5e, #fb7185);
|
||||
}
|
||||
|
||||
.metric-card--total::before {
|
||||
background: linear-gradient(90deg, #6366f1, #818cf8);
|
||||
}
|
||||
|
||||
.metric-icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.emerald {
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.rose {
|
||||
background: #fff1f2;
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.indigo {
|
||||
background: #eef2ff;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.emerald-val {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.rose-val {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.metric-sub-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.metric-sub-pill.success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.metric-sub-pill.danger {
|
||||
background: #fff1f2;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.metric-sub-text {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ================= 筛选面板 ================= */
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.full-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ================= 表格卡片 ================= */
|
||||
.table-section-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #0284c7;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #0284c7;
|
||||
background: #f0f9ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
|
||||
background-color: #f8fafc !important;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.mono-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.user-title {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.user-sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.order-link {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.order-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.biz-tag {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.direction-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-val {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.in-val {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.out-val {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.balance-type {
|
||||
font-size: 12.5px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
Clock,
|
||||
DocumentChecked,
|
||||
Money,
|
||||
Refresh,
|
||||
Search,
|
||||
Tickets,
|
||||
User,
|
||||
View,
|
||||
Wallet,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -9,9 +21,10 @@ import {
|
||||
confirmPayment,
|
||||
type WithdrawalDetail,
|
||||
} from '@/features/admin/api/adminWithdrawal'
|
||||
import { formatCent } from '@/shared/utils/money'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
import WithdrawalDetailDialog from '../components/WithdrawalDetailDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -32,9 +45,9 @@ const showDetailDialog = ref(false)
|
||||
const selectedWithdrawal = ref<WithdrawalDetail | null>(null)
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '待审核', value: 'pending' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '处理中 (待打款)', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已拒绝', value: 'rejected' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
@@ -51,6 +64,16 @@ const withdrawalReturnQuery = computed<Record<string, string>>(() => {
|
||||
return query
|
||||
})
|
||||
|
||||
const pendingCount = computed(() => withdrawals.value.filter(w => w.status === 'pending').length)
|
||||
const processingCount = computed(
|
||||
() => withdrawals.value.filter(w => w.status === 'processing').length
|
||||
)
|
||||
const completedAmount = computed(() =>
|
||||
withdrawals.value
|
||||
.filter(w => w.status === 'completed')
|
||||
.reduce((sum, w) => sum + Number(w.actual_amount_cent || w.amount_cent || 0), 0)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
restoreListState()
|
||||
void loadWithdrawals()
|
||||
@@ -94,7 +117,7 @@ async function loadWithdrawals() {
|
||||
withdrawals.value = result.items
|
||||
total.value = result.total
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.response?.data?.message || '加载失败')
|
||||
ElMessage.error(error.response?.data?.message || '提现列表加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -105,12 +128,18 @@ function handleFilter() {
|
||||
loadWithdrawals()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.value.status = ''
|
||||
filters.value.user_id = undefined
|
||||
currentPage.value = 1
|
||||
void loadWithdrawals()
|
||||
}
|
||||
|
||||
function openDetail(withdrawal: WithdrawalDetail) {
|
||||
selectedWithdrawal.value = withdrawal
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
/** 跳转订单管理页并预填号主搜索,快速查看该号主相关订单 */
|
||||
function goToOwnerOrders(withdrawal: WithdrawalDetail) {
|
||||
const keyword = withdrawal.user_phone?.trim() || String(withdrawal.user_id || '')
|
||||
void router.push({
|
||||
@@ -153,7 +182,7 @@ async function handleReview(withdrawal: WithdrawalDetail, approved: boolean) {
|
||||
async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
|
||||
try {
|
||||
const { value: remark } = await ElMessageBox.prompt(
|
||||
'请输入打款备注(如:已通过支付宝转账)',
|
||||
'请输入打款备注(如:已通过支付宝/微信转账)',
|
||||
'确认打款',
|
||||
{
|
||||
confirmButtonText: '确认完成',
|
||||
@@ -161,7 +190,7 @@ async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder: '输入打款备注...',
|
||||
inputValidator: value => {
|
||||
return value && value.trim().length > 0
|
||||
return !!(value && value.trim().length > 0)
|
||||
},
|
||||
inputErrorMessage: '请输入打款备注',
|
||||
}
|
||||
@@ -183,15 +212,15 @@ async function handleConfirmPayment(withdrawal: WithdrawalDetail) {
|
||||
function statusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
processing: '待打款',
|
||||
completed: '已打款',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function statusType(status: string) {
|
||||
function statusType(status: string): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||
const types: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
|
||||
pending: 'warning',
|
||||
processing: 'primary',
|
||||
@@ -215,29 +244,90 @@ function onDetailDialogSaved() {
|
||||
showDetailDialog.value = false
|
||||
loadWithdrawals()
|
||||
}
|
||||
|
||||
function money(cent: number | undefined | null) {
|
||||
return formatCentWithSymbol(cent || 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Withdrawals</p>
|
||||
<h1>提现管理</h1>
|
||||
<p>审核用户提现申请并确认打款</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="loadWithdrawals">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="finance-page-container">
|
||||
<!-- 头部卡片 -->
|
||||
<header class="page-header-card">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<div class="eyebrow-tag">
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>WITHDRAWALS</span>
|
||||
</div>
|
||||
<h1 class="page-title">提现审核</h1>
|
||||
<p class="page-subtitle">审核号主用户钱包提现申请,核验转账账户并执行打款放款处理。</p>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<el-card shadow="never" style="margin-bottom: 16px">
|
||||
<el-form :model="filters" inline>
|
||||
<el-form-item label="状态">
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="handleFilter">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 汇总指标卡片 -->
|
||||
<section class="summary-metric-grid">
|
||||
<div class="metric-card metric-card--review">
|
||||
<div class="metric-icon-wrap amber">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页待初审</span>
|
||||
<strong class="metric-val amber-val">{{ pendingCount }} 笔</strong>
|
||||
<span class="metric-sub-pill warning">待审核</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--pay">
|
||||
<div class="metric-icon-wrap indigo">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页待打款</span>
|
||||
<strong class="metric-val indigo-val">{{ processingCount }} 笔</strong>
|
||||
<span class="metric-sub-pill info">已过审待放款</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--paid">
|
||||
<div class="metric-icon-wrap emerald">
|
||||
<el-icon><Check /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">本页已打款总额</span>
|
||||
<strong class="metric-val emerald-val">{{ money(completedAmount) }}</strong>
|
||||
<span class="metric-sub-pill success">已完成出款</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card metric-card--total">
|
||||
<div class="metric-icon-wrap slate">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
</div>
|
||||
<div class="metric-info">
|
||||
<span class="metric-label">总申请记录</span>
|
||||
<strong class="metric-val">{{ total }} 笔</strong>
|
||||
<span class="metric-sub-text">所有提现记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 筛选卡片 -->
|
||||
<section class="section-card filter-card">
|
||||
<el-form class="filter-grid" label-position="top" @keyup.enter="handleFilter">
|
||||
<el-form-item label="提现状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="全部"
|
||||
style="width: 150px"
|
||||
placeholder="全部状态"
|
||||
class="full-control"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option
|
||||
@@ -248,106 +338,140 @@ function onDetailDialogSaved() {
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID">
|
||||
|
||||
<el-form-item label="用户 ID">
|
||||
<el-input
|
||||
v-model.number="filters.user_id"
|
||||
placeholder="输入用户ID"
|
||||
style="width: 150px"
|
||||
placeholder="输入用户 ID 检索"
|
||||
clearable
|
||||
@clear="handleFilter"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleFilter"> 查询 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<!-- 提现列表 -->
|
||||
<el-table v-loading="loading" class="table-panel" :data="withdrawals">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="withdraw_no" label="提现单号" min-width="180" />
|
||||
<el-table-column label="用户" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.user_nickname }}</div>
|
||||
<div style="font-size: 12px; color: #999">{{ row.user_phone }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提现金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #f56c6c; font-weight: 600"> ¥{{ formatCent(row.amount_cent) }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款方式" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tag size="small" style="margin-right: 4px">
|
||||
{{ accountTypeLabel(row.account_type) }}
|
||||
</el-tag>
|
||||
{{ row.account_name }}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 4px">
|
||||
{{ row.account_no }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">
|
||||
{{ statusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ new Date(row.created_at).toLocaleString('zh-CN') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openDetail(row)"> 详情 </el-button>
|
||||
<el-button size="small" type="primary" plain @click="goToOwnerOrders(row)">
|
||||
号主订单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="handleReview(row, true)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleReview(row, false)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'processing'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleConfirmPayment(row)"
|
||||
>
|
||||
确认打款
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 提现列表表格 -->
|
||||
<section class="section-card table-section-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-header-title">
|
||||
<el-icon class="header-icon"><DocumentChecked /></el-icon>
|
||||
<span>提现申请明细</span>
|
||||
</div>
|
||||
<span class="count-badge">共 {{ total }} 条申请</span>
|
||||
</div>
|
||||
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: center"
|
||||
@size-change="loadWithdrawals"
|
||||
@current-change="loadWithdrawals"
|
||||
/>
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
class="custom-enterprise-table"
|
||||
:data="withdrawals"
|
||||
stripe
|
||||
empty-text="暂无提现记录"
|
||||
>
|
||||
<el-table-column label="提现单号" min-width="190" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="withdraw-no-cell">
|
||||
<span class="mono-code">{{ row.withdraw_no }}</span>
|
||||
<small class="id-tag">#ID: {{ row.id }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="申请用户" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="user-cell">
|
||||
<div class="user-name-line">
|
||||
<el-icon class="user-icon"><User /></el-icon>
|
||||
<span class="user-name">{{ row.user_nickname || '-' }}</span>
|
||||
</div>
|
||||
<small class="user-sub">{{ row.user_phone || `UID: ${row.user_id}` }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="提现金额" min-width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-stack">
|
||||
<strong class="withdraw-amount">{{ money(row.amount_cent) }}</strong>
|
||||
<small v-if="row.fee_cent" class="fee-text">扣费 {{ money(row.fee_cent) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="收款账户信息" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div class="account-cell">
|
||||
<div class="account-header">
|
||||
<el-tag size="small" effect="plain" class="acc-tag">
|
||||
{{ accountTypeLabel(row.account_type) }}
|
||||
</el-tag>
|
||||
<span class="acc-name">{{ row.account_name || '-' }}</span>
|
||||
</div>
|
||||
<small class="acc-no">{{ row.account_no }}</small>
|
||||
<small v-if="row.bank_name" class="bank-name">{{ row.bank_name }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" class="status-pill">
|
||||
{{ statusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="申请时间" min-width="165">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="240" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-cell">
|
||||
<el-button link type="primary" :icon="View" @click="openDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="goToOwnerOrders(row)"> 号主订单 </el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleReview(row, true)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleReview(row, false)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'processing'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleConfirmPayment(row)"
|
||||
>
|
||||
确认打款
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<AdminTablePagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
@change="loadWithdrawals"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<WithdrawalDetailDialog
|
||||
@@ -356,52 +480,421 @@ function onDetailDialogSaved() {
|
||||
:return-query="withdrawalReturnQuery"
|
||||
@saved="onDetailDialogSaved"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 24px;
|
||||
.finance-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 4px 32px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
/* ================= 头部卡片 ================= */
|
||||
.page-header-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 28px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex: 1;
|
||||
.title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
.eyebrow-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: #7c3aed;
|
||||
background: #f5f3ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
.page-title {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
/* ================= 核心指标卡片 ================= */
|
||||
.summary-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.metric-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.metric-card--review::before {
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
|
||||
.metric-card--pay::before {
|
||||
background: linear-gradient(90deg, #6366f1, #818cf8);
|
||||
}
|
||||
|
||||
.metric-card--paid::before {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.metric-card--total::before {
|
||||
background: linear-gradient(90deg, #64748b, #94a3b8);
|
||||
}
|
||||
|
||||
.metric-icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.amber {
|
||||
background: #fffbeb;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.indigo {
|
||||
background: #eef2ff;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.emerald {
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-icon-wrap.slate {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.amber-val {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.indigo-val {
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.emerald-val {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.metric-sub-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.metric-sub-pill.warning {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.metric-sub-pill.info {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.metric-sub-pill.success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.metric-sub-text {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ================= 筛选面板 ================= */
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 22px 24px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.04),
|
||||
0 4px 12px rgba(15, 23, 42, 0.02);
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-grid :deep(.el-form-item__label) {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.full-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ================= 表格卡片 ================= */
|
||||
.table-section-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #7c3aed;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #7c3aed;
|
||||
background: #f5f3ff;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
|
||||
background-color: #f8fafc !important;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.withdraw-no-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mono-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.id-tag {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.user-name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-sub {
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.amount-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.withdraw-amount {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.fee-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.account-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.acc-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.acc-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.acc-no {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bank-name {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user