@@ -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'),
|
||||
|
||||
@@ -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
373
src/views/admin/OpLog.vue
Normal 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>
|
||||
Reference in New Issue
Block a user