Browse Source

✨ feat: 增加操作日志记录

Pchen. 10 months ago
parent
commit
1f7d521c6a

+ 10 - 1
src/api/user.js

@@ -7,7 +7,8 @@ const api = {
   BindEmail: '/User/BindEmail',
   BindEmail: '/User/BindEmail',
   GetRepos: '/User/GetRepos',
   GetRepos: '/User/GetRepos',
   AdminUserList: '/Admin/User/GetUserList',
   AdminUserList: '/Admin/User/GetUserList',
-  AdminChangeLepaoCount: '/Admin/User/ChangeLepaoCount'
+  AdminChangeLepaoCount: '/Admin/User/ChangeLepaoCount',
+  AdminGetReqLog: '/Admin/User/GetReqLog'
 }
 }
 
 
 export function ChangeUsername(parameter) {
 export function ChangeUsername(parameter) {
@@ -18,6 +19,14 @@ export function ChangeUsername(parameter) {
   })
   })
 }
 }
 
 
+export function adminGetReqLog(parameter) {
+  return request({
+    url: api.AdminGetReqLog,
+    method: 'post',
+    data: parameter
+  })
+}
+
 export function adminChangeLepaoCount(parameter) {
 export function adminChangeLepaoCount(parameter) {
   return request({
   return request({
     url: api.AdminChangeLepaoCount,
     url: api.AdminChangeLepaoCount,

+ 28 - 29
src/components/Menu/index.vue

@@ -1,27 +1,26 @@
 <template>
 <template>
-    <div class="menu">
-        <a-menu :style="{ width: '200px', height: '100%' }" v-model:selected-keys="selectedKey"  :default-open-keys="selectedKey">
-            <template v-for="menu in menuData" :key="menu.key">
-                <a-sub-menu v-if="menu.children" :key="menu.key">
-                    <template #icon>
-                        <component :is="menu.icon"></component>
-                    </template>
-                    <template #title>{{ menu.label }}</template>
-                    <a-menu-item v-for="child in menu.children" :key="child.key" :disabled="child.disabled"
-                        @click="$router.push(child.key)">
-                        {{ child.label }}
-                    </a-menu-item>
-                </a-sub-menu>
-
-                <a-menu-item v-else :key="menu.key" :disabled="menu.disabled" @click="$router.push(menu.key)">
-                    <template #icon>
-                        <component :is="menu.icon"></component>
-                    </template>
-                    {{ menu.label }}
+    <a-menu v-model:selected-keys="selectedKey"
+        :default-open-keys="selectedKey">
+        <template v-for="menu in menuData" :key="menu.key">
+            <a-sub-menu v-if="menu.children" :key="menu.key">
+                <template #icon>
+                    <component :is="menu.icon"></component>
+                </template>
+                <template #title>{{ menu.label }}</template>
+                <a-menu-item v-for="child in menu.children" :key="child.key" :disabled="child.disabled"
+                    @click="$router.push(child.key)">
+                    {{ child.label }}
                 </a-menu-item>
                 </a-menu-item>
-            </template>
-        </a-menu>
-    </div>
+            </a-sub-menu>
+
+            <a-menu-item v-else :key="menu.key" :disabled="menu.disabled" @click="$router.push(menu.key)">
+                <template #icon>
+                    <component :is="menu.icon"></component>
+                </template>
+                {{ menu.label }}
+            </a-menu-item>
+        </template>
+    </a-menu>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -33,22 +32,22 @@ import { isElectron } from '../../utils/electron'
 
 
 const userStore = useUserStore()
 const userStore = useUserStore()
 const user = ref({})
 const user = ref({})
-const electron  =ref(false)
+const electron = ref(false)
 const menuData = ref([])
 const menuData = ref([])
 
 
 const { selectedKey } = useRouteListener()
 const { selectedKey } = useRouteListener()
 
 
 const hasPermission = (route) => {
 const hasPermission = (route) => {
     if (!route.meta || !route.meta.permission) return true
     if (!route.meta || !route.meta.permission) return true
-    if(!user.value.roles || user.value.roles.length === 0) return false
+    if (!user.value.roles || user.value.roles.length === 0) return false
     return route.meta.permission.some((perm) => user.value.roles.includes(perm))
     return route.meta.permission.some((perm) => user.value.roles.includes(perm))
 }
 }
 
 
 const checkEnv = (route) => {
 const checkEnv = (route) => {
-  if (!route.meta) return true
-  if (route.meta.onlyWeb) return !electron.value
-  if (route.meta.onlyElectron) return !!electron.value
-  return true
+    if (!route.meta) return true
+    if (route.meta.onlyWeb) return !electron.value
+    if (route.meta.onlyElectron) return !!electron.value
+    return true
 }
 }
 
 
 const generateMenu = (routes, parentPath = '') => {
 const generateMenu = (routes, parentPath = '') => {
@@ -76,4 +75,4 @@ onMounted(async () => {
     electron.value = isElectron()
     electron.value = isElectron()
     menuData.value = generateMenu(routes)
     menuData.value = generateMenu(routes)
 })
 })
-</script>
+</script>

+ 6 - 10
src/layout/default-layout.vue

@@ -4,11 +4,11 @@
       <Navbar />
       <Navbar />
     </a-layout-header>
     </a-layout-header>
     <a-layout>
     <a-layout>
-       <a-layout-sider class="layout-sider">
+      <a-layout-sider class="layout-sider">
         <Menu class="menu-wrapper" />
         <Menu class="menu-wrapper" />
       </a-layout-sider>
       </a-layout-sider>
       <a-layout-content class="layout-content">
       <a-layout-content class="layout-content">
-        <PageLayout style="margin-bottom: 30px;"/>
+        <PageLayout style="margin-bottom: 30px;" />
         <Footer />
         <Footer />
       </a-layout-content>
       </a-layout-content>
     </a-layout>
     </a-layout>
@@ -22,7 +22,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { onMounted, onBeforeUnmount, ref,onUnmounted } from 'vue'
+import { onMounted, onBeforeUnmount, ref, onUnmounted } from 'vue'
 import { eventBus } from '@/utils/eventBus'
 import { eventBus } from '@/utils/eventBus'
 
 
 import PageLayout from './page-layout.vue'
 import PageLayout from './page-layout.vue'
@@ -71,7 +71,7 @@ onMounted(() => {
   if (button) {
   if (button) {
     button.addEventListener('mousedown', handleMouseDown)
     button.addEventListener('mousedown', handleMouseDown)
   }
   }
-  eventBus.on('closeAI',  ()=>{chat.value = false})
+  eventBus.on('closeAI', () => { chat.value = false })
 })
 })
 
 
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
@@ -81,7 +81,7 @@ onBeforeUnmount(() => {
 })
 })
 
 
 onUnmounted(() => {
 onUnmounted(() => {
-    eventBus.off('closeAI', ()=>{chat.value = false})
+  eventBus.off('closeAI', () => { chat.value = false })
 })
 })
 
 
 </script>
 </script>
@@ -121,14 +121,10 @@ onUnmounted(() => {
     background-color: var(--color-border);
     background-color: var(--color-border);
     content: '';
     content: '';
   }
   }
-
-  > :deep(.arco-layout-sider-children) {
-    overflow-y: hidden;
-  }
 }
 }
 
 
 .menu-wrapper {
 .menu-wrapper {
-  user-select: none; 
+  user-select: none;
   margin-top: 60px;
   margin-top: 60px;
   overflow: auto;
   overflow: auto;
   overflow-x: hidden;
   overflow-x: hidden;

+ 1 - 1
src/pages/admin/goods/goodsList.vue

@@ -66,7 +66,7 @@
                     </a-tag>
                     </a-tag>
                 </template>
                 </template>
                 <template #optional="{ record }">
                 <template #optional="{ record }">
-                    <a-button @click="$router.push(`/admin/goods/addGoods/${record.id}`)">编辑商品</a-button>
+                    <a-button @click="$router.push(`/goodsManage/goods/addGoods/${record.id}`)">编辑商品</a-button>
                 </template>
                 </template>
             </a-table>
             </a-table>
         </a-card>
         </a-card>

+ 255 - 0
src/pages/admin/reqLog/index.vue

@@ -0,0 +1,255 @@
+<template>
+    <div class="container">
+        <Breadcrumb :items="['日志记录', '请求日志']" />
+
+        <a-card title="请求日志">
+            <a-row>
+                <a-col :flex="1">
+                    <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
+                        label-align="left">
+                        <a-row :gutter="16">
+                            <a-col :span="8">
+                                <a-form-item field="begin_time" label="开始时间">
+                                    <a-date-picker show-time format="YYYY-MM-DD HH:mm:ss"
+                                        v-model="queryData.begin_time" />
+                                </a-form-item>
+                            </a-col>
+                            <a-col :span="8">
+                                <a-form-item field="end_time" label="结束时间">
+                                    <a-date-picker show-time format="YYYY-MM-DD HH:mm:ss"
+                                        v-model="queryData.end_time" />
+                                </a-form-item>
+                            </a-col>
+                            <a-col :span="8">
+                                <a-form-item field="create_user" label="用户UUID">
+                                    <a-input v-model="queryData.create_user" />
+                                </a-form-item>
+                            </a-col>
+                        </a-row>
+                    </a-form>
+                </a-col>
+                <a-divider style="height: 84px" direction="vertical" />
+                <a-col :flex="'86px'" style="text-align: right">
+                    <a-space direction="vertical" :size="18">
+                        <a-button type="primary" @click="search">
+                            <template #icon>
+                                <icon-search />
+                            </template>
+                            搜索
+                        </a-button>
+                        <a-button @click="reset">
+                            <template #icon>
+                                <icon-refresh />
+                            </template>
+                            重置
+                        </a-button>
+                    </a-space>
+                </a-col>
+            </a-row>
+
+            <a-table :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
+                :pagination="{
+                    showPageSize: true,
+                    showJumper: true,
+                    showTotal: true,
+                    pageSize: pagination.pagesize,
+                    current: pagination.current,
+                    total: pagination.total
+                }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
+                <template #username="{ record }">
+                    <a-avatar :size="35">
+                        <IconUser v-if="!record.avatar" />
+                        <img :alt="record.username ?? ''" :src="record.avatar" v-else />
+                    </a-avatar>
+                    {{ record.username }}
+                </template>
+                <template #time="{ record }">
+                    {{ stramptoTime(record.create_time) }}
+                </template>
+                <template #optional="{ record }">
+                    <a-button @click="showDetail(record)">查看详情</a-button>
+                </template>
+            </a-table>
+        </a-card>
+    </div>
+
+    <a-modal v-model:visible="showModal" @cancel="handleCancel" draggable>
+        <template #title>
+            日志详情
+        </template>
+        <div>
+            <a-descriptions :data="modalData" :column="1" />
+        </div>
+    </a-modal>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { adminGetReqLog } from '@/api/user'
+import { Notification } from '@arco-design/web-vue'
+
+const showModal = ref(false)
+const modalData = ref([])
+
+const queryData = reactive({
+    create_user: '',
+    begin_time: null,
+    end_time: null
+})
+
+const pagination = reactive({
+    total: 0,
+    current: 1, // 默认从第1页开始
+    pagesize: 20
+})
+
+const loading = ref(false)
+const data = ref([])
+
+const columns = [{
+    title: 'ID',
+    dataIndex: 'id',
+}, {
+    title: '操作人',
+    slotName: 'username'
+}, {
+    title: '访问路径',
+    dataIndex: 'url',
+}, {
+    title: '请求方法',
+    dataIndex: 'method',
+}, {
+    title: '请求IP',
+    dataIndex: 'ip',
+}, {
+    title: 'IP属地',
+    dataIndex: 'location',
+}, {
+    title: '操作时间',
+    slotName: 'time',
+}, {
+    title: '状态',
+    slotName: 'code'
+}, {
+    title: '操作',
+    slotName: 'optional'
+}
+]
+
+const search = () => {
+    pagination.current = 1
+    getLogList()
+}
+
+const reset = () => {
+    queryData.create_user = ''
+    queryData.begin_time = null
+    queryData.end_time = null
+    getLogList()
+}
+
+const handleCancel = () => {
+    showModal.value = false
+    modalData.value = []
+}
+
+const showDetail = (record) => {
+    const data = [{
+        label: '操作人',
+        value: record.username,
+    }, {
+        label: '操作人UUID',
+        value: record.create_user,
+    }, {
+        label: '请求方式',
+        value: record.method
+    }, {
+        label: '请求路径',
+        value: record.url,
+    }, {
+        label: '请求IP',
+        value: record.ip
+    }, {
+        label: 'IP属地',
+        value: record.location
+    }, {
+        label: '请求数据',
+        value: JSON.stringify(record.reqData)
+    }, {
+        label: '响应数据',
+        value: JSON.stringify(record.resData)
+    },
+    {
+        label: '状态码',
+        value: record.code
+    },
+    {
+        label: '操作时间',
+        value: stramptoTime(record.create_time)
+    },
+    ]
+
+    modalData.value = data
+    showModal.value = true
+}
+
+const getLogList = async () => {
+    try {
+        loading.value = true
+        const reqData = {
+            create_user: queryData.create_user,
+            begin_time: new Date(queryData.begin_time).getTime(),
+            end_time: new Date(queryData.end_time).getTime(),
+            pagesize: pagination.pagesize,
+            current: pagination.current
+        }
+        const res = await adminGetReqLog(reqData)
+        if (!res || res.code !== 0)
+            return Notification.error({
+                title: '获取日志数据失败!',
+                content: res?.msg ?? '请稍后再试'
+            })
+
+        data.value = res.data
+        pagination.total = res.pagination.total
+    } catch (error) {
+        Notification.error({
+            title: '获取日志数据失败!',
+            content: error.message || '请稍后再试'
+        })
+    } finally {
+        loading.value = false
+    }
+}
+
+// 分页 - 页码变化
+const handlePageChange = (page) => {
+    pagination.current = page
+    getLogList()
+}
+
+// 分页 - 每页条数变化
+const handlePageSizeChange = (size) => {
+    pagination.pagesize = size
+    pagination.current = 1 // 页大小变化后回到第一页
+    getLogList()
+}
+
+onMounted(() => {
+    getLogList()
+})
+
+const stramptoTime = (time) => {
+    return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
+}
+</script>
+
+<style scoped>
+.container {
+    padding: 0 20px 20px 20px;
+}
+
+.table {
+    margin-top: 15px;
+}
+</style>

+ 22 - 1
src/router/index.js

@@ -210,7 +210,7 @@ const routes = [
         component: DEFAULT_LAYOUT,
         component: DEFAULT_LAYOUT,
         meta: {
         meta: {
             title: '网站管理',
             title: '网站管理',
-            icon: 'icon-check-square',
+            icon: 'icon-computer',
             permission: ['admin', 'service', 'product']
             permission: ['admin', 'service', 'product']
         },
         },
         children: [
         children: [
@@ -271,6 +271,27 @@ const routes = [
                 }
                 }
             },
             },
             {
             {
+                path: 'reqLog',
+                name: 'admin.log.reqLog',
+                component: () => import('../pages/admin/reqLog/index.vue'),
+                meta: {
+                    title: '请求日志',
+                    permission: ['admin']
+                }
+            }
+        ]
+    },
+    {
+        path: '/goodsManage',
+        name: 'goodsManage',
+        component: DEFAULT_LAYOUT,
+        meta: {
+            title : '商品管理',
+            icon: 'icon-apps',
+            permission: ['admin', 'product']
+        },
+        children: [
+                        {
                 path: 'goods/addGoods',
                 path: 'goods/addGoods',
                 name: 'admin.goods.addGoods',
                 name: 'admin.goods.addGoods',
                 component: () => import('../pages/admin/goods/addGoods.vue'),
                 component: () => import('../pages/admin/goods/addGoods.vue'),