📝 工作台前端.
This commit is contained in:
26
orion-visor-ui/src/api/statistics/asset-statistics.ts
Normal file
26
orion-visor-ui/src/api/statistics/asset-statistics.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { LineSingleChartData } from '@/types/global';
|
||||||
|
import type { TerminalConnectLogQueryResponse } from '@/api/asset/terminal-connect-log';
|
||||||
|
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资产模块工作台响应
|
||||||
|
*/
|
||||||
|
export interface AssetWorkplaceStatisticsResponse {
|
||||||
|
execJobCount: number;
|
||||||
|
todayTerminalConnectCount: number;
|
||||||
|
todayExecCommandCount: number;
|
||||||
|
weekTerminalConnectCount: number;
|
||||||
|
weekExecCommandCount: number;
|
||||||
|
execCommandChart: LineSingleChartData;
|
||||||
|
terminalConnectChart: LineSingleChartData;
|
||||||
|
terminalConnectList: Array<TerminalConnectLogQueryResponse>;
|
||||||
|
execLogList: Array<ExecLogQueryResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询资产模块工作台统计信息
|
||||||
|
*/
|
||||||
|
export function getAssetWorkplaceStatisticsData() {
|
||||||
|
return axios.get<AssetWorkplaceStatisticsResponse>('/asset/statistics/get-workplace');
|
||||||
|
}
|
||||||
24
orion-visor-ui/src/api/statistics/infra-statistics.ts
Normal file
24
orion-visor-ui/src/api/statistics/infra-statistics.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { LineSingleChartData } from '@/types/global';
|
||||||
|
import type { LoginHistoryQueryResponse } from '@/api/user/user';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基建模块工作台响应
|
||||||
|
*/
|
||||||
|
export interface InfraWorkplaceStatisticsResponse {
|
||||||
|
userId: number;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
unreadMessageCount: number;
|
||||||
|
lastLoginTime: number;
|
||||||
|
userSessionCount: number;
|
||||||
|
operatorChart: LineSingleChartData;
|
||||||
|
loginHistoryList: Array<LoginHistoryQueryResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询基建模块工作台统计信息
|
||||||
|
*/
|
||||||
|
export function getInfraWorkplaceStatisticsData() {
|
||||||
|
return axios.get<InfraWorkplaceStatisticsResponse>('/infra/statistics/get-workplace');
|
||||||
|
}
|
||||||
@@ -1,25 +1,34 @@
|
|||||||
import type { EChartsOption } from 'echarts';
|
import type { EChartsOption } from 'echarts';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useAppStore } from '@/store';
|
import useThemes from '@/hooks/themes';
|
||||||
|
|
||||||
// for code hints
|
// 配置生成
|
||||||
// import { SeriesOption } from 'echarts';
|
interface OptionsFn {
|
||||||
// Because there are so many configuration items, this provides a relatively convenient code hint.
|
(isDark: boolean, themeTextColor: string, themeLineColor: string): EChartsOption;
|
||||||
// When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient.
|
|
||||||
interface optionsFn {
|
|
||||||
(isDark: boolean): EChartsOption;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useChartOption(sourceOption: optionsFn) {
|
// 亮色文本色
|
||||||
const appStore = useAppStore();
|
const lightTextColor = '#4E5969';
|
||||||
const isDark = computed(() => {
|
|
||||||
return appStore.theme === 'dark';
|
// 暗色文本色
|
||||||
});
|
const darkTextColor = 'rgba(255, 255, 255, 0.7)';
|
||||||
// echarts support https://echarts.apache.org/zh/theme-builder.html
|
|
||||||
// It's not used here
|
// 亮色线色
|
||||||
|
const lightLineColor = '#F2F3F5';
|
||||||
|
|
||||||
|
// 暗色线色
|
||||||
|
const darkLineColor = '#2E2E30';
|
||||||
|
|
||||||
|
export default function useChartOption(sourceOption: OptionsFn) {
|
||||||
|
const { isDark } = useThemes();
|
||||||
|
|
||||||
|
// 配置
|
||||||
const chartOption = computed<EChartsOption>(() => {
|
const chartOption = computed<EChartsOption>(() => {
|
||||||
return sourceOption(isDark.value);
|
return sourceOption(isDark.value,
|
||||||
|
isDark.value ? darkTextColor : lightTextColor,
|
||||||
|
isDark.value ? darkLineColor : lightLineColor);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chartOption,
|
chartOption,
|
||||||
};
|
};
|
||||||
|
|||||||
88
orion-visor-ui/src/types/chart.ts
Normal file
88
orion-visor-ui/src/types/chart.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type { LineSeriesOption } from 'echarts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 折线图系列定义
|
||||||
|
*/
|
||||||
|
export interface LineSeriesColor {
|
||||||
|
lineColor: string;
|
||||||
|
itemBorderColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 折线图系列常量
|
||||||
|
*/
|
||||||
|
export const LineSeriesColors: Record<string, LineSeriesColor> = {
|
||||||
|
BLUE: {
|
||||||
|
lineColor: '#4263EB',
|
||||||
|
itemBorderColor: '#DBE4FF',
|
||||||
|
},
|
||||||
|
CYAN: {
|
||||||
|
lineColor: '#1098AD',
|
||||||
|
itemBorderColor: '#C5F6FA',
|
||||||
|
},
|
||||||
|
GREEN: {
|
||||||
|
lineColor: '#37B24D',
|
||||||
|
itemBorderColor: '#D3F9D8',
|
||||||
|
},
|
||||||
|
PURPLE: {
|
||||||
|
lineColor: '#AE3EC9',
|
||||||
|
itemBorderColor: '#F3D9FA',
|
||||||
|
},
|
||||||
|
ORANGE: {
|
||||||
|
lineColor: '#F76707',
|
||||||
|
itemBorderColor: '#FFF3BF',
|
||||||
|
},
|
||||||
|
VIOLET: {
|
||||||
|
lineColor: '#7048E8',
|
||||||
|
itemBorderColor: '#E5DBFF',
|
||||||
|
},
|
||||||
|
YELLOW: {
|
||||||
|
lineColor: '#F59F00',
|
||||||
|
itemBorderColor: '#FFF3BF',
|
||||||
|
},
|
||||||
|
TEAL: {
|
||||||
|
lineColor: '#0CA678',
|
||||||
|
itemBorderColor: '#C3FAE8',
|
||||||
|
},
|
||||||
|
RED: {
|
||||||
|
lineColor: '#F03E3E',
|
||||||
|
itemBorderColor: '#FFE3E3',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成折线图系列
|
||||||
|
*/
|
||||||
|
export const createLineSeries = (name: string,
|
||||||
|
lineColor: string,
|
||||||
|
itemBorderColor: string,
|
||||||
|
data: number[]): LineSeriesOption => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: lineColor,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
itemStyle: {
|
||||||
|
color: lineColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: itemBorderColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: lineColor,
|
||||||
|
},
|
||||||
|
showSymbol: data.length === 1,
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.1,
|
||||||
|
color: lineColor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -46,3 +46,8 @@ export interface DataGrid<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TimeRanger = [string, string];
|
export type TimeRanger = [string, string];
|
||||||
|
|
||||||
|
export interface LineSingleChartData {
|
||||||
|
x: string[];
|
||||||
|
data: Array<number>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,11 +211,12 @@
|
|||||||
import { deleteTerminalConnectLog, getTerminalConnectLogPage, hostForceOffline } from '@/api/asset/terminal-connect-log';
|
import { deleteTerminalConnectLog, getTerminalConnectLogPage, hostForceOffline } from '@/api/asset/terminal-connect-log';
|
||||||
import { connectStatusKey, connectTypeKey, TerminalConnectStatus } from '../types/const';
|
import { connectStatusKey, connectTypeKey, TerminalConnectStatus } from '../types/const';
|
||||||
import { useTablePagination, useRowSelection } from '@/hooks/table';
|
import { useTablePagination, useRowSelection } from '@/hooks/table';
|
||||||
import { useDictStore } from '@/store';
|
import { useDictStore, useUserStore } from '@/store';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import columns from '../types/table.columns';
|
import columns from '../types/table.columns';
|
||||||
import useLoading from '@/hooks/loading';
|
import useLoading from '@/hooks/loading';
|
||||||
import { copy } from '@/hooks/copy';
|
import { copy } from '@/hooks/copy';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { dateFormat } from '@/utils';
|
import { dateFormat } from '@/utils';
|
||||||
import { openNewRoute } from '@/router';
|
import { openNewRoute } from '@/router';
|
||||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
|
|
||||||
const emits = defineEmits(['openClear', 'openDetail']);
|
const emits = defineEmits(['openClear', 'openDetail']);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const pagination = useTablePagination();
|
const pagination = useTablePagination();
|
||||||
const rowSelection = useRowSelection();
|
const rowSelection = useRowSelection();
|
||||||
const { loading, setLoading } = useLoading();
|
const { loading, setLoading } = useLoading();
|
||||||
@@ -316,6 +318,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 当前用户
|
||||||
|
const action = route.query.action as string;
|
||||||
|
if (action === 'self') {
|
||||||
|
formModel.userId = useUserStore().id;
|
||||||
|
}
|
||||||
|
// id
|
||||||
|
const id = route.query.id as string;
|
||||||
|
if (id) {
|
||||||
|
formModel.id = Number.parseInt(id);
|
||||||
|
}
|
||||||
fetchTableData();
|
fetchTableData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
<!-- SSH -->
|
<!-- SSH -->
|
||||||
<a-doption v-if="record.type === HostType.SSH.value"
|
<a-doption v-if="record.type === HostType.SSH.value"
|
||||||
v-permission="['terminal:terminal:access']"
|
v-permission="['asset:terminal:access']"
|
||||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
||||||
<span class="more-doption normal">
|
<span class="more-doption normal">
|
||||||
<icon-thunderbolt /> SSH
|
<icon-thunderbolt /> SSH
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
<!-- SFTP -->
|
<!-- SFTP -->
|
||||||
<a-doption v-if="record.type === HostType.SSH.value"
|
<a-doption v-if="record.type === HostType.SSH.value"
|
||||||
v-permission="['terminal:terminal:access']"
|
v-permission="['asset:terminal:access']"
|
||||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
|
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
|
||||||
<span class="more-doption normal">
|
<span class="more-doption normal">
|
||||||
<icon-folder /> SFTP
|
<icon-folder /> SFTP
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
<!-- SSH -->
|
<!-- SSH -->
|
||||||
<a-doption v-if="record.type === HostType.SSH.value"
|
<a-doption v-if="record.type === HostType.SSH.value"
|
||||||
v-permission="['terminal:terminal:access']"
|
v-permission="['asset:terminal:access']"
|
||||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
||||||
<span class="more-doption normal">
|
<span class="more-doption normal">
|
||||||
SSH
|
SSH
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
<!-- SFTP -->
|
<!-- SFTP -->
|
||||||
<a-doption v-if="record.type === HostType.SSH.value"
|
<a-doption v-if="record.type === HostType.SSH.value"
|
||||||
v-permission="['terminal:terminal:access']"
|
v-permission="['asset:terminal:access']"
|
||||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
|
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
|
||||||
<span class="more-doption normal">
|
<span class="more-doption normal">
|
||||||
SFTP
|
SFTP
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-col class="banner">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-typography-title :heading="5" style="margin-top: 0">
|
|
||||||
欢迎回来! {{ userInfo.name }}
|
|
||||||
</a-typography-title>
|
|
||||||
</a-col>
|
|
||||||
<a-divider class="panel-border" />
|
|
||||||
</a-col>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useUserStore } from '@/store';
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const userInfo = computed(() => {
|
|
||||||
return {
|
|
||||||
name: userStore.nickname,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.banner {
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
background-color: var(--color-bg-2);
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.arco-icon-home) {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<a-col :span="8">
|
||||||
|
<div class="card full">
|
||||||
|
<div class="card-title">
|
||||||
|
<p class="card-title-left">最近批量执行记录</p>
|
||||||
|
<!-- 跳转 -->
|
||||||
|
<span class="pointer span-blue"
|
||||||
|
title="详情"
|
||||||
|
@click="$router.push({ name: 'execCommandLog', query: { action: 'self' } })">
|
||||||
|
详情
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<a-table row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="batchExecColumns"
|
||||||
|
:data="data.asset?.execLogList || []"
|
||||||
|
:pagination="false"
|
||||||
|
:bordered="false"
|
||||||
|
:scroll="{ y: 258 }">
|
||||||
|
<!-- 执行状态 -->
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-tag :color="getDictValue(execHostStatusKey, record.status, 'color')">
|
||||||
|
{{ getDictValue(execHostStatusKey, record.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template #handle="{ record }">
|
||||||
|
<div class="table-handle-wrapper">
|
||||||
|
<!-- 日志 -->
|
||||||
|
<a-button v-permission="['asset:exec-command:exec']"
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
@click="() => $router.push({ name: 'execCommand', query: { id: record.id } })">
|
||||||
|
日志
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'batchExecTable'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||||
|
import { batchExecColumns } from '../types/table.columns';
|
||||||
|
import { useDictStore } from '@/store';
|
||||||
|
import { execHostStatusKey } from '@/components/exec/log/const';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
loading: boolean;
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { getDictValue } = useDictStore();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a-card class="general-card"
|
|
||||||
title="帮助文档"
|
|
||||||
:header-style="{ paddingBottom: '4px' }"
|
|
||||||
:body-style="{ padding: '0px 20px 8px 20px' }">
|
|
||||||
<a-row>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://github.com/dromara/orion-visor">github</a-link>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://gitee.com/dromara/orion-visor">gitee</a-link>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://github.com/dromara/orion-visor/blob/main/LICENSE">License</a-link>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://github.com/dromara/orion-visor/issues">上报 bug</a-link>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://visor.orionsec.cn/operator/asset.html">操作手册</a-link>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-link target="_blank" href="https://visor.orionsec.cn/update/change-log.html">更新日志</a-link>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.arco-card-body .arco-link {
|
|
||||||
margin: 10px 0;
|
|
||||||
color: rgb(var(--gray-8));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<a-col :span="16">
|
||||||
|
<div class="card full">
|
||||||
|
<div class="card-title">
|
||||||
|
<p class="card-title-left">系统操作数量 (7日)</p>
|
||||||
|
<!-- 跳转 -->
|
||||||
|
<span class="pointer span-blue"
|
||||||
|
title="详情"
|
||||||
|
@click="$router.push({ name: 'userInfo', query: { tab: 'operatorLog' } })">
|
||||||
|
详情
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 图表 -->
|
||||||
|
<div class="card-body">
|
||||||
|
<chart height="440px" :options="chartOption" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'operator-log-chart'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||||
|
import { createLineSeries, LineSeriesColors } from '@/types/chart';
|
||||||
|
import useChartOption from '@/hooks/chart-option';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 数量图表配置
|
||||||
|
const { chartOption } = useChartOption((dark, themeTextColor, themeLineColor) => {
|
||||||
|
return {
|
||||||
|
grid: {
|
||||||
|
left: '50',
|
||||||
|
right: '36',
|
||||||
|
top: '12',
|
||||||
|
bottom: '32',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
offset: 2,
|
||||||
|
data: props.data.infra?.operatorChart.x || [],
|
||||||
|
boundaryGap: false,
|
||||||
|
axisLabel: {
|
||||||
|
color: themeTextColor,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisPointer: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#23ADFF',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: themeTextColor,
|
||||||
|
formatter: (value: number) => {
|
||||||
|
return Math.floor(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: themeLineColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
createLineSeries('操作数量', LineSeriesColors.BLUE.lineColor, LineSeriesColors.BLUE.itemBorderColor, props.data.infra?.operatorChart.data || []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-card class="general-card"
|
<a-col :span="4">
|
||||||
title="快捷操作"
|
<div class="card full">
|
||||||
:body-style="{ padding: '0 20px' }">
|
<div class="card-title">
|
||||||
<a-row :gutter="8">
|
<p class="card-title-left">快捷操作</p>
|
||||||
<a-col v-for="link in links"
|
</div>
|
||||||
:key="link.meta.locale as string"
|
<a-row :gutter="[12, 18]" class="pb12">
|
||||||
:span="8"
|
<a-col v-for="{ locale, icon, name, newWindow} in links"
|
||||||
class="wrapper"
|
:key="locale as string"
|
||||||
@click="openRoute($event, link)">
|
:span="8"
|
||||||
<div class="icon">
|
:title="locale"
|
||||||
<component v-if="link.meta.icon" :is="link.meta.icon" />
|
class="wrapper"
|
||||||
</div>
|
@click="openRoute($event, name, newWindow)">
|
||||||
<a-typography-paragraph class="text">
|
<div class="icon">
|
||||||
{{ link.meta.locale }}
|
<component v-if="icon" :is="icon" />
|
||||||
</a-typography-paragraph>
|
</div>
|
||||||
</a-col>
|
<div class="text usn">
|
||||||
</a-row>
|
<span>{{ locale }}</span>
|
||||||
</a-card>
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -34,24 +38,80 @@
|
|||||||
.filter(s => s.meta)
|
.filter(s => s.meta)
|
||||||
.map(s => s as RouteRecordNormalized)
|
.map(s => s as RouteRecordNormalized)
|
||||||
.filter(s => s.meta.hideInMenu !== true)
|
.filter(s => s.meta.hideInMenu !== true)
|
||||||
.slice(0, 15);
|
.map(s => {
|
||||||
|
return {
|
||||||
|
name: s.name,
|
||||||
|
locale: s.meta.locale,
|
||||||
|
icon: s.meta.icon,
|
||||||
|
newWindow: s.meta.newWindow,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.slice(0, 10);
|
||||||
|
links.unshift({
|
||||||
|
locale: '个人中心', icon: 'icon-user', name: 'userInfo', newWindow: false
|
||||||
|
}, {
|
||||||
|
locale: '修改密码', icon: 'icon-safe', name: 'updatePassword', newWindow: false
|
||||||
|
});
|
||||||
|
|
||||||
// 打开路由
|
// 打开路由
|
||||||
const openRoute = (e: any, route: RouteRecordNormalized) => {
|
const openRoute = (e: any, name: any, newWindow: any) => {
|
||||||
// 新页面打开
|
// 新页面打开
|
||||||
if (route.meta.newWindow || e.ctrlKey) {
|
if (newWindow || e.ctrlKey) {
|
||||||
openNewRoute({
|
openNewRoute({
|
||||||
name: route.name as string,
|
name: name as string,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 触发跳转
|
// 触发跳转
|
||||||
router.push({
|
router.push({
|
||||||
name: route.name as string,
|
name: name as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.text {
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgb(var(--gray-8));
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: rgb(var(--dark-gray-1));
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgb(var(--gray-1));
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.icon {
|
||||||
|
color: rgb(var(--arcoblue-6));
|
||||||
|
background-color: #E8F3FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: rgb(var(--arcoblue-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="card full">
|
||||||
|
<div class="card-title">
|
||||||
|
<p class="card-title-left">最近终端连接记录</p>
|
||||||
|
<!-- 跳转 -->
|
||||||
|
<span class="pointer span-blue"
|
||||||
|
title="详情"
|
||||||
|
@click="$router.push({ name: 'connectLog', query: { action: 'self' } })">
|
||||||
|
详情
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<a-table row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="terminalLogColumns"
|
||||||
|
:data="data.asset?.terminalConnectList || []"
|
||||||
|
:pagination="false"
|
||||||
|
:bordered="false"
|
||||||
|
:scroll="{ y: 258 }">
|
||||||
|
<!-- 连接主机 -->
|
||||||
|
<template #hostName="{ record }">
|
||||||
|
<span class="table-cell-value" :title="record.hostName">
|
||||||
|
{{ record.hostName }}
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<span class="table-cell-sub-value text-copy"
|
||||||
|
:title="record.hostAddress"
|
||||||
|
@click="copy(record.hostAddress)">
|
||||||
|
{{ record.hostAddress }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- 类型 -->
|
||||||
|
<template #type="{ record }">
|
||||||
|
<a-tag :color="getDictValue(terminalConnectTypeKey, record.type, 'color')">
|
||||||
|
{{ getDictValue(terminalConnectTypeKey, record.type) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template #handle="{ record }">
|
||||||
|
<div class="table-handle-wrapper">
|
||||||
|
<!-- 连接 SSH -->
|
||||||
|
<a-button v-permission="['asset:terminal:access']"
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
@click="openNewRoute({ name: 'terminal', query: { connect: record.hostId, type: record.type } })">
|
||||||
|
连接
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'terminalConnectTable'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '../types/const';
|
||||||
|
import { copy } from '@/hooks/copy';
|
||||||
|
import { terminalLogColumns } from '../types/table.columns';
|
||||||
|
import { terminalConnectTypeKey } from '../types/const';
|
||||||
|
import { useDictStore } from '@/store';
|
||||||
|
import { openNewRoute } from '@/router';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
loading: boolean;
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { getDictValue } = useDictStore();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<a-col :span="8">
|
||||||
|
<div class="card full">
|
||||||
|
<div class="card-title">
|
||||||
|
<p class="card-title-left">用户登录日志</p>
|
||||||
|
<!-- 跳转 -->
|
||||||
|
<span class="pointer span-blue"
|
||||||
|
title="详情"
|
||||||
|
@click="$router.push({ name: 'userInfo', query: { tab: 'loginHistory' } })">
|
||||||
|
详情
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<a-table row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="userLoginColumns"
|
||||||
|
:data="data.infra?.loginHistoryList || []"
|
||||||
|
:pagination="false"
|
||||||
|
:bordered="false"
|
||||||
|
:scroll="{ y: 388 }">
|
||||||
|
<!-- 登录设备 -->
|
||||||
|
<template #content="{ record }">
|
||||||
|
<span>{{ record.address }} - {{ record.location }} - {{ record.userAgent }}</span>
|
||||||
|
</template>
|
||||||
|
<!-- 登录结果 -->
|
||||||
|
<template #result="{ record }">
|
||||||
|
<a-tag :color="getDictValue(operatorLogResultKey, record.result, 'color')">
|
||||||
|
{{ getDictValue(operatorLogResultKey, record.result) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'userLoginTable'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||||
|
import { userLoginColumns } from '../types/table.columns';
|
||||||
|
import { operatorLogResultKey } from '../types/const';
|
||||||
|
import { useDictStore } from '@/store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
loading: boolean;
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { getDictValue } = useDictStore();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<a-row class="card">
|
||||||
|
<!-- 登录用户 -->
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-typography-title :heading="5" style="padding: 4px 0; margin: 0;">
|
||||||
|
欢迎回来! {{ data.infra?.nickname }}
|
||||||
|
</a-typography-title>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="16" class="header-right flex-center flex-content-end">
|
||||||
|
<!-- 未读消息数-->
|
||||||
|
<a-tag v-if="data.infra?.unreadMessageCount"
|
||||||
|
class="pointer"
|
||||||
|
title="查看消息"
|
||||||
|
color="arcoblue"
|
||||||
|
@click="setVisibleMessageBox">
|
||||||
|
<template #icon>
|
||||||
|
<icon-message />
|
||||||
|
</template>
|
||||||
|
您有 {{ data.infra.unreadMessageCount }} 条未读消息
|
||||||
|
</a-tag>
|
||||||
|
<a-divider v-if="data.infra?.unreadMessageCount"
|
||||||
|
direction="vertical"
|
||||||
|
:margin="14"
|
||||||
|
style="height: 20px;" />
|
||||||
|
<!-- 上次登录时间 -->
|
||||||
|
<div v-if="data.infra?.lastLoginTime"
|
||||||
|
class="last-login-wrapper">
|
||||||
|
<span class="last-login-label usn">上次登录时间</span>
|
||||||
|
<a-tag color="purple">
|
||||||
|
<template #icon>
|
||||||
|
<icon-schedule />
|
||||||
|
</template>
|
||||||
|
{{ dateFormat(new Date(data.infra.lastLoginTime)) }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'workplaceHeader'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||||
|
import { dateFormat } from '@/utils';
|
||||||
|
import { openMessageBox } from '@/types/symbol';
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 注入打开消息盒子
|
||||||
|
const setVisibleMessageBox = inject(openMessageBox) as () => void;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.header-right {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-login-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-login-label {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<a-col v-for="item in summaryItems"
|
||||||
|
:span="4"
|
||||||
|
class="statistics-card">
|
||||||
|
<div class="card pointer"
|
||||||
|
title="双击查看详情"
|
||||||
|
@dblclick="item.go">
|
||||||
|
<a-statistic :title="item.title"
|
||||||
|
:value="item.value"
|
||||||
|
:value-from="0"
|
||||||
|
:animation-duration="1000"
|
||||||
|
animation
|
||||||
|
show-group-separator>
|
||||||
|
<template #prefix>
|
||||||
|
<span class="statistic-prefix"
|
||||||
|
:style="{ background: item.prefix.background }">
|
||||||
|
<component :is="item.prefix.icon"
|
||||||
|
:style="{ color: item.prefix.iconColor }" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</a-statistic>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<!-- 连接终端次数图表 -->
|
||||||
|
<a-col :span="4" class="statistics-card">
|
||||||
|
<div class="card" :style="{
|
||||||
|
background: isDark
|
||||||
|
? 'linear-gradient(180deg, #284991 0%, #122B62 100%)'
|
||||||
|
: 'linear-gradient(180deg, #CAE6FA 0%, #CAE6FA 100%)',
|
||||||
|
}">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="statistics-wrapper">
|
||||||
|
<a-statistic title="连接终端次数 (7日)"
|
||||||
|
:value="data.asset?.weekTerminalConnectCount || 0"
|
||||||
|
:value-from="0"
|
||||||
|
:animation-duration="1000"
|
||||||
|
animation
|
||||||
|
show-group-separator />
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<chart width="100%" height="64px" :option="terminalConnectChart" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<!-- 批量执行次数图表 -->
|
||||||
|
<a-col :span="4" class="statistics-card">
|
||||||
|
<div class="card" :style="{
|
||||||
|
background: isDark
|
||||||
|
? 'linear-gradient(180deg, #424f32 0%, #424f32 100%)'
|
||||||
|
: 'linear-gradient(180deg, #BCF5CF 0%, #BCF5CF 100%)',
|
||||||
|
}">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="statistics-wrapper">
|
||||||
|
<a-statistic title="批量执行次数 (7日)"
|
||||||
|
:value="data.asset?.weekExecCommandCount || 0"
|
||||||
|
:value-from="0"
|
||||||
|
:animation-duration="1000"
|
||||||
|
animation
|
||||||
|
show-group-separator />
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<chart width="100%" height="64px" :option="execCommandChart" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'workplaceStatistics'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from '@/views/dashboard/workplace/types/const';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import useThemes from '@/hooks/themes';
|
||||||
|
import useChartOption from '@/hooks/chart-option';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: WorkplaceStatisticsData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { isDark } = useThemes();
|
||||||
|
|
||||||
|
const summaryItems = computed(() => [
|
||||||
|
{
|
||||||
|
title: '今日连接终端次数',
|
||||||
|
value: props.data.asset?.todayTerminalConnectCount || 0,
|
||||||
|
prefix: {
|
||||||
|
icon: 'icon-history',
|
||||||
|
background: isDark.value ? '#354276' : '#E8F3FF',
|
||||||
|
iconColor: isDark.value ? '#4A7FF7' : '#165DFF',
|
||||||
|
},
|
||||||
|
go: () => router.push({ name: 'connectLog', query: { action: 'self' } })
|
||||||
|
}, {
|
||||||
|
title: '今日批量执行次数',
|
||||||
|
value: props.data.asset?.todayExecCommandCount || 0,
|
||||||
|
prefix: {
|
||||||
|
icon: 'icon-code-block',
|
||||||
|
background: isDark.value ? '#3F385E' : '#F5E8FF',
|
||||||
|
iconColor: isDark.value ? '#8558D3' : '#722ED1',
|
||||||
|
},
|
||||||
|
go: () => router.push({ name: 'execCommandLog', query: { action: 'self' } })
|
||||||
|
}, {
|
||||||
|
|
||||||
|
title: '当前登录设备数量',
|
||||||
|
value: props.data.infra?.userSessionCount || 0,
|
||||||
|
prefix: {
|
||||||
|
icon: 'icon-desktop',
|
||||||
|
background: isDark.value ? '#3D5A62' : '#D6FFF8',
|
||||||
|
iconColor: isDark.value ? '#6ED1CE' : '#33D1C9',
|
||||||
|
},
|
||||||
|
go: () => router.push({ name: 'userInfo', query: { tab: 'userSession' } })
|
||||||
|
|
||||||
|
}, {
|
||||||
|
title: '管理的任务数量',
|
||||||
|
value: props.data.asset?.execJobCount || 0,
|
||||||
|
prefix: {
|
||||||
|
icon: 'icon-calendar-clock',
|
||||||
|
background: isDark.value ? '#3F385E' : '#F5E8FF',
|
||||||
|
iconColor: isDark.value ? '#8558D3' : '#722ED1',
|
||||||
|
},
|
||||||
|
go: () => router.push({ name: 'execJob', query: { action: 'self' } })
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { chartOption: terminalConnectChart } = useChartOption(() => {
|
||||||
|
return {
|
||||||
|
grid: {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: (p: any) => {
|
||||||
|
return p[0].data.x + ' ' + p[0].data.value + ' 次';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
name: '连接次数',
|
||||||
|
data: !props.data.asset?.terminalConnectChart
|
||||||
|
? []
|
||||||
|
: props.data.asset?.terminalConnectChart.data.map((s, index) => {
|
||||||
|
const x = props.data.asset?.terminalConnectChart.x;
|
||||||
|
return {
|
||||||
|
x: x[index],
|
||||||
|
value: s,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
type: 'line',
|
||||||
|
showSymbol: false,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#165DFF',
|
||||||
|
width: 3,
|
||||||
|
type: 'dashed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { chartOption: execCommandChart } = useChartOption(() => {
|
||||||
|
return {
|
||||||
|
grid: {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: (p: any) => {
|
||||||
|
return p[0].data.x + ' ' + p[0].data.value + '次';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
name: '执行次数',
|
||||||
|
data: !props.data.asset?.execCommandChart
|
||||||
|
? []
|
||||||
|
: props.data.asset?.execCommandChart.data.map((s, index) => {
|
||||||
|
const x = props.data.asset?.execCommandChart.x;
|
||||||
|
return {
|
||||||
|
x: x[index],
|
||||||
|
value: s,
|
||||||
|
itemStyle: { color: '#2CAB40' },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: 7,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@card-height: 104px;
|
||||||
|
|
||||||
|
.statistics-card {
|
||||||
|
height: @card-height;
|
||||||
|
|
||||||
|
:deep(.arco-statistic) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.arco-statistic-title {
|
||||||
|
color: rgb(var(--gray-10));
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-statistic-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-prefix {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--color-white);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.statistics-wrapper {
|
||||||
|
width: 134px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
width: calc(100% - 134px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,127 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-container">
|
<div class="layout-container" v-if="render">
|
||||||
<!-- 顶部 -->
|
<!-- 头部 -->
|
||||||
<div class="top-side">
|
<workplace-header class="mb16"
|
||||||
<!-- 提示 -->
|
:data="statisticsData" />
|
||||||
<div class="panel">
|
<!-- 统计信息 -->
|
||||||
<banner />
|
<workplace-statistics class="mb16"
|
||||||
</div>
|
:data="statisticsData" />
|
||||||
</div>
|
<a-row :gutter="16" class="mb16" align="stretch">
|
||||||
<div class="row-wrapper">
|
<!-- 最近终端连接表格 -->
|
||||||
<div class="left-side">
|
<terminal-connect-table :loading="assetLoading"
|
||||||
<!-- 操作日志 -->
|
:data="statisticsData" />
|
||||||
<a-card class="general-card"
|
<!-- 最近批量执行表格 -->
|
||||||
title="操作日志"
|
<batch-exec-table :loading="assetLoading"
|
||||||
:body-style="{ padding: '0 20px 8px 20px' }">
|
:data="statisticsData" />
|
||||||
<operator-log-simple-table :current="true"
|
<!-- 快捷操作 -->
|
||||||
:handle-column="false" />
|
<quick-operation />
|
||||||
</a-card>
|
</a-row>
|
||||||
</div>
|
<a-row :gutter="16" align="stretch">
|
||||||
<a-grid class="right-side"
|
<!-- 每日操作数量图表 -->
|
||||||
:cols="24"
|
<operator-log-chart :data="statisticsData" />
|
||||||
:row-gap="16">
|
<!-- 用户登录日志 -->
|
||||||
<!-- 快捷操作 -->
|
<user-login-table :loading="infraLoading"
|
||||||
<a-grid-item class="card-wrapper" :span="24">
|
:data="statisticsData" />
|
||||||
<quick-operation />
|
</a-row>
|
||||||
</a-grid-item>
|
|
||||||
<!-- 文档 -->
|
|
||||||
<a-grid-item class="panel" :span="24">
|
|
||||||
<docs />
|
|
||||||
</a-grid-item>
|
|
||||||
</a-grid>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import Banner from './components/banner.vue';
|
|
||||||
import QuickOperation from './components/quick-operation.vue';
|
|
||||||
import Docs from './components/docs.vue';
|
|
||||||
import OperatorLogSimpleTable from '@/views/user/operator-log/components/operator-log-simple-table.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
name: 'workplace',
|
name: 'workplace',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WorkplaceStatisticsData } from './types/const';
|
||||||
|
import { onBeforeMount, onMounted, ref } from 'vue';
|
||||||
|
import { useDictStore } from '@/store';
|
||||||
|
import { dictKeys } from './types/const';
|
||||||
|
import useLoading from '@/hooks/loading';
|
||||||
|
import { getInfraWorkplaceStatisticsData } from '@/api/statistics/infra-statistics';
|
||||||
|
import { getAssetWorkplaceStatisticsData } from '@/api/statistics/asset-statistics';
|
||||||
|
import WorkplaceHeader from './components/workplace-header.vue';
|
||||||
|
import WorkplaceStatistics from './components/workplace-statistics.vue';
|
||||||
|
import TerminalConnectTable from './components/terminal-connect-table.vue';
|
||||||
|
import BatchExecTable from './components/batch-exec-table.vue';
|
||||||
|
import QuickOperation from './components/quick-operation.vue';
|
||||||
|
import UserLoginTable from './components/user-login-table.vue';
|
||||||
|
import OperatorLogChart from './components/operator-log-chart.vue';
|
||||||
|
|
||||||
|
const { loading: infraLoading, setLoading: setInfraLoading } = useLoading();
|
||||||
|
const { loading: assetLoading, setLoading: setAssetLoading } = useLoading();
|
||||||
|
|
||||||
|
const render = ref(false);
|
||||||
|
const statisticsData = ref({} as WorkplaceStatisticsData);
|
||||||
|
|
||||||
|
const getWorkplaceData = () => {
|
||||||
|
// 基建模块
|
||||||
|
setInfraLoading(true);
|
||||||
|
getInfraWorkplaceStatisticsData().then(({ data }) => {
|
||||||
|
setInfraLoading(false);
|
||||||
|
statisticsData.value.infra = data;
|
||||||
|
}).catch(() => {
|
||||||
|
setInfraLoading(false);
|
||||||
|
});
|
||||||
|
// 资产模块
|
||||||
|
setAssetLoading(true);
|
||||||
|
getAssetWorkplaceStatisticsData().then(({ data }) => {
|
||||||
|
setAssetLoading(false);
|
||||||
|
statisticsData.value.asset = data;
|
||||||
|
}).catch(() => {
|
||||||
|
setAssetLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getWorkplaceData);
|
||||||
|
|
||||||
|
// 加载字典值
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
await dictStore.loadKeys(dictKeys);
|
||||||
|
render.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.top-side {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-wrapper {
|
:deep(.card) {
|
||||||
margin-top: 16px;
|
padding: 16px 20px;
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.left-side {
|
|
||||||
width: calc(100% - 296px);
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side {
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background-color: var(--color-bg-2);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.panel-border) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
border-bottom: 1px solid rgb(var(--gray-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-wrapper {
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--color-bg-2);
|
background-color: var(--color-bg-2);
|
||||||
|
|
||||||
:deep(.text) {
|
&-title {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
text-align: center;
|
justify-content: space-between;
|
||||||
color: rgb(var(--gray-8));
|
margin-bottom: 16px;
|
||||||
}
|
user-select: none;
|
||||||
|
|
||||||
:deep(.wrapper) {
|
&-left {
|
||||||
margin-bottom: 8px;
|
margin: 0;
|
||||||
text-align: center;
|
color: var(--color-text-1);
|
||||||
cursor: pointer;
|
font-weight: 600;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.text {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.icon {
|
|
||||||
color: rgb(var(--arcoblue-6));
|
|
||||||
background-color: #E8F3FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
color: rgb(var(--arcoblue-6));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.icon) {
|
&-body {
|
||||||
display: inline-block;
|
height: calc(100% - 36px);
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: rgb(var(--dark-gray-1));
|
|
||||||
line-height: 32px;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: rgb(var(--gray-1));
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.arco-table-empty) {
|
||||||
|
.arco-table-td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
18
orion-visor-ui/src/views/dashboard/workplace/types/const.ts
Normal file
18
orion-visor-ui/src/views/dashboard/workplace/types/const.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { InfraWorkplaceStatisticsResponse } from '@/api/statistics/infra-statistics';
|
||||||
|
import type { AssetWorkplaceStatisticsResponse } from '@/api/statistics/asset-statistics';
|
||||||
|
import { execHostStatusKey } from '@/components/exec/log/const';
|
||||||
|
|
||||||
|
// 工作台统计数据
|
||||||
|
export interface WorkplaceStatisticsData {
|
||||||
|
infra: InfraWorkplaceStatisticsResponse;
|
||||||
|
asset: AssetWorkplaceStatisticsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终端连接类型 字典项
|
||||||
|
export const terminalConnectTypeKey = 'terminalConnectType';
|
||||||
|
|
||||||
|
// 操作日志结果 字典项
|
||||||
|
export const operatorLogResultKey = 'operatorLogResult';
|
||||||
|
|
||||||
|
// 加载的字典值
|
||||||
|
export const dictKeys = [terminalConnectTypeKey, execHostStatusKey, operatorLogResultKey];
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||||
|
import { dateFormat } from '@/utils';
|
||||||
|
|
||||||
|
// 终端日志列
|
||||||
|
export const terminalLogColumns = [
|
||||||
|
{
|
||||||
|
title: '连接主机',
|
||||||
|
dataIndex: 'hostName',
|
||||||
|
slotName: 'hostName',
|
||||||
|
align: 'left',
|
||||||
|
ellipsis: true,
|
||||||
|
}, {
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
slotName: 'type',
|
||||||
|
width: 88,
|
||||||
|
align: 'left',
|
||||||
|
}, {
|
||||||
|
title: '连接时间',
|
||||||
|
dataIndex: 'startTime',
|
||||||
|
slotName: 'startTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
render: ({ record }) => {
|
||||||
|
return dateFormat(new Date(record.startTime));
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
title: '操作',
|
||||||
|
slotName: 'handle',
|
||||||
|
width: 92,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
] as TableColumnData[];
|
||||||
|
|
||||||
|
// 批量执行列
|
||||||
|
export const batchExecColumns = [
|
||||||
|
{
|
||||||
|
title: '执行描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
slotName: 'description',
|
||||||
|
align: 'left',
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
}, {
|
||||||
|
title: '执行状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
slotName: 'status',
|
||||||
|
align: 'left',
|
||||||
|
width: 108,
|
||||||
|
}, {
|
||||||
|
title: '执行时间',
|
||||||
|
dataIndex: 'startTime',
|
||||||
|
slotName: 'startTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
render: ({ record }) => {
|
||||||
|
return dateFormat(new Date(record.startTime));
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
title: '操作',
|
||||||
|
slotName: 'handle',
|
||||||
|
width: 92,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
] as TableColumnData[];
|
||||||
|
|
||||||
|
// 用户登录日志列
|
||||||
|
export const userLoginColumns = [
|
||||||
|
{
|
||||||
|
title: '登录设备',
|
||||||
|
dataIndex: 'content',
|
||||||
|
slotName: 'content',
|
||||||
|
ellipsis: true,
|
||||||
|
tooltip: true,
|
||||||
|
}, {
|
||||||
|
title: '登录结果',
|
||||||
|
dataIndex: 'result',
|
||||||
|
slotName: 'result',
|
||||||
|
align: 'center',
|
||||||
|
width: 90,
|
||||||
|
}, {
|
||||||
|
title: '登录时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
slotName: 'createTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
render: ({ record }) => {
|
||||||
|
return dateFormat(new Date(record.createTime));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as TableColumnData[];
|
||||||
@@ -217,11 +217,12 @@
|
|||||||
getExecCommandLogStatus
|
getExecCommandLogStatus
|
||||||
} from '@/api/exec/exec-command-log';
|
} from '@/api/exec/exec-command-log';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import useLoading from '@/hooks/loading';
|
import useLoading from '@/hooks/loading';
|
||||||
import { tableColumns } from '../types/table.columns';
|
import { tableColumns } from '../types/table.columns';
|
||||||
import { ExecStatus, execStatusKey } from '@/components/exec/log/const';
|
import { ExecStatus, execStatusKey } from '@/components/exec/log/const';
|
||||||
import { useExpandable, useTablePagination, useRowSelection } from '@/hooks/table';
|
import { useExpandable, useTablePagination, useRowSelection } from '@/hooks/table';
|
||||||
import { useDictStore } from '@/store';
|
import { useDictStore, useUserStore } from '@/store';
|
||||||
import { dateFormat, formatDuration } from '@/utils';
|
import { dateFormat, formatDuration } from '@/utils';
|
||||||
import { reExecCommand } from '@/api/exec/exec-command';
|
import { reExecCommand } from '@/api/exec/exec-command';
|
||||||
import { interruptExecCommand } from '@/api/exec/exec-command-log';
|
import { interruptExecCommand } from '@/api/exec/exec-command-log';
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
|
|
||||||
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
|
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const pagination = useTablePagination();
|
const pagination = useTablePagination();
|
||||||
const rowSelection = useRowSelection();
|
const rowSelection = useRowSelection();
|
||||||
const expandable = useExpandable();
|
const expandable = useExpandable();
|
||||||
@@ -406,6 +408,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 当前用户
|
||||||
|
const action = route.query.action as string;
|
||||||
|
if (action === 'self') {
|
||||||
|
formModel.userId = useUserStore().id;
|
||||||
|
}
|
||||||
// 加载数据
|
// 加载数据
|
||||||
fetchTableData();
|
fetchTableData();
|
||||||
// 注册状态轮询
|
// 注册状态轮询
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
import { ref, onBeforeMount } from 'vue';
|
import { ref, onBeforeMount } from 'vue';
|
||||||
import { useDictStore } from '@/store';
|
import { useDictStore } from '@/store';
|
||||||
import { dictKeys } from '@/components/exec/log/const';
|
import { dictKeys } from '@/components/exec/log/const';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { openNewRoute } from '@/router';
|
import { openNewRoute } from '@/router';
|
||||||
import ExecCommandLogTable from './components/exec-command-log-table.vue';
|
import ExecCommandLogTable from './components/exec-command-log-table.vue';
|
||||||
import ExecCommandLogClearModal from './components/exec-command-log-clear-modal.vue';
|
import ExecCommandLogClearModal from './components/exec-command-log-clear-modal.vue';
|
||||||
@@ -40,8 +39,6 @@
|
|||||||
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
|
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
|
||||||
import ExecLogPanelModal from '@/components/exec/log/panel-modal/index.vue';
|
import ExecLogPanelModal from '@/components/exec/log/panel-modal/index.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const render = ref(false);
|
const render = ref(false);
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
const logModal = ref();
|
const logModal = ref();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tabs-container" v-if="render">
|
<div class="tabs-container" v-if="render">
|
||||||
<a-tabs type="rounded"
|
<a-tabs v-model:active-key="activeTab"
|
||||||
|
type="rounded"
|
||||||
size="medium"
|
size="medium"
|
||||||
position="left"
|
position="left"
|
||||||
:lazy-load="true"
|
:lazy-load="true"
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
|
const activeTab = ref<string>('mineInfo');
|
||||||
const render = ref<boolean>(false);
|
const render = ref<boolean>(false);
|
||||||
const userId = ref<number>();
|
const userId = ref<number>();
|
||||||
const user = ref<UserQueryResponse>();
|
const user = ref<UserQueryResponse>();
|
||||||
@@ -72,11 +74,18 @@
|
|||||||
const clickTab = (key: string) => {
|
const clickTab = (key: string) => {
|
||||||
if (key === 'back') {
|
if (key === 'back') {
|
||||||
router.back();
|
router.back();
|
||||||
|
} else {
|
||||||
|
activeTab.value = key;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
// 设置当前面板
|
||||||
|
const tab = route.query.tab as string;
|
||||||
|
if (tab) {
|
||||||
|
activeTab.value = tab;
|
||||||
|
}
|
||||||
// 获取 userId
|
// 获取 userId
|
||||||
const queryUserId = route.query.id as string;
|
const queryUserId = route.query.id as string;
|
||||||
if (!queryUserId) {
|
if (!queryUserId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user