增加流水通知审计分页

This commit is contained in:
yml
2026-05-24 17:51:00 +08:00
parent 03e5d211dc
commit cb636e4e89
21 changed files with 295 additions and 120 deletions
+6 -3
View File
@@ -1,5 +1,7 @@
import { apiClient } from './client'
import { type PaginatedResult } from './orders'
export interface AdminAuditLog {
id: number
actor_type: string
@@ -19,7 +21,8 @@ export interface AdminAuditQuery {
actor_id?: string
action?: string
biz_type?: string
limit?: number
page?: number
page_size?: number
}
interface ApiResponse<T> {
@@ -30,6 +33,6 @@ interface ApiResponse<T> {
export async function fetchAdminAuditLogs(query: AdminAuditQuery = {}) {
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
const { data } = await apiClient.get<ApiResponse<{ items: AdminAuditLog[] }>>('/admin/audit-logs', { params })
return data.data.items
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminAuditLog>>>('/admin/audit-logs', { params })
return data.data
}
+6 -3
View File
@@ -1,5 +1,7 @@
import { apiClient } from './client'
import { type PaginatedResult } from './orders'
export interface AdminWalletLedger {
id: number
ledger_no: string
@@ -22,7 +24,8 @@ export interface AdminWalletLedgerQuery {
user_id?: string
order_id?: string
biz_type?: string
limit?: number
page?: number
page_size?: number
}
interface ApiResponse<T> {
@@ -33,6 +36,6 @@ interface ApiResponse<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
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminWalletLedger>>>('/admin/wallet/ledger', { params })
return data.data
}
+7 -3
View File
@@ -1,5 +1,7 @@
import { apiClient } from './client'
import { type PaginatedResult } from './orders'
export interface NotificationItem {
id: number
user_id: number
@@ -18,9 +20,11 @@ interface ApiResponse<T> {
data: T
}
export async function fetchNotifications() {
const { data } = await apiClient.get<ApiResponse<{ items: NotificationItem[] }>>('/notifications')
return data.data.items
export async function fetchNotifications(page = 1, pageSize = 20) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<NotificationItem>>>('/notifications', {
params: { page, page_size: pageSize },
})
return data.data
}
export async function markNotificationRead(id: number) {
+7
View File
@@ -84,6 +84,13 @@ interface ApiResponse<T> {
data: T
}
export interface PaginatedResult<T> {
items: T[]
total: number
page: number
page_size: number
}
export async function createOrder(listingId: number) {
const { data } = await apiClient.post<ApiResponse<Order>>('/orders', {
listing_id: listingId,
+7 -3
View File
@@ -1,5 +1,7 @@
import { apiClient } from './client'
import { type PaginatedResult } from './orders'
export interface WalletAccount {
user_id: number
available_balance: number
@@ -33,9 +35,11 @@ export async function fetchWalletBalance() {
return data.data
}
export async function fetchWalletLedger() {
const { data } = await apiClient.get<ApiResponse<{ items: WalletLedger[] }>>('/wallet/ledger')
return data.data.items
export async function fetchWalletLedger(page = 1, pageSize = 20) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<WalletLedger>>>('/wallet/ledger', {
params: { page, page_size: pageSize },
})
return data.data
}
export async function rechargeWallet(amount: number) {
@@ -7,18 +7,28 @@ import { formatDateTime } from '@/utils/time'
const loading = ref(false)
const notifications = ref<NotificationItem[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
onMounted(loadNotifications)
async function loadNotifications() {
loading.value = true
try {
notifications.value = await fetchNotifications()
const result = await fetchNotifications(currentPage.value, currentPageSize.value)
notifications.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
function handleSizeChange() {
currentPage.value = 1
loadNotifications()
}
async function markRead(id: number) {
await markNotificationRead(id)
ElMessage.success('已标记为已读')
@@ -51,5 +61,17 @@ async function markRead(id: number) {
</div>
</div>
</div>
<div class="pagination-wrap" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="loadNotifications"
@size-change="handleSizeChange"
/>
</div>
</section>
</template>
+27 -2
View File
@@ -9,6 +9,9 @@ import { formatDateTime } from '@/utils/time'
const loading = ref(false)
const account = ref<WalletAccount | null>(null)
const ledger = ref<WalletLedger[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const rechargeAmount = ref(100)
const recharging = ref(false)
@@ -17,14 +20,24 @@ onMounted(loadWallet)
async function loadWallet() {
loading.value = true
try {
const [balance, rows] = await Promise.all([fetchWalletBalance(), fetchWalletLedger()])
const [balance, result] = await Promise.all([fetchWalletBalance(), fetchWalletLedger(currentPage.value, currentPageSize.value)])
account.value = balance
ledger.value = rows
ledger.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
function handleSizeChange() {
currentPage.value = 1
loadWallet()
}
function loadLedgerPage() {
loadWallet()
}
async function handleRecharge() {
recharging.value = true
try {
@@ -92,6 +105,18 @@ function readError(error: unknown, fallback: string) {
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
</el-table>
<div class="pagination-wrap" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="loadLedgerPage"
@size-change="handleSizeChange"
/>
</div>
</section>
</template>
+35 -24
View File
@@ -8,22 +8,30 @@ import { formatDateTime } from '@/utils/time'
const loading = ref(false)
const logs = ref<AdminAuditLog[]>([])
const activeLog = ref<AdminAuditLog | null>(null)
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const filters = reactive({
actor_id: '',
action: '',
biz_type: '',
limit: 200,
})
const highRiskCount = computed(() => logs.value.filter((item) => item.action.includes('freeze') || item.action.includes('update')).length)
const uniqueActors = computed(() => new Set(logs.value.map((item) => item.actor_id)).size)
onMounted(loadLogs)
async function loadLogs() {
loading.value = true
try {
logs.value = await fetchAdminAuditLogs(filters)
const query = {
...filters,
page: currentPage.value,
page_size: currentPageSize.value,
}
const result = await fetchAdminAuditLogs(query)
logs.value = result.items
total.value = result.total
} finally {
loading.value = false
}
@@ -33,10 +41,15 @@ function resetFilters() {
filters.actor_id = ''
filters.action = ''
filters.biz_type = ''
filters.limit = 200
currentPage.value = 1
void loadLogs()
}
function handleSizeChange() {
currentPage.value = 1
loadLogs()
}
function actorName(row: AdminAuditLog) {
return row.actor_nickname || row.actor_username || `${row.actor_type} ${row.actor_id}`
}
@@ -59,7 +72,7 @@ function actionType(action: string) {
<div class="page-header">
<p class="eyebrow">Audit Logs</p>
<h1>审计日志</h1>
<p>查看后台管理员操作业务对象来源 IP 和操作明细用于追踪冻结配置修改等高风险动作</p>
<p>查看后台管理员操作业务对象和操作明细用于追踪冻结配置修改等高风险动作</p>
</div>
<div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button>
@@ -69,12 +82,8 @@ function actionType(action: string) {
<div class="metric-grid">
<div class="metric-card">
<span>当前结果</span>
<strong>{{ logs.length }} </strong>
</div>
<div class="metric-card">
<span>操作管理员</span>
<strong>{{ uniqueActors }} </strong>
<span>总条数</span>
<strong>{{ total }} </strong>
</div>
<div class="metric-card">
<span>高风险动作</span>
@@ -94,13 +103,8 @@ function actionType(action: string) {
<el-option label="商品标记异常" value="listing.mark_abnormal" />
<el-option label="客服关闭订单" value="order.admin_close" />
<el-option label="订单标记异常" value="order.mark_abnormal" />
<el-option label="号主交接超时" value="order.timeout.owner_submit" />
<el-option label="租客确认超时" value="order.timeout.renter_confirm" />
<el-option label="租客归还逾期" value="order.timeout.return_overdue" />
<el-option label="号主确认归还超时" value="order.timeout.owner_return_confirm" />
<el-option label="申诉仲裁" value="dispute.arbitrate" />
<el-option label="更新系统配置" value="system_config.update" />
<el-option label="创建系统配置" value="system_config.create" />
</el-select>
</el-form-item>
<el-form-item label="业务类型">
@@ -112,17 +116,12 @@ function actionType(action: string) {
<el-option label="系统配置" value="system_config" />
</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="logs">
<el-table-column prop="id" label="ID" width="90" />
<el-table-column label="管理员" min-width="160">
<el-table-column label="操作人" min-width="160">
<template #default="{ row }">
<strong>{{ actorName(row) }}</strong>
<span class="table-subtext">{{ row.actor_type }} ID: {{ row.actor_id }}</span>
</template>
</el-table-column>
<el-table-column label="动作" min-width="180">
@@ -132,8 +131,6 @@ function actionType(action: string) {
</el-table-column>
<el-table-column prop="biz_type" label="业务类型" width="130" />
<el-table-column prop="biz_id" label="业务 ID" width="100" />
<el-table-column prop="ip" label="IP" min-width="130" />
<el-table-column prop="user_agent" label="User-Agent" min-width="240" show-overflow-tooltip />
<el-table-column label="时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
@@ -144,9 +141,23 @@ function actionType(action: string) {
</el-table-column>
</el-table>
<div class="pagination-wrap" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="loadLogs"
@size-change="handleSizeChange"
/>
</div>
<el-dialog :model-value="!!activeLog" title="审计明细" width="720px" @update:model-value="activeLog = null">
<div v-if="activeLog" class="dialog-body">
<p><strong>{{ activeLog.action }}</strong> · {{ activeLog.biz_type }} #{{ activeLog.biz_id || '-' }}</p>
<p>操作人{{ actorName(activeLog) }} · IP{{ activeLog.ip }}</p>
<p>User-Agent{{ activeLog.user_agent }}</p>
<div class="code-panel">
<pre>{{ detailText(activeLog) }}</pre>
</div>
@@ -8,11 +8,13 @@ import { formatDateTime } from '@/utils/time'
const loading = ref(false)
const ledger = ref<AdminWalletLedger[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const filters = reactive({
user_id: '',
order_id: '',
biz_type: '',
limit: 200,
})
const inAmount = computed(() =>
@@ -21,16 +23,20 @@ const inAmount = computed(() =>
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)
const query = {
...filters,
page: currentPage.value,
page_size: currentPageSize.value,
}
const result = await fetchAdminWalletLedger(query)
ledger.value = result.items
total.value = result.total
} finally {
loading.value = false
}
@@ -40,10 +46,15 @@ function resetFilters() {
filters.user_id = ''
filters.order_id = ''
filters.biz_type = ''
filters.limit = 200
currentPage.value = 1
void loadLedger()
}
function handleSizeChange() {
currentPage.value = 1
loadLedger()
}
function money(value: number) {
return `¥${value.toFixed(2)}`
}
@@ -65,7 +76,7 @@ function directionLabel(direction: string) {
<div class="page-header">
<p class="eyebrow">Wallet Ledger</p>
<h1>资金流水</h1>
<p>查看订单金额押金冻结解冻退款和结算流水当前为开发态账务记录不代表真实支付余额</p>
<p>查看订单金额押金冻结解冻退款和结算流水</p>
</div>
<div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button>
@@ -75,8 +86,8 @@ function directionLabel(direction: string) {
<div class="metric-grid">
<div class="metric-card">
<span>当前结果</span>
<strong>{{ ledger.length }} </strong>
<span>总条数</span>
<strong>{{ total }} </strong>
</div>
<div class="metric-card">
<span>入账合计</span>
@@ -86,10 +97,6 @@ function directionLabel(direction: string) {
<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">
@@ -107,20 +114,16 @@ function directionLabel(direction: string) {
<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">
<el-table-column label="用户" min-width="130">
<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">
<el-table-column label="订单" min-width="160">
<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>
@@ -141,10 +144,21 @@ function directionLabel(direction: string) {
<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 label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
</el-table>
<div class="pagination-wrap" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@current-change="loadLedger"
@size-change="handleSizeChange"
/>
</div>
</section>
</template>