增加流水通知审计分页
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user