后台“资金流水

This commit is contained in:
yml
2026-05-22 17:43:45 +08:00
parent 7df557d668
commit c9e6ea2b71
28 changed files with 1271 additions and 34 deletions
+38
View File
@@ -0,0 +1,38 @@
import { apiClient } from './client'
export interface AdminUserItem {
id: number
phone: string
nickname: string
realname_status: string
risk_status: string
credit_score: number
status: string
order_count: number
listing_count: number
dispute_count: number
last_login_at?: string
created_at: string
updated_at: string
}
interface ApiResponse<T> {
code: string
message: string
data: T
}
export async function fetchAdminUsers() {
const { data } = await apiClient.get<ApiResponse<{ items: AdminUserItem[] }>>('/admin/users')
return data.data.items
}
export async function freezeAdminUser(id: number, reason: string) {
const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(`/admin/users/${id}/freeze`, { reason })
return data.data
}
export async function unfreezeAdminUser(id: number) {
const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(`/admin/users/${id}/unfreeze`)
return data.data
}
+38
View File
@@ -0,0 +1,38 @@
import { apiClient } from './client'
export interface AdminWalletLedger {
id: number
ledger_no: string
user_id: number
user_phone: string
user_nickname: string
order_id?: number
order_no: string
direction: string
amount: number
balance_after: number
balance_type: string
biz_type: string
biz_no: string
remark: string
created_at: string
}
export interface AdminWalletLedgerQuery {
user_id?: string
order_id?: string
biz_type?: string
limit?: number
}
interface ApiResponse<T> {
code: string
message: string
data: T
}
export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {}) {
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
const { data } = await apiClient.get<ApiResponse<{ items: AdminWalletLedger[] }>>('/admin/wallet/ledger', { params })
return data.data.items
}
+17
View File
@@ -7,6 +7,8 @@ export interface Order {
account_id: number
owner_id: number
renter_id: number
owner_phone?: string
renter_phone?: string
title: string
server_region: string
login_platform: string
@@ -55,11 +57,21 @@ export async function fetchOrders() {
return data.data.items
}
export async function fetchAdminOrders() {
const { data } = await apiClient.get<ApiResponse<{ items: Order[] }>>('/admin/orders')
return data.data.items
}
export async function fetchOrder(id: string | number) {
const { data } = await apiClient.get<ApiResponse<Order>>(`/orders/${id}`)
return data.data
}
export async function fetchAdminOrder(id: string | number) {
const { data } = await apiClient.get<ApiResponse<Order>>(`/admin/orders/${id}`)
return data.data
}
export async function cancelOrder(id: number) {
const { data } = await apiClient.post<ApiResponse<{ cancelled: boolean }>>(`/orders/${id}/cancel`)
return data.data
@@ -75,6 +87,11 @@ export async function fetchHandoffRecords(id: string | number) {
return data.data.items
}
export async function fetchAdminHandoffRecords(id: string | number) {
const { data } = await apiClient.get<ApiResponse<{ items: HandoffRecord[] }>>(`/admin/orders/${id}/handoff-records`)
return data.data.items
}
export async function confirmReceive(id: number) {
const { data } = await apiClient.post<ApiResponse<{ received: boolean }>>(`/orders/${id}/confirm-receive`)
return data.data
+4 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DataLine, DocumentChecked, Operation, ScaleToOriginal, SwitchButton } from '@element-plus/icons-vue'
import { DataLine, DocumentChecked, Operation, ScaleToOriginal, SwitchButton, Tickets, User, Wallet } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
@@ -12,8 +12,11 @@ const adminSession = useAdminSessionStore()
const navItems = [
{ label: '仪表盘', to: '/admin/dashboard', icon: DataLine },
{ label: '用户管理', to: '/admin/users', icon: User },
{ label: '订单管理', to: '/admin/orders', icon: Tickets },
{ label: '商品审核', to: '/admin/listings/review', icon: DocumentChecked },
{ label: '仲裁中心', to: '/admin/disputes', icon: ScaleToOriginal },
{ label: '资金流水', to: '/admin/wallet-ledger', icon: Wallet },
{ label: '系统配置', to: '/admin/system-configs', icon: Operation },
]
+24
View File
@@ -25,6 +25,24 @@ const router = createRouter({
component: () => import('@/views/admin/AdminDashboardView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/users',
name: 'admin-users',
component: () => import('@/views/admin/AdminUsersView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/orders',
name: 'admin-orders',
component: () => import('@/views/admin/AdminOrdersView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/orders/:id',
name: 'admin-order-detail',
component: () => import('@/views/admin/AdminOrderDetailView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/listings/review',
name: 'admin-listing-review',
@@ -37,6 +55,12 @@ const router = createRouter({
component: () => import('@/views/admin/AdminDisputesView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/wallet-ledger',
name: 'admin-wallet-ledger',
component: () => import('@/views/admin/AdminWalletLedgerView.vue'),
meta: { layout: 'admin', requiresAdmin: true },
},
{
path: '/admin/system-configs',
name: 'admin-system-configs',
+154 -31
View File
@@ -91,6 +91,7 @@ a {
.admin-sidebar {
display: flex;
flex-direction: column;
min-width: 0;
border-right: 1px solid #dfe5ec;
background: #111827;
padding: 20px 16px;
@@ -130,6 +131,7 @@ a {
.admin-workspace {
min-width: 0;
overflow: hidden;
}
.admin-topbar {
@@ -155,8 +157,13 @@ a {
}
.admin-main {
width: 100%;
min-width: 0;
padding: 28px;
padding: 28px clamp(18px, 2.4vw, 36px);
}
.admin-main .page {
max-width: none;
}
.admin-login-shell {
@@ -231,7 +238,8 @@ a {
}
.page {
max-width: 1120px;
width: 100%;
max-width: 1440px;
}
.page-header {
@@ -260,7 +268,7 @@ h1 {
.metric-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-top: 28px;
}
@@ -285,12 +293,12 @@ h1 {
}
.dashboard-metrics {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.dashboard-panels {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
@@ -433,6 +441,15 @@ h1 {
margin-top: 28px;
}
.table-panel {
width: 100%;
overflow-x: auto;
}
.table-panel .el-table__inner-wrapper {
min-width: 760px;
}
.listing-form {
max-width: 860px;
border: 1px solid #e4e7ed;
@@ -448,7 +465,7 @@ h1 {
}
.order-panel {
max-width: 520px;
max-width: min(100%, 560px);
margin-top: 28px;
border: 1px solid #e4e7ed;
border-radius: 8px;
@@ -503,6 +520,50 @@ h1 {
line-height: 1.6;
}
.toolbar-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
.filter-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px 16px;
margin-top: 24px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #ffffff;
padding: 16px;
}
.filter-panel .el-form-item {
margin-bottom: 0;
}
.table-subtext {
display: block;
margin-top: 4px;
color: #6b7785;
font-size: 12px;
}
.code-panel {
max-width: none;
}
.code-panel pre {
overflow-x: auto;
margin: 0;
border-radius: 6px;
background: #111827;
color: #dbe4ee;
padding: 14px;
line-height: 1.6;
}
.notification-list {
display: grid;
gap: 12px;
@@ -551,12 +612,64 @@ h1 {
justify-content: flex-end;
}
@media (max-width: 760px) {
.app-shell {
@media (max-width: 1180px) {
.admin-shell {
grid-template-columns: 220px 1fr;
}
.admin-sidebar {
padding: 18px 12px;
}
.admin-nav-link {
padding: 0 10px;
}
}
@media (max-width: 900px) {
.admin-shell {
grid-template-columns: 1fr;
}
.admin-shell {
.admin-sidebar {
position: sticky;
top: 0;
z-index: 10;
border-right: 0;
border-bottom: 1px solid #1f2937;
padding: 12px 14px;
}
.admin-brand {
min-height: 36px;
}
.admin-nav {
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 8px;
margin-top: 12px;
overflow-x: auto;
padding-bottom: 2px;
}
.admin-nav-link {
min-height: 38px;
white-space: nowrap;
}
.admin-topbar {
min-height: 58px;
padding: 0 16px;
}
.admin-main {
padding: 22px 16px;
}
}
@media (max-width: 760px) {
.app-shell {
grid-template-columns: 1fr;
}
@@ -574,32 +687,10 @@ h1 {
overflow-x: auto;
}
.admin-sidebar {
position: sticky;
top: 0;
z-index: 10;
border-right: 0;
border-bottom: 1px solid #1f2937;
}
.admin-nav {
grid-auto-flow: column;
grid-auto-columns: max-content;
overflow-x: auto;
}
.admin-topbar {
padding: 0 16px;
}
.main {
padding: 24px 16px;
}
.admin-main {
padding: 24px 16px;
}
.metric-grid {
grid-template-columns: 1fr;
}
@@ -615,4 +706,36 @@ h1 {
.page-header-row {
display: grid;
}
.toolbar-actions {
justify-content: flex-start;
}
}
@media (max-width: 520px) {
.admin-topbar {
align-items: flex-start;
flex-direction: column;
justify-content: center;
gap: 10px;
padding: 12px 16px;
}
.admin-topbar .el-button {
width: 100%;
}
.dashboard-panels {
grid-template-columns: 1fr;
}
.admin-captcha-row,
.code-row {
grid-template-columns: 1fr;
}
.captcha-image-button,
.captcha-image-button img {
width: 100%;
}
}
@@ -91,6 +91,14 @@ function money(value?: number) {
<div class="order-panel dashboard-panel">
<h2>后台入口</h2>
<RouterLink class="pending-row" to="/admin/users">
<span>用户管理</span>
<strong>进入</strong>
</RouterLink>
<RouterLink class="pending-row" to="/admin/orders">
<span>订单管理</span>
<strong>进入</strong>
</RouterLink>
<RouterLink class="pending-row" to="/admin/disputes">
<span>仲裁中心</span>
<strong>进入</strong>
@@ -85,7 +85,7 @@ function readError(error: unknown, fallback: string) {
<el-table-column prop="price_hourly" label="时租" width="90" />
<el-table-column prop="deposit_amount" label="押金" width="90" />
<el-table-column prop="updated_at" label="提交时间" min-width="180" />
<el-table-column label="操作" width="170" fixed="right">
<el-table-column label="操作" width="170">
<template #default="{ row }">
<el-button size="small" type="primary" :loading="submitting" @click="handleApprove(row)">通过</el-button>
<el-button size="small" type="danger" @click="openReject(row)">拒绝</el-button>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { fetchAdminHandoffRecords, fetchAdminOrder, type HandoffRecord, type Order } from '@/api/orders'
const route = useRoute()
const loading = ref(false)
const order = ref<Order | null>(null)
const handoffRecords = ref<HandoffRecord[]>([])
const snapshotText = computed(() => JSON.stringify(order.value?.account_snapshot || {}, null, 2))
onMounted(loadOrder)
async function loadOrder() {
loading.value = true
try {
order.value = await fetchAdminOrder(String(route.params.id))
handoffRecords.value = await fetchAdminHandoffRecords(String(route.params.id))
} finally {
loading.value = false
}
}
</script>
<template>
<section class="page" v-loading="loading">
<div v-if="order" class="page-header-row">
<div class="page-header">
<p class="eyebrow">{{ order.order_no }}</p>
<h1>订单详情</h1>
<p>{{ order.title }} · {{ order.server_region }} / {{ order.login_platform }}</p>
</div>
<RouterLink to="/admin/orders">
<el-button>返回列表</el-button>
</RouterLink>
</div>
<div v-if="order" class="metric-grid dashboard-metrics">
<div class="metric-card">
<span>订单状态</span>
<strong>{{ order.status }}</strong>
</div>
<div class="metric-card">
<span>交接状态</span>
<strong>{{ order.handoff_status }}</strong>
</div>
<div class="metric-card">
<span>租金</span>
<strong>¥{{ order.rent_amount }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ order.deposit_amount }}</strong>
</div>
</div>
<div v-if="order" class="dashboard-panels">
<div class="order-panel dashboard-panel">
<h2>用户信息</h2>
<p>租客{{ order.renter_phone || order.renter_id }}</p>
<p>号主{{ order.owner_phone || order.owner_id }}</p>
<p>租期{{ order.rent_hours }} 小时</p>
<p>开始{{ order.rent_start_at || '未开始' }}</p>
<p>结束{{ order.rent_end_at || '未设置' }}</p>
</div>
<div class="order-panel dashboard-panel">
<h2>交接记录</h2>
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
<div v-for="record in handoffRecords" :key="record.id" class="timeline-item">
<strong>{{ record.type }}</strong>
<p>{{ record.content }}</p>
<span>{{ record.created_at }}</span>
</div>
</div>
</div>
<div v-if="order" class="order-panel dashboard-panel code-panel">
<h2>账号快照</h2>
<pre>{{ snapshotText }}</pre>
</div>
</section>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { fetchAdminOrders, type Order } from '@/api/orders'
const loading = ref(false)
const orders = ref<Order[]>([])
const status = ref('')
const filteredOrders = computed(() => {
if (!status.value) return orders.value
return orders.value.filter((item) => item.status === status.value)
})
onMounted(loadOrders)
async function loadOrders() {
loading.value = true
try {
orders.value = await fetchAdminOrders()
} finally {
loading.value = false
}
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Orders</p>
<h1>订单管理</h1>
<p>查看全量订单交接状态金额和结算状态</p>
</div>
<div class="toolbar-actions">
<el-select v-model="status" clearable placeholder="订单状态" style="width: 180px">
<el-option label="待交接" value="pending_handoff" />
<el-option label="租赁中" value="renting" />
<el-option label="待归还确认" value="pending_return_confirm" />
<el-option label="申诉中" value="disputing" />
<el-option label="已完成" value="completed" />
<el-option label="已关闭" value="closed" />
<el-option label="已取消" value="cancelled" />
</el-select>
<el-button @click="loadOrders">刷新</el-button>
</div>
</div>
<el-table v-loading="loading" class="table-panel" :data="filteredOrders">
<el-table-column prop="order_no" label="订单号" min-width="230" />
<el-table-column prop="title" label="账号" min-width="170" />
<el-table-column prop="renter_phone" label="租客" min-width="130" />
<el-table-column prop="owner_phone" label="号主" min-width="130" />
<el-table-column prop="status" label="状态" width="140" />
<el-table-column prop="handoff_status" label="交接" width="180" />
<el-table-column prop="settlement_status" label="结算" width="120" />
<el-table-column prop="rent_amount" label="租金" width="100" />
<el-table-column prop="deposit_amount" label="押金" width="100" />
<el-table-column prop="created_at" label="创建时间" min-width="180" />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<RouterLink :to="`/admin/orders/${row.id}`">
<el-button size="small">详情</el-button>
</RouterLink>
</template>
</el-table-column>
</el-table>
</section>
</template>
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { fetchAdminUsers, freezeAdminUser, unfreezeAdminUser, type AdminUserItem } from '@/api/adminUsers'
const loading = ref(false)
const submitting = ref(false)
const users = ref<AdminUserItem[]>([])
const activeUser = ref<AdminUserItem | null>(null)
const freezeReason = ref('')
onMounted(loadUsers)
async function loadUsers() {
loading.value = true
try {
users.value = await fetchAdminUsers()
} finally {
loading.value = false
}
}
function openFreeze(row: AdminUserItem) {
activeUser.value = row
freezeReason.value = ''
}
async function handleFreeze() {
if (!activeUser.value) return
submitting.value = true
try {
await freezeAdminUser(activeUser.value.id, freezeReason.value)
ElMessage.success('用户已冻结')
activeUser.value = null
await loadUsers()
} catch (error) {
ElMessage.error(readError(error, '冻结失败'))
} finally {
submitting.value = false
}
}
async function handleUnfreeze(row: AdminUserItem) {
submitting.value = true
try {
await unfreezeAdminUser(row.id)
ElMessage.success('用户已解冻')
await loadUsers()
} catch (error) {
ElMessage.error(readError(error, '解冻失败'))
} finally {
submitting.value = false
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === 'object' && error && 'response' in error) {
const response = (error as { response?: { data?: { message?: string } } }).response
return response?.data?.message || fallback
}
return fallback
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Users</p>
<h1>用户管理</h1>
<p>查看用户实名风险信用和业务数量处理冻结与解冻</p>
</div>
<el-button @click="loadUsers">刷新</el-button>
</div>
<el-table v-loading="loading" class="table-panel" :data="users">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="nickname" label="昵称" min-width="130" />
<el-table-column prop="realname_status" label="实名" width="110" />
<el-table-column prop="risk_status" label="风险" width="110" />
<el-table-column prop="credit_score" label="信用分" width="100" />
<el-table-column prop="status" label="状态" width="110" />
<el-table-column prop="order_count" label="订单" width="90" />
<el-table-column prop="listing_count" label="发布" width="90" />
<el-table-column prop="dispute_count" label="申诉" width="90" />
<el-table-column prop="last_login_at" label="最近登录" min-width="180" />
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button v-if="row.status === 'active'" size="small" type="danger" :loading="submitting" @click="openFreeze(row)">
冻结
</el-button>
<el-button v-else size="small" type="primary" :loading="submitting" @click="handleUnfreeze(row)">解冻</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :model-value="!!activeUser" title="冻结用户" width="560px" @update:model-value="activeUser = null">
<div v-if="activeUser" class="dialog-body">
<p><strong>{{ activeUser.phone }}</strong> · {{ activeUser.nickname }}</p>
<el-input v-model="freezeReason" type="textarea" :rows="4" placeholder="填写冻结原因,便于审计追踪" />
</div>
<template #footer>
<el-button @click="activeUser = null">取消</el-button>
<el-button type="danger" :loading="submitting" @click="handleFreeze">确认冻结</el-button>
</template>
</el-dialog>
</section>
</template>
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/api/adminWallet'
const loading = ref(false)
const ledger = ref<AdminWalletLedger[]>([])
const filters = reactive({
user_id: '',
order_id: '',
biz_type: '',
limit: 200,
})
const inAmount = computed(() =>
ledger.value.filter((item) => item.direction === 'in').reduce((sum, item) => sum + Number(item.amount || 0), 0),
)
const outAmount = computed(() =>
ledger.value.filter((item) => item.direction === 'out').reduce((sum, item) => sum + Number(item.amount || 0), 0),
)
const frozenAmount = computed(() =>
ledger.value.filter((item) => item.balance_type === 'frozen').reduce((sum, item) => sum + Number(item.amount || 0), 0),
)
onMounted(loadLedger)
async function loadLedger() {
loading.value = true
try {
ledger.value = await fetchAdminWalletLedger(filters)
} finally {
loading.value = false
}
}
function resetFilters() {
filters.user_id = ''
filters.order_id = ''
filters.biz_type = ''
filters.limit = 200
void loadLedger()
}
function money(value: number) {
return `¥${value.toFixed(2)}`
}
function directionType(direction: string) {
return direction === 'in' ? 'success' : 'danger'
}
function directionLabel(direction: string) {
return direction === 'in' ? '入账' : '出账'
}
</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>
<div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" :loading="loading" @click="loadLedger">查询</el-button>
</div>
</div>
<div class="metric-grid">
<div class="metric-card">
<span>当前结果</span>
<strong>{{ ledger.length }} </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 class="metric-card">
<span>冻结相关金额</span>
<strong>{{ money(frozenAmount) }}</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-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-item label="查询条数">
<el-input-number v-model="filters.limit" :min="20" :max="500" :step="20" class="full-control" />
</el-form-item>
</el-form>
<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="160">
<template #default="{ row }">
<strong>{{ row.user_phone || `用户 ${row.user_id}` }}</strong>
<span class="table-subtext">ID: {{ row.user_id }}</span>
</template>
</el-table-column>
<el-table-column label="订单" min-width="180">
<template #default="{ row }">
<RouterLink v-if="row.order_id" :to="`/admin/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)) }}</template>
</el-table-column>
<el-table-column prop="balance_type" label="余额类型" width="110" />
<el-table-column label="变化后余额" width="130">
<template #default="{ row }">{{ money(Number(row.balance_after)) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="220" />
<el-table-column prop="created_at" label="创建时间" min-width="180" />
</el-table>
</section>
</template>