Merge branch 'main' of https://git.apinb.com/ops/front
This commit is contained in:
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
|||||||
vue-router:
|
vue-router:
|
||||||
specifier: '5'
|
specifier: '5'
|
||||||
version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
||||||
|
vue-web-terminal:
|
||||||
|
specifier: ^3.4.1
|
||||||
|
version: 3.4.1(typescript@5.9.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@arco-plugins/vite-vue':
|
'@arco-plugins/vite-vue':
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
@@ -1551,6 +1554,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
|
resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
clipboard@2.0.11:
|
||||||
|
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1865,6 +1871,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
delegate@3.2.0:
|
||||||
|
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
|
||||||
|
|
||||||
dir-glob@2.2.2:
|
dir-glob@2.2.2:
|
||||||
resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==}
|
resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2411,6 +2420,9 @@ packages:
|
|||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
good-listener@1.2.2:
|
||||||
|
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
|
||||||
|
|
||||||
gopd@1.2.0:
|
gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3707,6 +3719,9 @@ packages:
|
|||||||
scule@1.3.0:
|
scule@1.3.0:
|
||||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||||
|
|
||||||
|
select@1.1.2:
|
||||||
|
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||||
|
|
||||||
semver@5.7.2:
|
semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4056,6 +4071,9 @@ packages:
|
|||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
|
tiny-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
|
||||||
|
|
||||||
tinyexec@1.0.2:
|
tinyexec@1.0.2:
|
||||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4344,6 +4362,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
|
vue-json-viewer@3.0.4:
|
||||||
|
resolution: {integrity: sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.2
|
||||||
|
|
||||||
vue-router@5.0.3:
|
vue-router@5.0.3:
|
||||||
resolution: {integrity: sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==}
|
resolution: {integrity: sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4365,6 +4388,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.0.0'
|
typescript: '>=5.0.0'
|
||||||
|
|
||||||
|
vue-web-terminal@3.4.1:
|
||||||
|
resolution: {integrity: sha512-+gU28qClqvIZQlzokcvDS2tbFpGfIJKIPc6dvLm2VYX110c6NOh7mV1YrcUESnaE5VQ9DgxqtIbr1YraEA/GRQ==}
|
||||||
|
|
||||||
vue@3.5.29:
|
vue@3.5.29:
|
||||||
resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==}
|
resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5884,6 +5910,12 @@ snapshots:
|
|||||||
slice-ansi: 8.0.0
|
slice-ansi: 8.0.0
|
||||||
string-width: 8.2.0
|
string-width: 8.2.0
|
||||||
|
|
||||||
|
clipboard@2.0.11:
|
||||||
|
dependencies:
|
||||||
|
good-listener: 1.2.2
|
||||||
|
select: 1.1.2
|
||||||
|
tiny-emitter: 2.1.0
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -6178,6 +6210,8 @@ snapshots:
|
|||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
|
delegate@3.2.0: {}
|
||||||
|
|
||||||
dir-glob@2.2.2:
|
dir-glob@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 3.0.0
|
path-type: 3.0.0
|
||||||
@@ -6882,6 +6916,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
good-listener@1.2.2:
|
||||||
|
dependencies:
|
||||||
|
delegate: 3.2.0
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
@@ -8164,6 +8202,8 @@ snapshots:
|
|||||||
|
|
||||||
scule@1.3.0: {}
|
scule@1.3.0: {}
|
||||||
|
|
||||||
|
select@1.1.2: {}
|
||||||
|
|
||||||
semver@5.7.2: {}
|
semver@5.7.2: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -8630,6 +8670,8 @@ snapshots:
|
|||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
|
tiny-emitter@2.1.0: {}
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
tinyexec@1.0.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
@@ -8953,6 +8995,11 @@ snapshots:
|
|||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.29(typescript@5.9.3)
|
vue: 3.5.29(typescript@5.9.3)
|
||||||
|
|
||||||
|
vue-json-viewer@3.0.4(vue@3.5.29(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
clipboard: 2.0.11
|
||||||
|
vue: 3.5.29(typescript@5.9.3)
|
||||||
|
|
||||||
vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)):
|
vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.1
|
||||||
@@ -8983,6 +9030,13 @@ snapshots:
|
|||||||
'@vue/language-core': 3.2.5
|
'@vue/language-core': 3.2.5
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
vue-web-terminal@3.4.1(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.29(typescript@5.9.3)
|
||||||
|
vue-json-viewer: 3.0.4(vue@3.5.29(typescript@5.9.3))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
vue@3.5.29(typescript@5.9.3):
|
vue@3.5.29(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-dom': 3.5.29
|
'@vue/compiler-dom': 3.5.29
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
import { request } from "@/api/request";
|
import { request } from "@/api/request";
|
||||||
|
|
||||||
|
/** 许可证配置(与 DC-Control `LicenceConfig` / 接口 `data` 一致;字段按实际响应可能部分缺失) */
|
||||||
|
export interface LicenceConfig {
|
||||||
|
title?: string
|
||||||
|
version?: string
|
||||||
|
company_name?: string
|
||||||
|
create_time?: string
|
||||||
|
expire_time?: string
|
||||||
|
machine_code?: string
|
||||||
|
max_database?: number
|
||||||
|
max_middleware?: number
|
||||||
|
max_pc?: number
|
||||||
|
max_server?: number
|
||||||
|
max_client?: number
|
||||||
|
max_user?: number
|
||||||
|
max_role?: number
|
||||||
|
max_permission?: number
|
||||||
|
max_menu?: number
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取 采集器 */
|
/** 获取 采集器 */
|
||||||
export const fetchCollectors = (data: { page: number, size: number, keyword?: string }) => request.get("/DC-Control/v1/collectors", { params: data });
|
export const fetchCollectors = (data: { page: number, size: number, keyword?: string }) => request.get("/DC-Control/v1/collectors", { params: data });
|
||||||
|
|
||||||
@@ -19,4 +38,5 @@ export const updateCollector = (data: any) => request.put(`/DC-Control/v1/collec
|
|||||||
export const fetchCollectorStatistics = () => request.get("/DC-Control/v1/statistics");
|
export const fetchCollectorStatistics = () => request.get("/DC-Control/v1/statistics");
|
||||||
|
|
||||||
/** 获取 许可证信息 */
|
/** 获取 许可证信息 */
|
||||||
export const fetchLicenseInfo = () => request.get("/DC-Control/v1/license");
|
export const fetchLicenseInfo = () =>
|
||||||
|
request.get<{ code?: number; data?: LicenceConfig; message?: string }>("/DC-Control/v1/license");
|
||||||
|
|||||||
@@ -60,7 +60,12 @@
|
|||||||
:data="data"
|
:data="data"
|
||||||
:bordered="bordered"
|
:bordered="bordered"
|
||||||
:size="size"
|
:size="size"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:scroll="scroll"
|
||||||
@page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
|
@page-size-change="onPageSizeChange"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
|
@row-click="onRowClick"
|
||||||
>
|
>
|
||||||
<!-- 动态插槽:根据 columns 的 slotName 动态渲染 -->
|
<!-- 动态插槽:根据 columns 的 slotName 动态渲染 -->
|
||||||
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
||||||
@@ -72,7 +77,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch, nextTick, onUnmounted, PropType } from 'vue'
|
import { computed, ref, watch, nextTick, onUnmounted, PropType } from 'vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
|
|
||||||
@@ -107,6 +112,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
rowSelection: {
|
||||||
|
type: Object as PropType<TableRowSelection | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
scroll: {
|
||||||
|
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
showToolbar: {
|
showToolbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -147,6 +160,9 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'page-change', current: number): void
|
(e: 'page-change', current: number): void
|
||||||
|
(e: 'page-size-change', pageSize: number): void
|
||||||
|
(e: 'selection-change', rowKeys: (string | number)[]): void
|
||||||
|
(e: 'row-click', record: any, ev: Event): void
|
||||||
(e: 'refresh'): void
|
(e: 'refresh'): void
|
||||||
(e: 'download'): void
|
(e: 'download'): void
|
||||||
(e: 'density-change', size: SizeProps): void
|
(e: 'density-change', size: SizeProps): void
|
||||||
@@ -178,6 +194,18 @@ const onPageChange = (current: number) => {
|
|||||||
emit('page-change', current)
|
emit('page-change', current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPageSizeChange = (pageSize: number) => {
|
||||||
|
emit('page-size-change', pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectionChange = (rowKeys: (string | number)[]) => {
|
||||||
|
emit('selection-change', rowKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRowClick = (record: any, ev: Event) => {
|
||||||
|
emit('row-click', record, ev)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import { listenerRouteChange } from '@/utils/route-listener'
|
|||||||
import { compile, computed, defineComponent, h, ref } from 'vue'
|
import { compile, computed, defineComponent, h, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { RouteMeta } from 'vue-router'
|
import type { RouteMeta } from 'vue-router'
|
||||||
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router'
|
import {
|
||||||
|
isNavigationFailure,
|
||||||
|
NavigationFailureType,
|
||||||
|
RouteRecordRaw,
|
||||||
|
useRoute,
|
||||||
|
useRouter,
|
||||||
|
} from 'vue-router'
|
||||||
import useMenuTree from './use-menu-tree'
|
import useMenuTree from './use-menu-tree'
|
||||||
import { COMMON_ICONS } from '@/views/ops/pages/system-settings/menu-management/menuIcons'
|
import { COMMON_ICONS } from '@/views/ops/pages/system-settings/menu-management/menuIcons'
|
||||||
|
|
||||||
@@ -45,10 +51,20 @@ export default defineComponent({
|
|||||||
selectedKey.value = [item.name as string]
|
selectedKey.value = [item.name as string]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('item', item)
|
const name = item.name
|
||||||
// Trigger router change
|
if (name == null || name === '') {
|
||||||
router.push({
|
console.warn('[Menu] 无法跳转:路由缺少 name', item.path, item.meta?.locale)
|
||||||
name: item.name,
|
return
|
||||||
|
}
|
||||||
|
if (!router.hasRoute(name as string)) {
|
||||||
|
console.warn('[Menu] 无法跳转:未注册的路由 name', name, item.meta?.locale)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({ name }).catch((err) => {
|
||||||
|
if (isNavigationFailure(err, NavigationFailureType.duplicated)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error('[Menu] 路由跳转失败', name, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const findMenuOpenKeys = (target: string) => {
|
const findMenuOpenKeys = (target: string) => {
|
||||||
@@ -80,7 +96,8 @@ export default defineComponent({
|
|||||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value])
|
const keySet = new Set([...menuOpenKeys, ...openKeys.value])
|
||||||
openKeys.value = [...keySet]
|
openKeys.value = [...keySet]
|
||||||
|
|
||||||
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]]
|
const leafKey = (activeMenu || menuOpenKeys[menuOpenKeys.length - 1]) as string | undefined
|
||||||
|
selectedKey.value = leafKey ? [leafKey] : []
|
||||||
}
|
}
|
||||||
}, true)
|
}, true)
|
||||||
const setCollapse = (val: boolean) => {
|
const setCollapse = (val: boolean) => {
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export default function useMenuTree() {
|
|||||||
return appClientMenus
|
return appClientMenus
|
||||||
})
|
})
|
||||||
const menuTree = computed(() => {
|
const menuTree = computed(() => {
|
||||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[]
|
const copyRouter = cloneDeep(appRoute.value || []) as RouteRecordNormalized[]
|
||||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||||
return (a.meta.order || 0) - (b.meta.order || 0)
|
return (a.meta.order || 0) - (b.meta.order || 0)
|
||||||
})
|
})
|
||||||
function travel(_routes: RouteRecordRaw[], layer: number) {
|
function travel(_routes: RouteRecordRaw[], layer: number): RouteRecordRaw[] {
|
||||||
if (!_routes) return null
|
if (!_routes?.length) return []
|
||||||
|
|
||||||
const collector: any = _routes.map((element) => {
|
const collector: any = _routes.map((element) => {
|
||||||
// no access
|
// no access
|
||||||
@@ -44,16 +44,8 @@ export default function useMenuTree() {
|
|||||||
element.children = subItem
|
element.children = subItem
|
||||||
return element
|
return element
|
||||||
}
|
}
|
||||||
// the else logic
|
|
||||||
if (layer > 1) {
|
|
||||||
element.children = subItem
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.meta?.hideInMenu === false) {
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 子级全部被权限/隐藏规则过滤时,不再把父级当成可点击叶子(避免 push 父级 name 无对应页面)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
return collector.filter(Boolean)
|
return collector.filter(Boolean)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
:bordered="bordered"
|
:bordered="bordered"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:scroll="scroll"
|
||||||
:show-toolbar="showToolbar"
|
:show-toolbar="showToolbar"
|
||||||
:show-download="showDownload"
|
:show-download="showDownload"
|
||||||
:show-refresh="showRefresh"
|
:show-refresh="showRefresh"
|
||||||
@@ -36,6 +38,9 @@
|
|||||||
:density-tooltip-text="densityTooltipText"
|
:density-tooltip-text="densityTooltipText"
|
||||||
:column-setting-tooltip-text="columnSettingTooltipText"
|
:column-setting-tooltip-text="columnSettingTooltipText"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
|
@page-size-change="handlePageSizeChange"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@row-click="handleRowClick"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
@download="handleDownload"
|
@download="handleDownload"
|
||||||
@density-change="handleDensityChange"
|
@density-change="handleDensityChange"
|
||||||
@@ -61,7 +66,7 @@ import { computed, PropType } from 'vue'
|
|||||||
import SearchForm from '../search-form/index.vue'
|
import SearchForm from '../search-form/index.vue'
|
||||||
import type { FormItem } from '../search-form/types'
|
import type { FormItem } from '../search-form/types'
|
||||||
import DataTable from '../data-table/index.vue'
|
import DataTable from '../data-table/index.vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
|
||||||
|
|
||||||
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
|
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
|
||||||
|
|
||||||
@@ -115,6 +120,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
rowSelection: {
|
||||||
|
type: Object as PropType<TableRowSelection | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
scroll: {
|
||||||
|
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
// 工具栏相关
|
// 工具栏相关
|
||||||
showToolbar: {
|
showToolbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -164,6 +177,9 @@ const emit = defineEmits<{
|
|||||||
(e: 'search'): void
|
(e: 'search'): void
|
||||||
(e: 'reset'): void
|
(e: 'reset'): void
|
||||||
(e: 'page-change', current: number): void
|
(e: 'page-change', current: number): void
|
||||||
|
(e: 'page-size-change', pageSize: number): void
|
||||||
|
(e: 'selection-change', rowKeys: (string | number)[]): void
|
||||||
|
(e: 'row-click', record: any, ev: Event): void
|
||||||
(e: 'refresh'): void
|
(e: 'refresh'): void
|
||||||
(e: 'download'): void
|
(e: 'download'): void
|
||||||
(e: 'density-change', size: SizeProps): void
|
(e: 'density-change', size: SizeProps): void
|
||||||
@@ -192,6 +208,18 @@ const handlePageChange = (current: number) => {
|
|||||||
emit('page-change', current)
|
emit('page-change', current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (pageSize: number) => {
|
||||||
|
emit('page-size-change', pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (rowKeys: (string | number)[]) => {
|
||||||
|
emit('selection-change', rowKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowClick = (record: any, ev: Event) => {
|
||||||
|
emit('row-click', record, ev)
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
'menu.ops.systemSettings': 'System Settings',
|
'menu.ops.systemSettings': 'System Settings',
|
||||||
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
||||||
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
||||||
|
'menu.ops.systemSettings.licenseCenter': 'License Center',
|
||||||
'menu.ops.webTest': 'Web Test',
|
'menu.ops.webTest': 'Web Test',
|
||||||
'menu.ops.report': 'Report Management',
|
'menu.ops.report': 'Report Management',
|
||||||
'menu.ops.report.history': 'Report History',
|
'menu.ops.report.history': 'Report History',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default {
|
|||||||
'menu.ops.systemSettings': '系统设置',
|
'menu.ops.systemSettings': '系统设置',
|
||||||
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
||||||
'menu.ops.systemSettings.systemLogs': '系统日志',
|
'menu.ops.systemSettings.systemLogs': '系统日志',
|
||||||
|
'menu.ops.systemSettings.licenseCenter': '许可授权中心',
|
||||||
'menu.ops.webTest': '网页测试',
|
'menu.ops.webTest': '网页测试',
|
||||||
'menu.ops.report': '报表管理',
|
'menu.ops.report': '报表管理',
|
||||||
'menu.ops.report.history': '报表历史',
|
'menu.ops.report.history': '报表历史',
|
||||||
|
|||||||
@@ -181,6 +181,17 @@ function extractRelativePath(childPath: string, parentPath: string): string {
|
|||||||
* @param parentIsFull 父级菜单的 is_full 字段
|
* @param parentIsFull 父级菜单的 is_full 字段
|
||||||
* @returns 子路由配置数组
|
* @returns 子路由配置数组
|
||||||
*/
|
*/
|
||||||
|
/** 许可授权中心菜单路径(严格匹配末段,避免 menu_path 中含 query 等导致误伤其它菜单如用户管理) */
|
||||||
|
const LICENSE_CENTER_VIEW = 'ops/pages/system-settings/license-center'
|
||||||
|
|
||||||
|
function isLicenseCenterMenuPath(fullPath: string): boolean {
|
||||||
|
const path = String(fullPath ?? '')
|
||||||
|
.trim()
|
||||||
|
.split('?')[0]
|
||||||
|
.replace(/\/+$/, '')
|
||||||
|
return path.endsWith('/license-center') || path === 'license-center'
|
||||||
|
}
|
||||||
|
|
||||||
function transformChildRoutes(
|
function transformChildRoutes(
|
||||||
children: ServerMenuItem[],
|
children: ServerMenuItem[],
|
||||||
parentComponent?: string,
|
parentComponent?: string,
|
||||||
@@ -188,10 +199,17 @@ function transformChildRoutes(
|
|||||||
parentIsFull?: boolean
|
parentIsFull?: boolean
|
||||||
): AppRouteRecordRaw[] {
|
): AppRouteRecordRaw[] {
|
||||||
return children.map((child) => {
|
return children.map((child) => {
|
||||||
// 优先使用子菜单自己的 component,否则继承父级的 component
|
const childFullPath = String(child.menu_path ?? child.path ?? '').trim()
|
||||||
const componentPath = child.component || parentComponent
|
|
||||||
// 计算子路由的相对路径
|
// 已配置 component 的菜单绝不覆盖;仅对许可页做路径/code 兜底,避免 includes 误匹配
|
||||||
const childFullPath = child.menu_path || child.path || ''
|
let componentPath = child.component || parentComponent
|
||||||
|
if (
|
||||||
|
!child.component &&
|
||||||
|
(isLicenseCenterMenuPath(childFullPath) || child.code === 'LicenseCenter')
|
||||||
|
) {
|
||||||
|
componentPath = LICENSE_CENTER_VIEW
|
||||||
|
}
|
||||||
|
|
||||||
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
||||||
|
|
||||||
const route: AppRouteRecordRaw = {
|
const route: AppRouteRecordRaw = {
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ const OPS: AppRouteRecordRaw = {
|
|||||||
roles: ['*'],
|
roles: ['*'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'license-center',
|
||||||
|
name: 'OpsLicenseCenter',
|
||||||
|
component: () => import('@/views/ops/pages/system-settings/license-center/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.ops.systemSettings.licenseCenter',
|
||||||
|
requiresAuth: true,
|
||||||
|
roles: ['*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'web-test',
|
path: 'web-test',
|
||||||
name: 'WebTest',
|
name: 'WebTest',
|
||||||
|
|||||||
@@ -1,43 +1,4 @@
|
|||||||
import { DEFAULT_LAYOUT } from '../base'
|
import type { AppRouteRecordRaw } from '../types'
|
||||||
import { AppRouteRecordRaw } from '../types'
|
|
||||||
|
|
||||||
const REMOTE: AppRouteRecordRaw = {
|
/** 占位:勿导出空对象 `{}`,否则会被当作一条无效路由加入 router,导致部分菜单匹配异常 */
|
||||||
// path: '/dc',
|
export default [] as AppRouteRecordRaw[]
|
||||||
// name: 'DC',
|
|
||||||
// component: DEFAULT_LAYOUT,
|
|
||||||
// meta: {
|
|
||||||
// locale: 'menu.dc',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// icon: 'icon-desktop',
|
|
||||||
// order: 99,
|
|
||||||
// hideInMenu: true,
|
|
||||||
// },
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// path: 'detail',
|
|
||||||
// name: 'DCDetail',
|
|
||||||
// component: () => import('@/views/ops/pages/dc/detail/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// locale: 'menu.dc.detail',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// // is_full: true,
|
|
||||||
// isNewTab: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'remote',
|
|
||||||
// name: 'DCRemote',
|
|
||||||
// component: () => import('@/views/ops/pages/dc/remote/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// locale: 'menu.dc.remote',
|
|
||||||
// requiresAuth: true,
|
|
||||||
// roles: ['*'],
|
|
||||||
// // is_full: true,
|
|
||||||
// isNewTab: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default REMOTE
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notice-channel-container">
|
<div class="container">
|
||||||
<SearchTable
|
<search-table
|
||||||
v-model:form-model="formModel"
|
:form-model="formModel"
|
||||||
:form-items="formItems"
|
:form-items="formItems"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
title="通知渠道管理"
|
title="通知渠道管理"
|
||||||
search-button-text="查询"
|
search-button-text="查询"
|
||||||
reset-button-text="重置"
|
reset-button-text="重置"
|
||||||
|
refresh-tooltip-text="刷新数据"
|
||||||
|
density-tooltip-text="表格密度"
|
||||||
|
column-setting-tooltip-text="列设置"
|
||||||
|
@update:form-model="handleFormModelUpdate"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@@ -56,7 +60,7 @@
|
|||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</SearchTable>
|
</search-table>
|
||||||
|
|
||||||
<!-- 表单对话框 -->
|
<!-- 表单对话框 -->
|
||||||
<channel-form-dialog
|
<channel-form-dialog
|
||||||
@@ -95,12 +99,15 @@ const pagination = reactive({
|
|||||||
total: 0,
|
total: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索表单数据
|
|
||||||
const formModel = ref<Record<string, any>>({
|
const formModel = ref<Record<string, any>>({
|
||||||
name: '',
|
name: '',
|
||||||
type: '',
|
type: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleFormModelUpdate = (value: Record<string, any>) => {
|
||||||
|
formModel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
// 表单项配置
|
// 表单项配置
|
||||||
const formItems = computed<FormItem[]>(() => [
|
const formItems = computed<FormItem[]>(() => [
|
||||||
{
|
{
|
||||||
@@ -240,9 +247,9 @@ const handlePageChange = (current: number) => {
|
|||||||
fetchList()
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
fetchList()
|
fetchList()
|
||||||
|
Message.success('数据已刷新')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新建
|
// 新建
|
||||||
@@ -292,7 +299,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.notice-channel-container {
|
.container {
|
||||||
padding: 0 20px 20px 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
:data="dispatchRuleData"
|
:data="dispatchRuleData"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
size="small"
|
size="medium"
|
||||||
>
|
>
|
||||||
<template #columns>
|
<template #columns>
|
||||||
<a-table-column title="告警级别" data-index="severity" :width="150">
|
<a-table-column title="告警级别" data-index="severity" :width="150">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
:loading="ruleLoading"
|
:loading="ruleLoading"
|
||||||
:pagination="rulePagination"
|
:pagination="rulePagination"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
size="small"
|
size="medium"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
>
|
>
|
||||||
<template #columns>
|
<template #columns>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
:data="processRecords"
|
:data="processRecords"
|
||||||
:columns="processColumns"
|
:columns="processColumns"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
size="small"
|
size="medium"
|
||||||
>
|
>
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-tag :color="getActionColor(record.action)">
|
<a-tag :color="getActionColor(record.action)">
|
||||||
|
|||||||
@@ -1,188 +1,144 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="alert-tackle-container">
|
<div class="container">
|
||||||
<a-card :bordered="false" class="general-card">
|
<search-table
|
||||||
<search-form
|
:form-model="formModel"
|
||||||
v-model="searchParams"
|
:form-items="formItems"
|
||||||
:form-items="searchFormConfig"
|
:data="tableData"
|
||||||
:show-buttons="false"
|
:columns="columns"
|
||||||
>
|
:loading="loading"
|
||||||
<template #extra>
|
:pagination="pagination"
|
||||||
<a-range-picker
|
title="告警受理处理"
|
||||||
v-model="timeRange"
|
search-button-text="查询"
|
||||||
:time-picker-props="{ defaultValue: '00:00:00' }"
|
reset-button-text="重置"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
refresh-tooltip-text="刷新数据"
|
||||||
show-time
|
density-tooltip-text="表格密度"
|
||||||
style="width: 380px; margin-right: 12px"
|
column-setting-tooltip-text="列设置"
|
||||||
/>
|
@update:form-model="handleFormModelUpdate"
|
||||||
<a-button type="primary" @click="handleSearch">
|
@search="handleSearch"
|
||||||
<template #icon>
|
@reset="handleReset"
|
||||||
<icon-search />
|
@page-change="handlePageChange"
|
||||||
</template>
|
@page-size-change="handlePageSizeChange"
|
||||||
查询
|
@row-click="handleRowClick"
|
||||||
</a-button>
|
@refresh="handleRefresh"
|
||||||
<a-button @click="handleReset">
|
>
|
||||||
<template #icon>
|
<template #form-items>
|
||||||
<icon-refresh />
|
<a-col :span="16">
|
||||||
</template>
|
<a-form-item
|
||||||
重置
|
label="时间范围"
|
||||||
</a-button>
|
:label-col-props="{ span: 6 }"
|
||||||
</template>
|
:wrapper-col-props="{ span: 18 }"
|
||||||
</search-form>
|
>
|
||||||
|
<a-range-picker
|
||||||
<a-divider style="margin: 0" />
|
v-model="timeRange"
|
||||||
|
:time-picker-props="{ defaultValue: '00:00:00' }"
|
||||||
<a-row class="toolbar">
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
<a-col :span="12">
|
show-time
|
||||||
<a-space>
|
style="width: 100%; max-width: 420px"
|
||||||
<a-button
|
/>
|
||||||
v-if="selectedRowKeys.length > 0"
|
</a-form-item>
|
||||||
type="primary"
|
|
||||||
status="danger"
|
|
||||||
@click="handleBatchAck"
|
|
||||||
>
|
|
||||||
批量确认 ({{ selectedRowKeys.length }})
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</template>
|
||||||
|
|
||||||
<a-table
|
<template #index="{ rowIndex }">
|
||||||
:data="tableData"
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
:columns="columns"
|
</template>
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
:row-selection="rowSelection"
|
|
||||||
:scroll="{ x: 2000 }"
|
|
||||||
@page-change="onPageChange"
|
|
||||||
@page-size-change="onPageSizeChange"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
@row-click="handleRowClick"
|
|
||||||
>
|
|
||||||
<template #index="{ rowIndex }">
|
|
||||||
{{ rowIndex + 1 }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #status="{ record }">
|
<template #status="{ record }">
|
||||||
<a-tag :color="getStatusColor(record.status)">
|
<a-tag :color="getStatusColor(record.status)">
|
||||||
{{ getStatusText(record.status) }}
|
{{ getStatusText(record.status) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #severity="{ record }">
|
<template #severity="{ record }">
|
||||||
<a-tag v-if="record.severity" :color="record.severity.color">
|
<a-tag v-if="record.severity" :color="record.severity.color">
|
||||||
{{ record.severity.name || record.severity.code }}
|
{{ record.severity.name || record.severity.code }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #starts_at="{ record }">
|
<template #starts_at="{ record }">
|
||||||
{{ formatDateTime(record.starts_at) }}
|
{{ formatDateTime(record.starts_at) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #duration="{ record }">
|
<template #duration="{ record }">
|
||||||
{{ formatDuration(record.duration) }}
|
{{ formatDuration(record.duration) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #process_status="{ record }">
|
<template #process_status="{ record }">
|
||||||
<a-tag v-if="record.process_status" color="arcoblue">
|
<a-tag v-if="record.process_status" color="arcoblue">
|
||||||
{{ record.process_status }}
|
{{ record.process_status }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #processed_at="{ record }">
|
<template #processed_at="{ record }">
|
||||||
{{ record.processed_at ? formatDateTime(record.processed_at) : '-' }}
|
{{ record.processed_at ? formatDateTime(record.processed_at) : '-' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #notify_status="{ record }">
|
<template #notify_status="{ record }">
|
||||||
<a-tag v-if="record.notify_status" :color="getNotifyStatusColor(record.notify_status)">
|
<a-tag v-if="record.notify_status" :color="getNotifyStatusColor(record.notify_status)">
|
||||||
{{ getNotifyStatusText(record.notify_status) }}
|
{{ getNotifyStatusText(record.notify_status) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #labels="{ record }">
|
<template #labels="{ record }">
|
||||||
<a-space v-if="parsedLabels(record.labels)" wrap>
|
<a-tooltip v-if="formatLabelsLine(record.labels)" :content="formatLabelsLine(record.labels)">
|
||||||
<a-tag
|
<span class="cell-ellipsis">{{ formatLabelsLine(record.labels) }}</span>
|
||||||
v-for="(value, key) in parsedLabels(record.labels)"
|
</a-tooltip>
|
||||||
:key="key"
|
<span v-else>-</span>
|
||||||
size="small"
|
</template>
|
||||||
>
|
|
||||||
{{ key }}: {{ value }}
|
|
||||||
</a-tag>
|
|
||||||
</a-space>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<a-space>
|
<a-space size="small" :wrap="false">
|
||||||
<a-button type="text" size="small" @click="handleAck(record)">
|
<a-button type="text" size="small" @click.stop="handleAck(record)">
|
||||||
<template #icon>
|
确认
|
||||||
<icon-check />
|
</a-button>
|
||||||
</template>
|
<a-button type="text" size="small" @click.stop="handleResolve(record)">
|
||||||
确认
|
解决
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" @click.stop="handleSilence(record)">
|
||||||
|
屏蔽
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" @click.stop="handleComment(record)">
|
||||||
|
评论
|
||||||
|
</a-button>
|
||||||
|
<a-dropdown @select="(v) => handleMoreSelect(v, record)">
|
||||||
|
<a-button type="text" size="small" @click.stop>
|
||||||
|
更多
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="text" size="small" @click="handleResolve(record)">
|
<template #content>
|
||||||
<template #icon>
|
<a-doption value="detail">详情</a-doption>
|
||||||
<icon-check-circle />
|
<a-doption value="process">处理记录</a-doption>
|
||||||
</template>
|
</template>
|
||||||
解决
|
</a-dropdown>
|
||||||
</a-button>
|
</a-space>
|
||||||
<a-button type="text" size="small" @click="handleSilence(record)">
|
</template>
|
||||||
<template #icon>
|
</search-table>
|
||||||
<icon-eye-invisible />
|
|
||||||
</template>
|
|
||||||
屏蔽
|
|
||||||
</a-button>
|
|
||||||
<a-button type="text" size="small" @click="handleComment(record)">
|
|
||||||
<template #icon>
|
|
||||||
<icon-message />
|
|
||||||
</template>
|
|
||||||
评论
|
|
||||||
</a-button>
|
|
||||||
<a-dropdown @select="handleMoreAction($event, record)">
|
|
||||||
<a-button type="text" size="small">
|
|
||||||
更多
|
|
||||||
<icon-down />
|
|
||||||
</a-button>
|
|
||||||
<template #content>
|
|
||||||
<a-doption value="detail">详情</a-doption>
|
|
||||||
<a-doption value="process">处理记录</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
|
|
||||||
<!-- 确认对话框 -->
|
|
||||||
<ack-dialog
|
<ack-dialog
|
||||||
v-model:visible="ackDialogVisible"
|
v-model:visible="ackDialogVisible"
|
||||||
:alert-record-id="currentRecord.id"
|
:alert-record-id="currentRecord.id"
|
||||||
@success="handleSuccess"
|
@success="handleSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 解决对话框 -->
|
|
||||||
<resolve-dialog
|
<resolve-dialog
|
||||||
v-model:visible="resolveDialogVisible"
|
v-model:visible="resolveDialogVisible"
|
||||||
:alert-record-id="currentRecord.id"
|
:alert-record-id="currentRecord.id"
|
||||||
@success="handleSuccess"
|
@success="handleSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 屏蔽对话框 -->
|
|
||||||
<silence-dialog
|
<silence-dialog
|
||||||
v-model:visible="silenceDialogVisible"
|
v-model:visible="silenceDialogVisible"
|
||||||
:alert-record-id="currentRecord.id"
|
:alert-record-id="currentRecord.id"
|
||||||
@success="handleSuccess"
|
@success="handleSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 评论对话框 -->
|
|
||||||
<comment-dialog
|
<comment-dialog
|
||||||
v-model:visible="commentDialogVisible"
|
v-model:visible="commentDialogVisible"
|
||||||
:alert-record-id="currentRecord.id"
|
:alert-record-id="currentRecord.id"
|
||||||
@success="handleSuccess"
|
@success="handleSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 详情对话框 -->
|
|
||||||
<detail-dialog
|
<detail-dialog
|
||||||
v-model:visible="detailDialogVisible"
|
v-model:visible="detailDialogVisible"
|
||||||
:alert-record-id="currentRecord.id"
|
:alert-record-id="currentRecord.id"
|
||||||
@@ -193,21 +149,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import {
|
|
||||||
IconSearch,
|
|
||||||
IconRefresh,
|
|
||||||
IconCheck,
|
|
||||||
IconCheckCircle,
|
|
||||||
IconEyeInvisible,
|
|
||||||
IconMessage,
|
|
||||||
IconDown,
|
|
||||||
} from '@arco-design/web-vue/es/icon'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
import SearchForm from '@/components/search-form/index.vue'
|
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface'
|
||||||
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
import { searchFormConfig } from './config/search-form'
|
import { searchFormConfig } from './config/search-form'
|
||||||
import { columns } from './config/columns'
|
import { columns } from './config/columns'
|
||||||
import { fetchAlertRecords, createAlertProcess } from '@/api/ops/alertRecord'
|
import { fetchAlertRecords } from '@/api/ops/alertRecord'
|
||||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||||
|
|
||||||
import AckDialog from './components/AckDialog.vue'
|
import AckDialog from './components/AckDialog.vue'
|
||||||
@@ -218,21 +166,17 @@ import DetailDialog from './components/DetailDialog.vue'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
const timeRange = ref<any[]>([])
|
const timeRange = ref<any[]>([])
|
||||||
const selectedRowKeys = ref<number[]>([])
|
|
||||||
const currentRecord = ref<any>({})
|
const currentRecord = ref<any>({})
|
||||||
|
|
||||||
// 对话框状态
|
|
||||||
const ackDialogVisible = ref(false)
|
const ackDialogVisible = ref(false)
|
||||||
const resolveDialogVisible = ref(false)
|
const resolveDialogVisible = ref(false)
|
||||||
const silenceDialogVisible = ref(false)
|
const silenceDialogVisible = ref(false)
|
||||||
const commentDialogVisible = ref(false)
|
const commentDialogVisible = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
|
|
||||||
// 分页
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -242,16 +186,23 @@ const pagination = reactive({
|
|||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 行选择
|
const severityOptions = ref<SelectOptionData[]>([])
|
||||||
const rowSelection = computed(() => ({
|
|
||||||
type: 'checkbox',
|
|
||||||
showCheckedAll: true,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 搜索参数
|
const formModel = ref<Record<string, any>>({
|
||||||
const searchParams = ref<any>({})
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
severity_id: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const formItems = computed<FormItem[]>(() =>
|
||||||
|
searchFormConfig.map((item) => {
|
||||||
|
if (item.field === 'severity_id') {
|
||||||
|
return { ...item, options: severityOptions.value }
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// 加载告警级别列表
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadSeverityOptions()
|
await loadSeverityOptions()
|
||||||
handleSearch()
|
handleSearch()
|
||||||
@@ -259,51 +210,58 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const loadSeverityOptions = async () => {
|
const loadSeverityOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await fetchAlertLevelList({ page: 1, page_size: 100 })
|
const res = await fetchAlertLevelList({ page: 1, page_size: 100 })
|
||||||
const severityConfig = searchFormConfig.find((item) => item.field === 'severity_id')
|
const list = res.details?.data ?? (res as any).data ?? []
|
||||||
if (severityConfig && result.details) {
|
severityOptions.value = list.map((item: any) => ({
|
||||||
severityConfig.options = result.data.map((item: any) => ({
|
label: item.name || item.code,
|
||||||
label: item.name || item.code,
|
value: item.id,
|
||||||
value: item.id,
|
}))
|
||||||
}))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载告警级别失败:', error)
|
console.error('加载告警级别失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
const handleFormModelUpdate = (value: Record<string, any>) => {
|
||||||
|
formModel.value = value
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
|
formModel.value = {
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
severity_id: '',
|
||||||
|
}
|
||||||
timeRange.value = []
|
timeRange.value = []
|
||||||
searchParams.value = {}
|
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
...searchParams.value,
|
keyword: formModel.value.keyword || undefined,
|
||||||
|
status: formModel.value.status || undefined,
|
||||||
|
}
|
||||||
|
if (formModel.value.severity_id !== '' && formModel.value.severity_id != null) {
|
||||||
|
params.severity_id = formModel.value.severity_id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理时间范围
|
|
||||||
if (timeRange.value && timeRange.value.length === 2) {
|
if (timeRange.value && timeRange.value.length === 2) {
|
||||||
params.start_time = new Date(timeRange.value[0]).toISOString()
|
params.start_time = new Date(timeRange.value[0]).toISOString()
|
||||||
params.end_time = new Date(timeRange.value[1]).toISOString()
|
params.end_time = new Date(timeRange.value[1]).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fetchAlertRecords(params)
|
const result = await fetchAlertRecords(params)
|
||||||
tableData.value = result.details.data || []
|
tableData.value = result.details?.data || []
|
||||||
pagination.total = result.details.total || 0
|
pagination.total = result.details?.total || 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('加载数据失败:', error)
|
||||||
Message.error('加载数据失败')
|
Message.error('加载数据失败')
|
||||||
@@ -312,30 +270,22 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
const handlePageChange = (page: number) => {
|
||||||
const onPageChange = (page: number) => {
|
|
||||||
pagination.current = page
|
pagination.current = page
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPageSizeChange = (pageSize: number) => {
|
const handlePageSizeChange = (pageSize: number) => {
|
||||||
pagination.pageSize = pageSize
|
pagination.pageSize = pageSize
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行选择
|
const handleRowClick = (record: any, _ev: Event) => {
|
||||||
const handleSelectionChange = (rowKeys: number[]) => {
|
|
||||||
selectedRowKeys.value = rowKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
// 行点击
|
|
||||||
const handleRowClick = (record: any) => {
|
|
||||||
currentRecord.value = record
|
currentRecord.value = record
|
||||||
detailDialogVisible.value = true
|
detailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理操作
|
|
||||||
const handleAck = (record: any) => {
|
const handleAck = (record: any) => {
|
||||||
currentRecord.value = record
|
currentRecord.value = record
|
||||||
ackDialogVisible.value = true
|
ackDialogVisible.value = true
|
||||||
@@ -356,28 +306,6 @@ const handleComment = (record: any) => {
|
|||||||
commentDialogVisible.value = true
|
commentDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量确认
|
|
||||||
const handleBatchAck = async () => {
|
|
||||||
try {
|
|
||||||
const promises = selectedRowKeys.value.map((id) =>
|
|
||||||
createAlertProcess({
|
|
||||||
alert_record_id: id,
|
|
||||||
action: 'ack',
|
|
||||||
operator: getCurrentUser(),
|
|
||||||
comment: '批量确认',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
await Promise.all(promises)
|
|
||||||
Message.success(`成功确认 ${selectedRowKeys.value.length} 条告警`)
|
|
||||||
selectedRowKeys.value = []
|
|
||||||
loadData()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量确认失败:', error)
|
|
||||||
Message.error('批量确认失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更多操作
|
|
||||||
const handleMoreAction = (action: string, record: any) => {
|
const handleMoreAction = (action: string, record: any) => {
|
||||||
currentRecord.value = record
|
currentRecord.value = record
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -393,12 +321,22 @@ const handleMoreAction = (action: string, record: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 操作成功回调
|
const handleMoreSelect = (
|
||||||
|
value: string | number | Record<string, unknown> | undefined,
|
||||||
|
record: any,
|
||||||
|
) => {
|
||||||
|
handleMoreAction(String(value), record)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSuccess = () => {
|
const handleSuccess = () => {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化函数
|
const handleRefresh = () => {
|
||||||
|
loadData()
|
||||||
|
Message.success('数据已刷新')
|
||||||
|
}
|
||||||
|
|
||||||
const formatDateTime = (datetime: string) => {
|
const formatDateTime = (datetime: string) => {
|
||||||
if (!datetime) return '-'
|
if (!datetime) return '-'
|
||||||
return new Date(datetime).toLocaleString('zh-CN')
|
return new Date(datetime).toLocaleString('zh-CN')
|
||||||
@@ -424,7 +362,15 @@ const parsedLabels = (labels: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态相关
|
/** 单行展示,避免 a-space wrap + 多 tag 撑高整行 td(与其它告警列表页行高一致) */
|
||||||
|
const formatLabelsLine = (labels: string) => {
|
||||||
|
const obj = parsedLabels(labels)
|
||||||
|
if (!obj || typeof obj !== 'object') return ''
|
||||||
|
return Object.entries(obj as Record<string, unknown>)
|
||||||
|
.map(([k, v]) => `${k}: ${v == null ? '' : String(v)}`)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
firing: 'red',
|
firing: 'red',
|
||||||
@@ -466,23 +412,35 @@ const getNotifyStatusText = (status: string) => {
|
|||||||
}
|
}
|
||||||
return textMap[status] || status
|
return textMap[status] || status
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
function getCurrentUser() {
|
<script lang="ts">
|
||||||
// TODO: 从全局状态获取当前用户
|
export default {
|
||||||
return 'admin'
|
name: 'AlertTackle',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.alert-tackle-container {
|
.container {
|
||||||
padding: 20px;
|
margin-top: 20px;
|
||||||
|
|
||||||
.general-card {
|
/* 与未设置 table scroll 的告警列表页一致行高;宽表在表格外层横向滚动,避免 Arco scroll 模式改变单元格高度 */
|
||||||
padding: 20px;
|
:deep(.search-table-container .data-table) {
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
/* 标签列单行省略,与其它页纯文本单元格行高一致 */
|
||||||
padding: 16px 0;
|
.cell-ellipsis {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与其它告警 SearchTable 页一致:正文单元格垂直居中 */
|
||||||
|
:deep(.arco-table-size-medium .arco-table-td) {
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
378
src/views/ops/pages/system-settings/license-center/index.vue
Normal file
378
src/views/ops/pages/system-settings/license-center/index.vue
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.licenseCenter']" />
|
||||||
|
|
||||||
|
<a-card class="license-page-card" :bordered="false">
|
||||||
|
<template #title>
|
||||||
|
<div class="card-head-title">
|
||||||
|
<div class="page-title">许可证信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="outline" :loading="loading" :disabled="loading" @click="loadLicense">
|
||||||
|
<template #icon><icon-refresh /></template>
|
||||||
|
刷新
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-spin :loading="loading" class="page-spin">
|
||||||
|
<a-empty v-if="!loading && loadError && !license" description="加载失败,请稍后重试" />
|
||||||
|
|
||||||
|
<template v-else-if="license">
|
||||||
|
<a-card class="section-card" :bordered="false">
|
||||||
|
<template #title>
|
||||||
|
<span class="section-title">基本信息</span>
|
||||||
|
</template>
|
||||||
|
<div class="kv-list">
|
||||||
|
<div v-for="row in basicRows" :key="row.key" class="basic-row">
|
||||||
|
<span class="label">{{ row.label }}</span>
|
||||||
|
<span class="value" :class="{ 'value-code': row.key === 'machine_code' }">{{ row.display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<a-card class="section-card section-card--quota" :bordered="false">
|
||||||
|
<template #title>
|
||||||
|
<span class="section-title">资源限制</span>
|
||||||
|
</template>
|
||||||
|
<p class="quota-hint">配额项为 0 时表示该维度不按许可证限制数量(由业务逻辑决定)。</p>
|
||||||
|
<div class="kv-list">
|
||||||
|
<div v-for="row in quotaRows" :key="row.key" class="quota-row">
|
||||||
|
<span class="label">{{ row.label }}</span>
|
||||||
|
<span class="value">{{ row.display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
</a-spin>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { IconRefresh } from '@arco-design/web-vue/es/icon'
|
||||||
|
import { fetchLicenseInfo, type LicenceConfig } from '@/api/ops/dcControl'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadError = ref(false)
|
||||||
|
const license = ref<LicenceConfig | null>(null)
|
||||||
|
|
||||||
|
const dash = (v: string | undefined | null) => (v != null && String(v).length > 0 ? String(v) : '—')
|
||||||
|
|
||||||
|
const formatQuota = (n: number | undefined) => {
|
||||||
|
if (n == null || Number.isNaN(Number(n))) return '—'
|
||||||
|
const v = Number(n)
|
||||||
|
return v > 0 ? String(v) : '无限制'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStr(o: Record<string, unknown>, snake: string, camel: string): string | undefined {
|
||||||
|
const v = o[snake] ?? o[camel]
|
||||||
|
return typeof v === 'string' ? v : v != null ? String(v) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNum(o: Record<string, unknown>, snake: string, camel: string): number | undefined {
|
||||||
|
const v = o[snake] ?? o[camel]
|
||||||
|
if (v == null || v === '') return undefined
|
||||||
|
const n = Number(v)
|
||||||
|
return Number.isNaN(n) ? undefined : n
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将接口对象统一为 LicenceConfig(兼容 snake_case / camelCase) */
|
||||||
|
function normalizeLicensePayload(raw: Record<string, unknown>): LicenceConfig {
|
||||||
|
return {
|
||||||
|
title: pickStr(raw, 'title', 'title'),
|
||||||
|
version: pickStr(raw, 'version', 'version'),
|
||||||
|
company_name: pickStr(raw, 'company_name', 'companyName'),
|
||||||
|
create_time: pickStr(raw, 'create_time', 'createTime'),
|
||||||
|
expire_time: pickStr(raw, 'expire_time', 'expireTime'),
|
||||||
|
machine_code: pickStr(raw, 'machine_code', 'machineCode'),
|
||||||
|
max_database: pickNum(raw, 'max_database', 'maxDatabase'),
|
||||||
|
max_middleware: pickNum(raw, 'max_middleware', 'maxMiddleware'),
|
||||||
|
max_pc: pickNum(raw, 'max_pc', 'maxPc'),
|
||||||
|
max_server: pickNum(raw, 'max_server', 'maxServer'),
|
||||||
|
max_client: pickNum(raw, 'max_client', 'maxClient'),
|
||||||
|
max_user: pickNum(raw, 'max_user', 'maxUser'),
|
||||||
|
max_role: pickNum(raw, 'max_role', 'maxRole'),
|
||||||
|
max_permission: pickNum(raw, 'max_permission', 'maxPermission'),
|
||||||
|
max_menu: pickNum(raw, 'max_menu', 'maxMenu'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLicensePayloadRaw(v: unknown): v is Record<string, unknown> {
|
||||||
|
if (v == null || typeof v !== 'object' || Array.isArray(v)) return false
|
||||||
|
const o = v as Record<string, unknown>
|
||||||
|
const strHit = (s: string, c: string) => {
|
||||||
|
const x = o[s] ?? o[c]
|
||||||
|
return typeof x === 'string' && x.length > 0
|
||||||
|
}
|
||||||
|
const numHit = (s: string, c: string) => {
|
||||||
|
const x = o[s] ?? o[c]
|
||||||
|
return typeof x === 'number' && !Number.isNaN(x)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
strHit('company_name', 'companyName') ||
|
||||||
|
strHit('machine_code', 'machineCode') ||
|
||||||
|
strHit('title', 'title') ||
|
||||||
|
strHit('version', 'version') ||
|
||||||
|
numHit('max_database', 'maxDatabase') ||
|
||||||
|
numHit('max_middleware', 'maxMiddleware') ||
|
||||||
|
numHit('max_pc', 'maxPc') ||
|
||||||
|
numHit('max_server', 'maxServer') ||
|
||||||
|
numHit('max_client', 'maxClient') ||
|
||||||
|
numHit('max_user', 'maxUser') ||
|
||||||
|
numHit('max_role', 'maxRole') ||
|
||||||
|
numHit('max_permission', 'maxPermission') ||
|
||||||
|
numHit('max_menu', 'maxMenu')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLicenseResponse(res: unknown): {
|
||||||
|
code?: number
|
||||||
|
success?: boolean
|
||||||
|
data: unknown
|
||||||
|
message?: string
|
||||||
|
} {
|
||||||
|
if (res == null || typeof res !== 'object') {
|
||||||
|
return { data: undefined, message: undefined }
|
||||||
|
}
|
||||||
|
const r = res as Record<string, unknown>
|
||||||
|
const code = typeof r.code === 'number' ? r.code : undefined
|
||||||
|
const success = typeof r.success === 'boolean' ? r.success : undefined
|
||||||
|
const message =
|
||||||
|
typeof r.message === 'string' ? r.message : typeof r.msg === 'string' ? r.msg : undefined
|
||||||
|
|
||||||
|
let data: unknown = r.data ?? r.details ?? r.result
|
||||||
|
if (data == null && isLicensePayloadRaw(r)) {
|
||||||
|
data = r
|
||||||
|
}
|
||||||
|
return { code, success, data, message }
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseIndicatesSuccess(code: number | undefined, success: boolean | undefined): boolean {
|
||||||
|
if (success === false) return false
|
||||||
|
if (success === true) return true
|
||||||
|
if (code === undefined) return true
|
||||||
|
return code === 200 || code === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const basicRows = computed(() => {
|
||||||
|
const L = license.value
|
||||||
|
if (!L) return []
|
||||||
|
return [
|
||||||
|
{ key: 'company_name', label: '公司名称', display: dash(L.company_name) },
|
||||||
|
{ key: 'title', label: '版本标题', display: dash(L.title) },
|
||||||
|
{ key: 'version', label: '版本号', display: dash(L.version) },
|
||||||
|
{ key: 'machine_code', label: '机器码', display: dash(L.machine_code) },
|
||||||
|
{ key: 'create_time', label: '创建时间', display: dash(L.create_time) },
|
||||||
|
{ key: 'expire_time', label: '过期时间', display: dash(L.expire_time) },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const quotaRows = computed(() => {
|
||||||
|
const L = license.value
|
||||||
|
if (!L) return []
|
||||||
|
return [
|
||||||
|
{ key: 'max_database', label: '数据库', display: formatQuota(L.max_database) },
|
||||||
|
{ key: 'max_middleware', label: '中间件', display: formatQuota(L.max_middleware) },
|
||||||
|
{ key: 'max_pc', label: 'PC', display: formatQuota(L.max_pc) },
|
||||||
|
{ key: 'max_server', label: '服务器', display: formatQuota(L.max_server) },
|
||||||
|
{ key: 'max_client', label: '客户端', display: formatQuota(L.max_client) },
|
||||||
|
{ key: 'max_user', label: '用户', display: formatQuota(L.max_user) },
|
||||||
|
{ key: 'max_role', label: '角色', display: formatQuota(L.max_role) },
|
||||||
|
{ key: 'max_permission', label: '权限', display: formatQuota(L.max_permission) },
|
||||||
|
{ key: 'max_menu', label: '菜单', display: formatQuota(L.max_menu) },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadLicense() {
|
||||||
|
loading.value = true
|
||||||
|
loadError.value = false
|
||||||
|
try {
|
||||||
|
const res = await fetchLicenseInfo()
|
||||||
|
const { code, success, data, message } = parseLicenseResponse(res)
|
||||||
|
const ok = responseIndicatesSuccess(code, success)
|
||||||
|
if (data != null && typeof data === 'object' && !Array.isArray(data) && isLicensePayloadRaw(data) && ok) {
|
||||||
|
license.value = normalizeLicensePayload(data as Record<string, unknown>)
|
||||||
|
loadError.value = false
|
||||||
|
} else {
|
||||||
|
loadError.value = true
|
||||||
|
Message.error(message || '获取许可证失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError.value = true
|
||||||
|
console.error('[license-center] fetchLicenseInfo', e)
|
||||||
|
Message.error('获取许可证失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLicense()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'LicenseCenter',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 与 system-settings / 报表等运维页一致 */
|
||||||
|
.container {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-page-card {
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-spin {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-card-header) {
|
||||||
|
border-bottom: 1px solid var(--color-border-2);
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding: 0;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card--quota {
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-hint {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 20px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
line-height: 1.5;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv-list {
|
||||||
|
padding: 0;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-row,
|
||||||
|
.quota-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid var(--color-border-1);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-row {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-code {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-card-header) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-row .label {
|
||||||
|
width: auto;
|
||||||
|
min-width: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||||
|
|
||||||
<!-- 使用 SearchTable 公共组件 -->
|
|
||||||
<SearchTable
|
<SearchTable
|
||||||
:form-model="formModel"
|
:form-model="formModel"
|
||||||
:form-items="formItems"
|
:form-items="formItems"
|
||||||
@@ -23,24 +22,57 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
@download="handleDownload"
|
@download="handleDownload"
|
||||||
>
|
>
|
||||||
<!-- 表格自定义列:日志级别 -->
|
|
||||||
<template #level="{ record }">
|
<template #level="{ record }">
|
||||||
<a-tag :color="getLevelColor(record.level)">
|
<a-tag :color="getLevelColor(record.level)">
|
||||||
{{ record.level }}
|
{{ record.level }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<!-- 表格自定义列:序号 -->
|
<template #index="{ rowIndex }">
|
||||||
<template #index="{ rowIndex }">
|
|
||||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 表格自定义列:操作 -->
|
|
||||||
<template #operations="{ record }">
|
<template #operations="{ record }">
|
||||||
<a-button type="text" size="small" @click="handleView(record)">
|
<a-button type="text" size="small" @click="handleView(record)">
|
||||||
查看
|
查看
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</SearchTable>
|
</SearchTable>
|
||||||
|
|
||||||
|
<a-drawer
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
:width="480"
|
||||||
|
placement="right"
|
||||||
|
:title="detailRecord ? `日志详情 #${detailRecord.id}` : '日志详情'"
|
||||||
|
:footer="false"
|
||||||
|
unmount-on-close
|
||||||
|
>
|
||||||
|
<template v-if="detailRecord">
|
||||||
|
<a-descriptions :column="1" size="large" bordered>
|
||||||
|
<a-descriptions-item label="日志级别">
|
||||||
|
<a-tag :color="getLevelColor(detailRecord.level)">
|
||||||
|
{{ detailRecord.level }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="模块">
|
||||||
|
{{ detailRecord.module }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="操作人">
|
||||||
|
{{ detailRecord.operator }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="IP 地址">
|
||||||
|
{{ detailRecord.ip }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="操作时间">
|
||||||
|
{{ detailRecord.createdAt }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="请求 ID">
|
||||||
|
{{ detailRecord.requestId }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="日志内容">
|
||||||
|
<div class="detail-content">{{ detailRecord.content }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</template>
|
||||||
|
</a-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,7 +82,6 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
import type { FormItem } from '@/components/search-form/types'
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
|
||||||
// 定义表格数据类型
|
|
||||||
interface LogRecord {
|
interface LogRecord {
|
||||||
id: number
|
id: number
|
||||||
level: string
|
level: string
|
||||||
@@ -59,32 +90,42 @@ interface LogRecord {
|
|||||||
operator: string
|
operator: string
|
||||||
ip: string
|
ip: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
/** 用于时间范围筛选(毫秒时间戳) */
|
||||||
|
timestamp: number
|
||||||
|
requestId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟数据生成
|
|
||||||
const generateMockData = (count: number): LogRecord[] => {
|
const generateMockData = (count: number): LogRecord[] => {
|
||||||
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG']
|
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG']
|
||||||
const modules = ['用户管理', '权限管理', '系统配置', '数据备份', '登录认证']
|
const modules = ['用户管理', '权限管理', '系统配置', '数据备份', '登录认证']
|
||||||
const operators = ['管理员', '张三', '李四', '系统', '定时任务']
|
const operators = ['管理员', '张三', '李四', '系统', '定时任务']
|
||||||
|
|
||||||
return Array.from({ length: count }, (_, i) => ({
|
return Array.from({ length: count }, (_, i) => {
|
||||||
id: i + 1,
|
const timestamp = Date.now() - i * 3600000
|
||||||
level: levels[i % levels.length],
|
return {
|
||||||
module: modules[i % modules.length],
|
id: i + 1,
|
||||||
content: `日志内容描述 ${i + 1}`,
|
level: levels[i % levels.length],
|
||||||
operator: operators[i % operators.length],
|
module: modules[i % modules.length],
|
||||||
ip: `192.168.${Math.floor(i / 255)}.${i % 255}`,
|
content: `日志内容描述 ${i + 1}:系统执行例行检查与状态同步。`,
|
||||||
createdAt: new Date(Date.now() - i * 3600000).toLocaleString('zh-CN'),
|
operator: operators[i % operators.length],
|
||||||
}))
|
ip: `192.168.${Math.floor(i / 255) % 256}.${i % 256}`,
|
||||||
|
createdAt: new Date(timestamp).toLocaleString('zh-CN'),
|
||||||
|
timestamp,
|
||||||
|
requestId: `req-${10000 + i}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<LogRecord[]>([])
|
const tableData = ref<LogRecord[]>([])
|
||||||
|
const allFilteredData = ref<LogRecord[]>([])
|
||||||
|
|
||||||
const formModel = ref({
|
const formModel = ref({
|
||||||
level: '',
|
level: '',
|
||||||
module: '',
|
module: '',
|
||||||
operator: '',
|
operator: '',
|
||||||
|
keyword: '',
|
||||||
|
dateRange: [] as unknown[],
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@@ -93,7 +134,9 @@ const pagination = reactive({
|
|||||||
total: 0,
|
total: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单项配置
|
const detailVisible = ref(false)
|
||||||
|
const detailRecord = ref<LogRecord | null>(null)
|
||||||
|
|
||||||
const formItems = computed<FormItem[]>(() => [
|
const formItems = computed<FormItem[]>(() => [
|
||||||
{
|
{
|
||||||
field: 'level',
|
field: 'level',
|
||||||
@@ -126,9 +169,21 @@ const formItems = computed<FormItem[]>(() => [
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入操作人',
|
placeholder: '请输入操作人',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '搜索日志内容',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'dateRange',
|
||||||
|
label: '时间范围',
|
||||||
|
type: 'dateRange',
|
||||||
|
placeholder: '选择时间范围',
|
||||||
|
span: 16,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 表格列配置
|
|
||||||
const columns = computed<TableColumnData[]>(() => [
|
const columns = computed<TableColumnData[]>(() => [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
@@ -177,7 +232,6 @@ const columns = computed<TableColumnData[]>(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 获取日志级别颜色
|
|
||||||
const getLevelColor = (level: string) => {
|
const getLevelColor = (level: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
INFO: 'arcoblue',
|
INFO: 'arcoblue',
|
||||||
@@ -188,38 +242,54 @@ const getLevelColor = (level: string) => {
|
|||||||
return colorMap[level] || 'gray'
|
return colorMap[level] || 'gray'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟异步获取数据
|
function applyFilters(source: LogRecord[]): LogRecord[] {
|
||||||
const fetchData = async () => {
|
let data = source
|
||||||
loading.value = true
|
const f = formModel.value
|
||||||
|
|
||||||
// 模拟网络延迟
|
if (f.level) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
data = data.filter(item => item.level === f.level)
|
||||||
|
|
||||||
let data = generateMockData(100)
|
|
||||||
|
|
||||||
// 根据搜索条件过滤
|
|
||||||
if (formModel.value.level) {
|
|
||||||
data = data.filter(item => item.level === formModel.value.level)
|
|
||||||
}
|
}
|
||||||
if (formModel.value.module) {
|
if (f.module) {
|
||||||
data = data.filter(item => item.module === formModel.value.module)
|
data = data.filter(item => item.module === f.module)
|
||||||
}
|
}
|
||||||
if (formModel.value.operator) {
|
if (f.operator) {
|
||||||
data = data.filter(item => item.operator.includes(formModel.value.operator))
|
data = data.filter(item => item.operator.includes(f.operator))
|
||||||
|
}
|
||||||
|
if (f.keyword?.trim()) {
|
||||||
|
const kw = f.keyword.trim()
|
||||||
|
data = data.filter(item => item.content.includes(kw))
|
||||||
|
}
|
||||||
|
if (f.dateRange && f.dateRange.length === 2) {
|
||||||
|
const [start, end] = f.dateRange
|
||||||
|
const startMs = new Date(start as string | Date).getTime()
|
||||||
|
const endMs = new Date(end as string | Date).getTime()
|
||||||
|
if (!Number.isNaN(startMs) && !Number.isNaN(endMs)) {
|
||||||
|
data = data.filter(item => item.timestamp >= startMs && item.timestamp <= endMs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新分页
|
return data
|
||||||
pagination.total = data.length
|
}
|
||||||
|
|
||||||
// 分页截取
|
function slicePage(data: LogRecord[]) {
|
||||||
const start = (pagination.current - 1) * pagination.pageSize
|
const start = (pagination.current - 1) * pagination.pageSize
|
||||||
const end = start + pagination.pageSize
|
const end = start + pagination.pageSize
|
||||||
tableData.value = data.slice(start, end)
|
tableData.value = data.slice(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400))
|
||||||
|
|
||||||
|
const base = generateMockData(100)
|
||||||
|
const filtered = applyFilters(base)
|
||||||
|
allFilteredData.value = filtered
|
||||||
|
pagination.total = filtered.length
|
||||||
|
slicePage(filtered)
|
||||||
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -230,6 +300,8 @@ const handleReset = () => {
|
|||||||
level: '',
|
level: '',
|
||||||
module: '',
|
module: '',
|
||||||
operator: '',
|
operator: '',
|
||||||
|
keyword: '',
|
||||||
|
dateRange: [],
|
||||||
}
|
}
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -237,7 +309,7 @@ const handleReset = () => {
|
|||||||
|
|
||||||
const handlePageChange = (current: number) => {
|
const handlePageChange = (current: number) => {
|
||||||
pagination.current = current
|
pagination.current = current
|
||||||
fetchData()
|
slicePage(allFilteredData.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
@@ -250,10 +322,10 @@ const handleDownload = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleView = (record: LogRecord) => {
|
const handleView = (record: LogRecord) => {
|
||||||
Message.info(`查看日志详情:${record.id}`)
|
detailRecord.value = record
|
||||||
|
detailVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
|
||||||
fetchData()
|
fetchData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -267,4 +339,11 @@ export default {
|
|||||||
.container {
|
.container {
|
||||||
padding: 0 20px 20px 20px;
|
padding: 0 20px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user