管理员管理增加独立「操作日志」菜单页。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-17 03:07:55 +08:00
parent d99dec79d9
commit b9d5ee1180
3 changed files with 379 additions and 0 deletions

View File

@@ -168,6 +168,11 @@ const routes = [
component: () => import('@/views/admin/AdminUser.vue'),
meta: { title: '后台用户' }
},
{
path: 'admin/oplog',
component: () => import('@/views/admin/OpLog.vue'),
meta: { title: '操作日志' }
},
{
path: 'admin/user/detail/:id',
component: () => import('@/views/admin/AdminUserDetail.vue'),

View File

@@ -100,6 +100,7 @@
</template>
<el-menu-item index="/admin/role">角色管理</el-menu-item>
<el-menu-item index="/admin/user">后台用户</el-menu-item>
<el-menu-item index="/admin/oplog">操作日志</el-menu-item>
</el-sub-menu>
<!-- 处罚管理 -->

373
src/views/admin/OpLog.vue Normal file
View File

@@ -0,0 +1,373 @@
<template>
<div class="op-log-page">
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div class="no-permission-text">您无权访问此页面</div>
<div class="no-permission-desc">需要超级管理员权限000001</div>
</div>
<div v-else class="page-body">
<div class="page-title">操作日志</div>
<div class="page-desc">查看后台账号对打手/商家/管事/组长等敏感操作记录</div>
<div class="layout">
<!-- 左侧操作人列表 -->
<div class="left-panel">
<div class="panel-head">
<el-input
v-model="userKeyword"
placeholder="搜索账号/昵称"
clearable
@keyup.enter="fetchUsers"
/>
<el-button type="primary" @click="fetchUsers">搜索</el-button>
</div>
<div class="user-list" v-loading="userLoading">
<div
v-for="u in userList"
:key="u.phone"
class="user-item"
:class="{ active: selectedPhone === u.phone }"
@click="selectUser(u)"
>
<div class="user-main">
<div class="user-name">{{ u.nicheng || '未命名' }}</div>
<div class="user-phone">{{ u.phone }}</div>
</div>
<el-tag size="small" type="info">{{ u.jilushuliang ?? 0 }} </el-tag>
</div>
<div v-if="!userLoading && !userList.length" class="empty">暂无后台用户</div>
</div>
<div class="left-pager">
<el-pagination
small
layout="prev, pager, next"
:total="userTotal"
v-model:current-page="userPage"
:page-size="userPageSize"
@current-change="fetchUsers"
/>
</div>
</div>
<!-- 右侧日志明细 -->
<div class="right-panel">
<div v-if="!selectedPhone" class="empty-right">请先选择左侧操作人</div>
<template v-else>
<div class="right-head">
<div>
<span class="sel-name">{{ selectedUser?.nicheng || '' }}</span>
<span class="sel-phone">{{ selectedPhone }}</span>
</div>
<div class="filters">
<el-select
v-model="logFilters.leixing"
placeholder="类型"
clearable
style="width: 130px"
@change="fetchLogs(1)"
>
<el-option label="全部类型" value="" />
<el-option label="打手" :value="2" />
<el-option label="管事" :value="3" />
<el-option label="商家" :value="4" />
<el-option label="组长" :value="5" />
<el-option label="后台账号" :value="9" />
<el-option label="系统配置" :value="0" />
</el-select>
<el-input
v-model="logFilters.yonghuid"
placeholder="被操作用户ID"
clearable
style="width: 150px"
@keyup.enter="fetchLogs(1)"
/>
<el-button type="primary" @click="fetchLogs(1)">查询</el-button>
</div>
</div>
<el-table :data="logList" v-loading="logLoading" border class="log-table" max-height="620">
<el-table-column prop="create_time" label="时间" width="170" />
<el-table-column prop="leixing_name" label="类型" width="90" />
<el-table-column prop="beixiugai_yonghuid" label="被操作用户" width="110" />
<el-table-column label="资金/数值变动" min-width="200">
<template #default="{ row }">
<div class="log-change">{{ formatLogChange(row) }}</div>
</template>
</el-table-column>
<el-table-column prop="qitashuoming" label="操作说明" min-width="260" show-overflow-tooltip />
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="logPage"
v-model:page-size="logPageSize"
:page-sizes="[10, 20, 50]"
:total="logTotal"
layout="total, sizes, prev, pager, next"
@size-change="fetchLogs()"
@current-change="fetchLogs()"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const username = localStorage.getItem('username') || ''
const hasPermission = ref(true)
const userKeyword = ref('')
const userList = ref([])
const userLoading = ref(false)
const userPage = ref(1)
const userPageSize = ref(20)
const userTotal = ref(0)
const selectedPhone = ref('')
const selectedUser = ref(null)
const logList = ref([])
const logLoading = ref(false)
const logPage = ref(1)
const logPageSize = ref(20)
const logTotal = ref(0)
const logFilters = ref({ leixing: '', yonghuid: '' })
const _changed = (before, after) => {
const b = Number(before)
const a = Number(after)
if (Number.isNaN(b) || Number.isNaN(a)) return false
return b !== a
}
const formatLogChange = (row) => {
const parts = []
if (_changed(row.dashou_yue_qian, row.dashou_yue_hou)) {
parts.push(`打手余额 ${row.dashou_yue_qian}${row.dashou_yue_hou}`)
}
if (_changed(row.dashou_jifen_qian, row.dashou_jifen_hou)) {
parts.push(`积分 ${row.dashou_jifen_qian}${row.dashou_jifen_hou}`)
}
if (_changed(row.dashou_yajin_qian, row.dashou_yajin_hou)) {
parts.push(`押金 ${row.dashou_yajin_qian}${row.dashou_yajin_hou}`)
}
if (_changed(row.shangjia_yue_qian, row.shangjia_yue_hou)) {
parts.push(`商家余额 ${row.shangjia_yue_qian}${row.shangjia_yue_hou}`)
}
if (_changed(row.guanshi_yue_qian, row.guanshi_yue_hou)) {
parts.push(`管事余额 ${row.guanshi_yue_qian}${row.guanshi_yue_hou}`)
}
if (_changed(row.zuzhang_yue_qian, row.zuzhang_yue_hou)) {
parts.push(`组长可提现 ${row.zuzhang_yue_qian}${row.zuzhang_yue_hou}`)
}
if (row.huiyuants) {
parts.push(`会员天数 ${row.huiyuants}${row.huiyuan_id ? `${row.huiyuan_id}` : ''}`)
}
return parts.length ? parts.join('\n') : '—'
}
const fetchUsers = async () => {
userLoading.value = true
try {
const kw = (userKeyword.value || '').trim()
const params = {
username,
page: userPage.value,
pageSize: userPageSize.value
}
if (kw) {
if (/^\d+$/.test(kw)) params.phone = kw
else params.nicheng = kw
}
const res = await request.post('/houtai/hqjsyhsj', params)
if (res.code === 0) {
userList.value = res.data.list || []
userTotal.value = res.data.total || 0
hasPermission.value = true
} else if (res.code === 403) {
hasPermission.value = false
} else {
ElMessage.error(res.msg || '获取用户失败')
}
} catch (e) {
ElMessage.error('网络错误')
} finally {
userLoading.value = false
}
}
const selectUser = (u) => {
selectedUser.value = u
selectedPhone.value = u.phone
logFilters.value = { leixing: '', yonghuid: '' }
fetchLogs(1)
}
const fetchLogs = async (pageOverride) => {
if (!selectedPhone.value) return
if (pageOverride) logPage.value = pageOverride
logLoading.value = true
try {
const payload = {
username,
phone: selectedPhone.value,
page: logPage.value,
pageSize: logPageSize.value
}
if (logFilters.value.leixing !== '' && logFilters.value.leixing !== null && logFilters.value.leixing !== undefined) {
payload.leixing = logFilters.value.leixing
}
if (logFilters.value.yonghuid) {
payload.yonghuid = String(logFilters.value.yonghuid).trim()
}
const res = await request.post('/houtai/hqczrz', payload)
if (res.code === 0) {
logList.value = res.data.list || []
logTotal.value = res.data.total || 0
} else if (res.code === 403) {
hasPermission.value = false
} else {
ElMessage.error(res.msg || '获取操作日志失败')
}
} catch (e) {
ElMessage.error('网络错误')
} finally {
logLoading.value = false
}
}
onMounted(async () => {
if (!username) {
ElMessage.error('未获取到用户信息')
return
}
await fetchUsers()
})
</script>
<style scoped>
.op-log-page {
background: radial-gradient(circle at 20% 30%, #0a0f1a, #03060c);
min-height: 100vh;
padding: 24px;
color: #eef2ff;
}
.no-permission {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
}
.no-permission-icon { font-size: 80px; margin-bottom: 20px; opacity: 0.6; }
.no-permission-text { font-size: 24px; font-weight: bold; margin-bottom: 8px; }
.no-permission-desc { font-size: 14px; color: rgba(255,255,255,0.5); }
.page-title {
font-size: 22px;
font-weight: 700;
color: #00f2ff;
margin-bottom: 6px;
}
.page-desc {
font-size: 13px;
color: rgba(255,255,255,0.55);
margin-bottom: 18px;
}
.layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: 16px;
min-height: 70vh;
}
.left-panel, .right-panel {
background: rgba(6,12,20,0.65);
border: 1px solid rgba(0,242,255,0.2);
border-radius: 14px;
padding: 14px;
}
.panel-head {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.user-list {
max-height: 560px;
overflow-y: auto;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
border: 1px solid transparent;
margin-bottom: 6px;
}
.user-item:hover { background: rgba(0,242,255,0.08); }
.user-item.active {
background: rgba(0,242,255,0.15);
border-color: rgba(0,242,255,0.45);
}
.user-name { font-weight: 600; }
.user-phone { font-size: 12px; color: rgba(255,255,255,0.55); margin-top: 2px; }
.left-pager { margin-top: 10px; display: flex; justify-content: center; }
.empty, .empty-right {
color: rgba(255,255,255,0.45);
text-align: center;
padding: 40px 0;
}
.right-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.sel-name { color: #00f2ff; font-weight: 700; font-size: 16px; }
.sel-phone { color: rgba(255,255,255,0.6); margin-left: 4px; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.log-change {
white-space: pre-line;
font-size: 13px;
color: #a8e6ff;
line-height: 1.5;
}
.pager {
margin-top: 14px;
display: flex;
justify-content: flex-end;
}
:deep(.el-table) { background: transparent; color: #fff; }
:deep(.el-table th) {
background: rgba(0,242,255,0.15);
color: #00f2ff;
border-bottom: 1px solid rgba(0,242,255,0.3);
}
:deep(.el-table td) {
background: transparent;
color: #fff;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
:deep(.el-table__row:hover td) { background: rgba(0,242,255,0.1) !important; }
:deep(.el-input__wrapper) {
background: rgba(0,0,0,0.5);
box-shadow: 0 0 0 1px rgba(0,242,255,0.25) inset;
}
:deep(.el-input__inner) { color: #fff; }
@media (max-width: 960px) {
.layout { grid-template-columns: 1fr; }
}
</style>