admin_new/src/modules/user/FinanceListView.vue
Sisyphus 78360d1d6e feat(F006-W3xC): 7 个新 view 接真 API (W3.x C P0 全部)
- P0.1 数据统计首页 (走 /sys/openapi/order/list 真 API, 4 stat 500/¥47M 真值)
- P0.2 supplier/inquiry/inquiry 重定向 /inquiry (2 个 redirect)
- P0.3 /user/user 用户列表 (2284 真实用户)
- P0.4 /other/operateguide 内容列表 (1 条真数据)
- P0.5 /api/lunbo 轮播占位 (后端 404 优雅提示)
- P0.6 /api/help 帮助中心占位 (后端 404 优雅提示)
- P0.7 /sys/region 行政区域树 (省市区 4 级 3000+ 节点)

同步: src/modules/user/{Enterprise,Finance}ListView.vue (Mac 端 working copy 补传)

P0.7 nginx 修复 (conf 改动非本次提交, 见 nginx-vue.conf.bak-20260704-194200):
- 新增 location = /admin/sys/region { rewrite /admin/index.html last; }
- 原因: location /admin/sys/ 代理抢 /admin/sys/region 走 gateway 返 401
- 已 docker compose restart frontend 生效, devbox 端 7 SPA 路径全 200
2026-07-04 20:09:11 +08:00

179 lines
6.3 KiB
Vue

<script setup lang="ts">
/**
* F006 财务管理 — 余额明细
*
* 后端端点 (BalanceDetailController @ /user/balancedetail):
* GET /user/balancedetail/page 分页列表
* GET /user/balancedetail/{id} 详情
*/
import { onMounted, ref, reactive } from 'vue'
import { httpGet } from '@/utils/request'
import DataTable from '@/components/DataTable/DataTable.vue'
interface BalanceDetail {
id: string
userId: string
type: number
money: number
balance: number
createDate: string
remark: string
orderId: string | null
orderNo: string | null
enterpriseId: string | null
enterpriseName: string | null
raw: any
}
const TYPE_MAP: Record<number, { label: string; type: string }> = {
1: { label: '收入', type: 'success' },
2: { label: '支出', type: 'danger' },
3: { label: '提现', type: 'warning' },
4: { label: '冻结', type: 'info' }
}
const list = ref<BalanceDetail[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const limit = ref(10)
const loadError = ref<string | null>(null)
const filters = reactive({
type: ''
})
function fmtDate(ts?: string | number | null) {
if (!ts) return '--'
const n = typeof ts === 'string' ? Date.parse(ts) : ts
if (!n) return String(ts)
const d = new Date(n)
if (isNaN(d.getTime())) return String(ts)
const pad = (x: number) => String(x).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
function fmtMoney(cent?: number | null) {
if (cent == null) return '--'
return '¥' + (cent / 100).toFixed(2)
}
async function load() {
loading.value = true
loadError.value = null
try {
const data = await httpGet<{ total: number; list: any[] }>('/user/balancedetail/page', {
page: page.value,
limit: limit.value,
...(filters.type !== '' ? { type: Number(filters.type) } : {})
})
list.value = (data.list || []).map((it: any) => ({
id: String(it.id),
userId: String(it.userId),
type: it.type ?? 0,
money: it.money ?? 0,
balance: it.balance ?? 0,
createDate: it.createDate || '--',
remark: it.remark || '--',
orderId: it.orderId ? String(it.orderId) : null,
orderNo: it.orderNo || null,
enterpriseId: it.enterpriseId ? String(it.enterpriseId) : null,
enterpriseName: it.enterpriseName || null,
raw: it
}))
total.value = data.total || 0
} catch (e: any) {
loadError.value = e?.msg || e?.message || '加载失败'
} finally {
loading.value = false
}
}
function onSearch() { page.value = 1; load() }
function onReset() { filters.type = ''; page.value = 1; load() }
function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
onMounted(() => { load() })
</script>
<template>
<div class="rr-sys-user">
<div class="rr-page-header">
<div class="rr-page-titles">
<h2 class="rr-page-title">财务管理</h2>
<p class="rr-page-sub">共 {{ total }} 条明细</p>
</div>
<div class="rr-page-actions">
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
</div>
</div>
<div class="rr-filters-row">
<el-select v-model="filters.type" placeholder="类型筛选" class="rr-type-select" clearable size="small">
<el-option value="1" label="收入" />
<el-option value="2" label="支出" />
<el-option value="3" label="提现" />
<el-option value="4" label="冻结" />
</el-select>
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
</div>
<div class="rr-list-card">
<DataTable
:columns="[
{ prop: 'enterpriseName', label: '企业', width: 180 },
{ prop: 'type', label: '类型', width: 90, slot: 'type' },
{ prop: 'money', label: '金额', width: 120, slot: 'money' },
{ prop: 'balance', label: '余额', width: 120, slot: 'balance' },
{ prop: 'orderNo', label: '订单号', width: 180 },
{ prop: 'remark', label: '备注', minWidth: 160 },
{ prop: 'createDate', label: '时间', width: 170, slot: 'createDate' }
]"
:data="list"
:loading="loading"
:total="total"
:page="page"
:limit="limit"
:show-pagination="true"
:error="loadError"
empty-text="暂无明细"
row-key="id"
@page-change="onPageChange"
@limit-change="onLimitChange"
>
<template #type="{ row }">
<el-tag v-if="TYPE_MAP[row.type]" :type="TYPE_MAP[row.type].type as any" size="small" effect="light">
{{ TYPE_MAP[row.type].label }}
</el-tag>
<span v-else class="rr-muted">--</span>
</template>
<template #money="{ row }">
<span class="rr-money">{{ fmtMoney(row.money) }}</span>
</template>
<template #balance="{ row }">
<span class="rr-money">{{ fmtMoney(row.balance) }}</span>
</template>
<template #createDate="{ row }">
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
</template>
</DataTable>
</div>
</div>
</template>
<style scoped>
.rr-sys-user { padding: 16px; }
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
.rr-page-actions { display: flex; gap: 8px; }
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
.rr-type-select { width: 140px; }
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
.rr-money { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--el-text-color-primary); }
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
.rr-muted { color: var(--el-text-color-placeholder); font-size: 12px; }
</style>