feat:【system】操作日志 50%

This commit is contained in:
YunaiV
2025-12-17 13:07:45 +08:00
parent 3676cd702b
commit 9ae1cb9bdf
5 changed files with 480 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 操作日志信息 */
export interface OperateLog {
id?: number
traceId?: string
userId?: number
userType?: number
userName?: string
type?: string
subType?: string
bizId?: number
action?: string
extra?: string
requestMethod?: string
requestUrl?: string
userIp?: string
userAgent?: string
createTime?: Date
}
/** 获取操作日志分页列表 */
export function getOperateLogPage(params: PageParam) {
return http.get<PageResult<OperateLog>>('/system/operate-log/page', params)
}
/** 获取操作日志详情 */
export function getOperateLog(id: number) {
return http.get<OperateLog>(`/system/operate-log/get?id=${id}`)
}

View File

@@ -0,0 +1,90 @@
<template>
<view class="page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="操作日志详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
<wd-cell title="日志编号" :value="String(formData?.id ?? '-')" />
<wd-cell v-if="formData?.traceId" title="链路追踪" :value="formData.traceId" />
<wd-cell title="操作人编号" :value="String(formData?.userId ?? '-')" />
<wd-cell title="操作人类型">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
</wd-cell>
<wd-cell title="操作人名字" :value="formData?.userName || '-'" />
<wd-cell title="操作人 IP" :value="formData?.userIp || '-'" />
<wd-cell title="操作人 UA" :value="formData?.userAgent || '-'" />
<wd-cell title="操作模块" :value="formData?.type || '-'" />
<wd-cell title="操作名" :value="formData?.subType || '-'" />
<wd-cell title="操作内容" :value="formData?.action || '-'" />
<wd-cell v-if="formData?.extra" title="操作拓展参数" :value="formData.extra" />
<wd-cell title="请求 URL" :value="getRequestUrl()" />
<wd-cell title="操作时间" :value="formatDateTime(formData?.createTime) || '-'" />
<wd-cell title="业务编号" :value="String(formData?.bizId ?? '-')" />
</wd-cell-group>
</view>
</view>
</template>
<script lang="ts" setup>
import type { OperateLog } from '@/api/system/operate-log'
import { onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { getOperateLog } from '@/api/system/operate-log'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const formData = ref<OperateLog>()
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-system/operate-log/index')
}
/** 获取请求 URL */
function getRequestUrl() {
if (formData.value?.requestMethod && formData.value?.requestUrl) {
return `${formData.value.requestMethod} ${formData.value.requestUrl}`
}
return '-'
}
/** 加载操作日志详情 */
async function getDetail() {
if (!props.id) {
return
}
try {
toast.loading('加载中...')
formData.value = await getOperateLog(props.id)
} finally {
toast.close()
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,171 @@
<template>
<view class="page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="操作日志管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm
:search-params="queryParams"
@search="handleQuery"
@reset="handleReset"
/>
<!-- 操作日志列表 -->
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.type }} / {{ item.subType }}
</view>
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="item.userType" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">操作人</text>
<text class="line-clamp-1">{{ item.userName }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx shrink-0 text-[#999]">操作内容</text>
<text class="min-w-0 flex-1 truncate">{{ item.action }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">操作时间</text>
<text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">业务编号</text>
<text class="line-clamp-1">{{ item.bizId }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">操作 IP</text>
<text class="line-clamp-1">{{ item.userIp }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无操作日志数据" />
</view>
<wd-loadmore
v-if="list.length > 0"
:state="loadMoreState"
@reload="loadMore"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import type { SearchFormData } from './modules/search-form.vue'
import type { OperateLog } from '@/api/system/operate-log'
import type { LoadMoreState } from '@/http/types'
import { onReachBottom } from '@dcloudio/uni-app'
import { onMounted, reactive, ref } from 'vue'
import { getOperateLogPage } from '@/api/system/operate-log'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import SearchForm from './modules/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const total = ref(0)
const list = ref<OperateLog[]>([])
const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: undefined as number | undefined,
type: undefined as string | undefined,
subType: undefined as string | undefined,
bizId: undefined as number | undefined,
action: undefined as string | undefined,
createTime: undefined as string | undefined,
})
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询操作日志列表 */
async function getList() {
loadMoreState.value = 'loading'
try {
const data = await getOperateLogPage({
...queryParams,
})
list.value = [...list.value, ...data.list]
total.value = data.total
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
} catch {
queryParams.pageNo = queryParams.pageNo > 1 ? queryParams.pageNo - 1 : 1
loadMoreState.value = 'error'
}
}
/** 搜索按钮操作 */
function handleQuery(data?: SearchFormData) {
queryParams.userId = data?.userId
queryParams.type = data?.type
queryParams.subType = data?.subType
queryParams.bizId = data?.bizId
queryParams.action = data?.action
queryParams.createTime = data?.createTime
queryParams.pageNo = 1
list.value = []
getList()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 加载更多 */
function loadMore() {
if (loadMoreState.value === 'finished') {
return
}
queryParams.pageNo++
getList()
}
/** 查看详情 */
function handleDetail(item: OperateLog) {
uni.navigateTo({
url: `/pages-system/operate-log/detail/index?id=${item.id}`,
})
}
/** 触底加载更多 */
onReachBottom(() => {
loadMore()
})
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,180 @@
<template>
<!-- 搜索框入口 -->
<wd-search
:placeholder="searchPlaceholder"
:hide-cancel="true"
disabled
@click="visible = true"
/>
<!-- 搜索弹窗 -->
<wd-popup
v-model="visible"
position="top"
custom-style="border-radius: 0 0 24rpx 24rpx;"
safe-area-inset-top
@close="visible = false"
>
<view class="p-32rpx">
<view class="mb-24rpx text-32rpx text-[#333] font-semibold">
搜索操作日志
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
用户编号
</view>
<wd-input
v-model="formData.userId"
placeholder="请输入用户编号"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
操作模块类型
</view>
<wd-input
v-model="formData.type"
placeholder="请输入操作模块类型"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
操作名
</view>
<wd-input
v-model="formData.subType"
placeholder="请输入操作名"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
操作数据模块编号
</view>
<wd-input
v-model="formData.bizId"
placeholder="请输入操作数据模块编号"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
操作内容
</view>
<wd-input
v-model="formData.action"
placeholder="请输入操作内容"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-28rpx text-[#666]">
创建时间
</view>
<wd-input
v-model="formData.createTime"
placeholder="请输入创建时间"
clearable
/>
</view>
<view class="w-full flex justify-center gap-24rpx">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue'
/** 搜索表单数据 */
export interface SearchFormData {
userId?: number
type?: string
subType?: string
bizId?: number
action?: string
createTime?: string
}
const props = defineProps<{
searchParams?: Partial<SearchFormData>
}>()
const emit = defineEmits<{
search: [data: SearchFormData]
reset: []
}>()
const visible = ref(false)
/** 搜索条件 placeholder 拼接 */
const searchPlaceholder = computed(() => {
const conditions: string[] = []
if (props.searchParams?.userId !== undefined) {
conditions.push(`用户编号:${props.searchParams.userId}`)
}
if (props.searchParams?.type) {
conditions.push(`操作模块类型:${props.searchParams.type}`)
}
if (props.searchParams?.subType) {
conditions.push(`操作名:${props.searchParams.subType}`)
}
if (props.searchParams?.bizId !== undefined) {
conditions.push(`操作数据模块编号:${props.searchParams.bizId}`)
}
if (props.searchParams?.action) {
conditions.push(`操作内容:${props.searchParams.action}`)
}
if (props.searchParams?.createTime) {
conditions.push(`创建时间:${props.searchParams.createTime}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索操作日志'
})
const formData = reactive<SearchFormData>({
userId: undefined,
type: undefined,
subType: undefined,
bizId: undefined,
action: undefined,
createTime: undefined,
})
/** 监听弹窗打开,同步外部参数 */
watch(visible, (val) => {
if (val && props.searchParams) {
formData.userId = props.searchParams.userId
formData.type = props.searchParams.type
formData.subType = props.searchParams.subType
formData.bizId = props.searchParams.bizId
formData.action = props.searchParams.action
formData.createTime = props.searchParams.createTime
}
})
/** 搜索 */
function handleSearch() {
visible.value = false
emit('search', { ...formData } as SearchFormData)
}
/** 重置 */
function handleReset() {
formData.userId = undefined
formData.type = undefined
formData.subType = undefined
formData.bizId = undefined
formData.action = undefined
formData.createTime = undefined
visible.value = false
emit('reset')
}
</script>

View File

@@ -77,6 +77,14 @@ const menuGroupsData: MenuGroup[] = [
iconColor: '#faad14',
permission: 'system:notice:query',
},
{
key: 'operateLog',
name: '操作日志',
icon: 'notes',
url: '/pages-system/operate-log/index',
iconColor: '#722ed1',
permission: 'system:operate-log:query',
},
],
},
{