- 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
179 lines
6.3 KiB
Vue
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>
|