|
|
@@ -0,0 +1,423 @@
|
|
|
+## log.ts
|
|
|
+
|
|
|
+``` ts
|
|
|
+import type { Recordable } from '@vben/types';
|
|
|
+
|
|
|
+import { requestClient } from '#/api/request';
|
|
|
+
|
|
|
+export namespace SystemLogApi {
|
|
|
+ export interface LogItem {
|
|
|
+ id: string;
|
|
|
+ appId: string;
|
|
|
+ time: string;
|
|
|
+ sys: string;
|
|
|
+ ver: string;
|
|
|
+ resVersion: string;
|
|
|
+ ip: string;
|
|
|
+ uid: string;
|
|
|
+ device: string;
|
|
|
+ model: string;
|
|
|
+ country: string;
|
|
|
+ tag: string;
|
|
|
+ message: string;
|
|
|
+ level: 'INFO' | 'WARN' | 'ERROR';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function getLogList(params: Recordable<any>) {
|
|
|
+ return requestClient.get<{
|
|
|
+ items: SystemLogApi.LogItem[];
|
|
|
+ page: number;
|
|
|
+ pageSize: number;
|
|
|
+ total: number;
|
|
|
+ }>('/system/log/list', { params });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## list.vue
|
|
|
+
|
|
|
+``` js
|
|
|
+<script lang="ts" setup>
|
|
|
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|
|
+
|
|
|
+import { Page } from '@vben/common-ui';
|
|
|
+
|
|
|
+import { Tag, Tooltip } from 'ant-design-vue';
|
|
|
+
|
|
|
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
|
+import { getLogList } from '#/api/system/log';
|
|
|
+import type { SystemLogApi } from '#/api/system/log';
|
|
|
+import { $t } from '#/locales';
|
|
|
+
|
|
|
+const levelColor: Record<SystemLogApi.LogItem['level'], string> = {
|
|
|
+ INFO: 'default',
|
|
|
+ WARN: 'warning',
|
|
|
+ ERROR: 'error',
|
|
|
+};
|
|
|
+
|
|
|
+// 固定引用,避免模板创建新对象导致子树重渲染
|
|
|
+const tooltipOverlayStyle = { maxWidth: '60vw' } as const;
|
|
|
+
|
|
|
+function getPreviewMessage(message: string) {
|
|
|
+ if (!message) return '';
|
|
|
+ const idx = message.toLowerCase().indexOf('c#');
|
|
|
+ if (idx >= 0) return message.slice(idx);
|
|
|
+ // 兜底:取最后一个大括号之后的部分
|
|
|
+ const brace = message.lastIndexOf('\n}\n');
|
|
|
+ return brace >= 0 ? message.slice(brace + 2) : message;
|
|
|
+}
|
|
|
+
|
|
|
+const [Grid] = useVbenVxeGrid({
|
|
|
+ formOptions: {
|
|
|
+ schema: [
|
|
|
+ { fieldName: 'appId', label: $t('system.log.appId'), component: 'Input' },
|
|
|
+ {
|
|
|
+ fieldName: 'level',
|
|
|
+ label: $t('system.log.level'),
|
|
|
+ component: 'Select',
|
|
|
+ componentProps: {
|
|
|
+ options: [
|
|
|
+ { label: 'INFO', value: 'INFO' },
|
|
|
+ { label: 'WARN', value: 'WARN' },
|
|
|
+ { label: 'ERROR', value: 'ERROR' },
|
|
|
+ ],
|
|
|
+ allowClear: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ { fieldName: 'q', label: 'Query', component: 'Input', colProps: { span: 8 } },
|
|
|
+ ],
|
|
|
+ submitOnChange: false,
|
|
|
+ },
|
|
|
+ gridOptions: {
|
|
|
+ columns: [
|
|
|
+ {
|
|
|
+ field: 'time',
|
|
|
+ title: $t('system.log.time'),
|
|
|
+ width: 160,
|
|
|
+ sortable: true,
|
|
|
+ sortBy: ({ row }) => Date.parse(String(row.time).replace(/\//g, '-')) || 0,
|
|
|
+ },
|
|
|
+ { field: 'appId', title: $t('system.log.appId'), width: 90, sortable: true },
|
|
|
+ { field: 'level', title: $t('system.log.level'), width: 90, slots: { default: 'level' }, sortable: true },
|
|
|
+ { field: 'sys', title: $t('system.log.sys'), width: 90, sortable: true },
|
|
|
+ { field: 'ver', title: $t('system.log.ver'), width: 100, sortable: true },
|
|
|
+ { field: 'ip', title: $t('system.log.ip'), width: 130, sortable: true },
|
|
|
+ { field: 'uid', title: $t('system.log.uid'), width: 130, sortable: true },
|
|
|
+ { field: 'model', title: $t('system.log.model'), width: 160, sortable: true },
|
|
|
+ { field: 'country', title: $t('system.log.country'), width: 120, sortable: true },
|
|
|
+ {
|
|
|
+ field: 'message',
|
|
|
+ title: $t('system.log.message'),
|
|
|
+ minWidth: 600,
|
|
|
+ showOverflow: false,
|
|
|
+ slots: { default: 'message' },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ sortConfig: {
|
|
|
+ trigger: 'cell',
|
|
|
+ remote: false,
|
|
|
+ orders: ['desc', 'asc', null],
|
|
|
+ defaultSort: { field: 'time', order: 'desc' },
|
|
|
+ },
|
|
|
+ height: 'auto',
|
|
|
+ keepSource: true,
|
|
|
+ proxyConfig: {
|
|
|
+ ajax: {
|
|
|
+ query: async ({ page }, formValues) => {
|
|
|
+ return await getLogList({
|
|
|
+ page: page.currentPage,
|
|
|
+ pageSize: page.pageSize,
|
|
|
+ ...formValues,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ rowConfig: { keyField: 'id', height: 44 },
|
|
|
+ toolbarConfig: { custom: true, export: false, refresh: true, search: true, zoom: true },
|
|
|
+ } as VxeTableGridOptions<SystemLogApi.LogItem>,
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <Page auto-content-height>
|
|
|
+ <Grid class="log-grid">
|
|
|
+ <template #level="{ row }">
|
|
|
+ <Tag :color="levelColor[row.level]">{{ row.level }}</Tag>
|
|
|
+ </template>
|
|
|
+ <template #message="{ row }">
|
|
|
+ <Tooltip
|
|
|
+ :key="row.id"
|
|
|
+ :mouse-enter-delay="0.2"
|
|
|
+ :mouse-leave-delay="0.05"
|
|
|
+ :destroy-tooltip-on-hide="true"
|
|
|
+ placement="topLeft"
|
|
|
+ :overlayStyle="tooltipOverlayStyle"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ <pre class="tooltip-pre whitespace-pre-wrap break-all m-0">{{ row.message }}</pre>
|
|
|
+ </template>
|
|
|
+ <pre class="whitespace-pre-line break-all m-0 line-clamp-1 leading-8">{{ getPreviewMessage(row.message) }}</pre>
|
|
|
+ </Tooltip>
|
|
|
+ </template>
|
|
|
+ </Grid>
|
|
|
+ </Page>
|
|
|
+
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.log-grid :deep(.vxe-table--body-wrapper .vxe-body--row) {
|
|
|
+ line-height: 28px;
|
|
|
+}
|
|
|
+.log-grid :deep(.vxe-body--row .vxe-body--column) {
|
|
|
+ padding-top: 8px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+.tooltip-pre {
|
|
|
+ max-height: 60vh;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+## index.ts
|
|
|
+
|
|
|
+``` ts
|
|
|
+export * from './core';
|
|
|
+
|
|
|
+export * from './examples';
|
|
|
+
|
|
|
+export * from './system';
|
|
|
+```
|
|
|
+
|
|
|
+## system.ts
|
|
|
+
|
|
|
+``` ts
|
|
|
+import type { RouteRecordRaw } from 'vue-router';
|
|
|
+
|
|
|
+import { $t } from '#/locales';
|
|
|
+
|
|
|
+const routes: RouteRecordRaw[] = [
|
|
|
+ {
|
|
|
+ meta: {
|
|
|
+ icon: 'ion:settings-outline',
|
|
|
+ order: 9997,
|
|
|
+ title: $t('system.title'),
|
|
|
+ },
|
|
|
+ name: 'System',
|
|
|
+ path: '/system',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ path: '/system/role',
|
|
|
+ name: 'SystemRole',
|
|
|
+ meta: {
|
|
|
+ icon: 'mdi:account-group',
|
|
|
+ title: $t('system.role.title'),
|
|
|
+ },
|
|
|
+ component: () => import('#/views/system/role/list.vue'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: '/system/menu',
|
|
|
+ name: 'SystemMenu',
|
|
|
+ meta: {
|
|
|
+ icon: 'mdi:menu',
|
|
|
+ title: $t('system.menu.title'),
|
|
|
+ },
|
|
|
+ component: () => import('#/views/system/menu/list.vue'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: '/system/dept',
|
|
|
+ name: 'SystemDept',
|
|
|
+ meta: {
|
|
|
+ icon: 'charm:organisation',
|
|
|
+ title: $t('system.dept.title'),
|
|
|
+ },
|
|
|
+ component: () => import('#/views/system/dept/list.vue'),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: '/system/log',
|
|
|
+ name: 'SystemLog',
|
|
|
+ meta: {
|
|
|
+ icon: 'mdi:clipboard-text-clock-outline',
|
|
|
+ title: $t('system.log.title'),
|
|
|
+ },
|
|
|
+ component: () => import('#/views/system/log/list.vue'),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+export default routes;
|
|
|
+```
|
|
|
+
|
|
|
+## system.json
|
|
|
+
|
|
|
+``` json
|
|
|
+{
|
|
|
+ "title": "System Management",
|
|
|
+ "dept": {
|
|
|
+ "name": "Department",
|
|
|
+ "title": "Department Management",
|
|
|
+ "deptName": "Department Name",
|
|
|
+ "status": "Status",
|
|
|
+ "createTime": "Create Time",
|
|
|
+ "remark": "Remark",
|
|
|
+ "operation": "Operation",
|
|
|
+ "parentDept": "Parent Department"
|
|
|
+ },
|
|
|
+ "menu": {
|
|
|
+ "title": "Menu Management",
|
|
|
+ "parent": "Parent Menu",
|
|
|
+ "menuTitle": "Title",
|
|
|
+ "menuName": "Menu Name",
|
|
|
+ "name": "Menu",
|
|
|
+ "type": "Type",
|
|
|
+ "typeCatalog": "Catalog",
|
|
|
+ "typeMenu": "Menu",
|
|
|
+ "typeButton": "Button",
|
|
|
+ "typeLink": "Link",
|
|
|
+ "typeEmbedded": "Embedded",
|
|
|
+ "icon": "Icon",
|
|
|
+ "activeIcon": "Active Icon",
|
|
|
+ "activePath": "Active Path",
|
|
|
+ "path": "Route Path",
|
|
|
+ "component": "Component",
|
|
|
+ "status": "Status",
|
|
|
+ "authCode": "Auth Code",
|
|
|
+ "badge": "Badge",
|
|
|
+ "operation": "Operation",
|
|
|
+ "linkSrc": "Link Address",
|
|
|
+ "affixTab": "Affix In Tabs",
|
|
|
+ "keepAlive": "Keep Alive",
|
|
|
+ "hideInMenu": "Hide In Menu",
|
|
|
+ "hideInTab": "Hide In Tabbar",
|
|
|
+ "hideChildrenInMenu": "Hide Children In Menu",
|
|
|
+ "hideInBreadcrumb": "Hide In Breadcrumb",
|
|
|
+ "advancedSettings": "Other Settings",
|
|
|
+ "activePathMustExist": "The path could not find a valid menu",
|
|
|
+ "activePathHelp": "When jumping to the current route, \nthe menu path that needs to be activated must be specified when it does not display in the navigation menu.",
|
|
|
+ "badgeType": {
|
|
|
+ "title": "Badge Type",
|
|
|
+ "dot": "Dot",
|
|
|
+ "normal": "Text",
|
|
|
+ "none": "None"
|
|
|
+ },
|
|
|
+ "badgeVariants": "Badge Style"
|
|
|
+ },
|
|
|
+ "role": {
|
|
|
+ "title": "Role Management",
|
|
|
+ "list": "Role List",
|
|
|
+ "name": "Role",
|
|
|
+ "roleName": "Role Name",
|
|
|
+ "id": "Role ID",
|
|
|
+ "status": "Status",
|
|
|
+ "remark": "Remark",
|
|
|
+ "createTime": "Creation Time",
|
|
|
+ "operation": "Operation",
|
|
|
+ "permissions": "Permissions",
|
|
|
+ "setPermissions": "Permissions"
|
|
|
+ }
|
|
|
+ ,
|
|
|
+ "log": {
|
|
|
+ "title": "Log Management",
|
|
|
+ "list": "Log List",
|
|
|
+ "level": "Level",
|
|
|
+ "appId": "AppID",
|
|
|
+ "time": "Time",
|
|
|
+ "sys": "System",
|
|
|
+ "ver": "Version",
|
|
|
+ "ip": "IP",
|
|
|
+ "uid": "User ID",
|
|
|
+ "device": "Device",
|
|
|
+ "model": "Model",
|
|
|
+ "country": "Country/Region",
|
|
|
+ "message": "Message"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## system.json
|
|
|
+
|
|
|
+``` json
|
|
|
+{
|
|
|
+ "dept": {
|
|
|
+ "list": "部门列表",
|
|
|
+ "createTime": "创建时间",
|
|
|
+ "deptName": "部门名称",
|
|
|
+ "name": "部门",
|
|
|
+ "operation": "操作",
|
|
|
+ "parentDept": "上级部门",
|
|
|
+ "remark": "备注",
|
|
|
+ "status": "状态",
|
|
|
+ "title": "部门管理"
|
|
|
+ },
|
|
|
+ "menu": {
|
|
|
+ "list": "菜单列表",
|
|
|
+ "activeIcon": "激活图标",
|
|
|
+ "activePath": "激活路径",
|
|
|
+ "activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径",
|
|
|
+ "activePathMustExist": "该路径未能找到有效的菜单",
|
|
|
+ "advancedSettings": "其它设置",
|
|
|
+ "affixTab": "固定在标签",
|
|
|
+ "authCode": "权限标识",
|
|
|
+ "badge": "徽章内容",
|
|
|
+ "badgeVariants": "徽标样式",
|
|
|
+ "badgeType": {
|
|
|
+ "dot": "点",
|
|
|
+ "none": "无",
|
|
|
+ "normal": "文字",
|
|
|
+ "title": "徽标类型"
|
|
|
+ },
|
|
|
+ "component": "页面组件",
|
|
|
+ "hideChildrenInMenu": "隐藏子菜单",
|
|
|
+ "hideInBreadcrumb": "在面包屑中隐藏",
|
|
|
+ "hideInMenu": "隐藏菜单",
|
|
|
+ "hideInTab": "在标签栏中隐藏",
|
|
|
+ "icon": "图标",
|
|
|
+ "keepAlive": "缓存标签页",
|
|
|
+ "linkSrc": "链接地址",
|
|
|
+ "menuName": "菜单名称",
|
|
|
+ "menuTitle": "标题",
|
|
|
+ "name": "菜单",
|
|
|
+ "operation": "操作",
|
|
|
+ "parent": "上级菜单",
|
|
|
+ "path": "路由地址",
|
|
|
+ "status": "状态",
|
|
|
+ "title": "菜单管理",
|
|
|
+ "type": "类型",
|
|
|
+ "typeButton": "按钮",
|
|
|
+ "typeCatalog": "目录",
|
|
|
+ "typeEmbedded": "内嵌",
|
|
|
+ "typeLink": "外链",
|
|
|
+ "typeMenu": "菜单"
|
|
|
+ },
|
|
|
+ "role": {
|
|
|
+ "title": "角色管理",
|
|
|
+ "list": "角色列表",
|
|
|
+ "name": "角色",
|
|
|
+ "roleName": "角色名称",
|
|
|
+ "id": "角色ID",
|
|
|
+ "status": "状态",
|
|
|
+ "remark": "备注",
|
|
|
+ "createTime": "创建时间",
|
|
|
+ "operation": "操作",
|
|
|
+ "permissions": "权限",
|
|
|
+ "setPermissions": "授权"
|
|
|
+ },
|
|
|
+ "log": {
|
|
|
+ "title": "日志管理",
|
|
|
+ "list": "日志列表",
|
|
|
+ "level": "级别",
|
|
|
+ "appId": "AppID",
|
|
|
+ "time": "时间",
|
|
|
+ "sys": "系统",
|
|
|
+ "ver": "版本",
|
|
|
+ "ip": "IP",
|
|
|
+ "uid": "用户ID",
|
|
|
+ "device": "设备",
|
|
|
+ "model": "机型",
|
|
|
+ "country": "国家/地区",
|
|
|
+ "message": "内容"
|
|
|
+ },
|
|
|
+ "title": "系统管理"
|
|
|
+}
|
|
|
+```
|