📝 工作台前端.
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 { computed } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import useThemes from '@/hooks/themes';
|
||||
|
||||
// for code hints
|
||||
// import { SeriesOption } from 'echarts';
|
||||
// Because there are so many configuration items, this provides a relatively convenient code hint.
|
||||
// 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;
|
||||
// 配置生成
|
||||
interface OptionsFn {
|
||||
(isDark: boolean, themeTextColor: string, themeLineColor: string): EChartsOption;
|
||||
}
|
||||
|
||||
export default function useChartOption(sourceOption: optionsFn) {
|
||||
const appStore = useAppStore();
|
||||
const isDark = computed(() => {
|
||||
return appStore.theme === 'dark';
|
||||
});
|
||||
// echarts support https://echarts.apache.org/zh/theme-builder.html
|
||||
// It's not used here
|
||||
// 亮色文本色
|
||||
const lightTextColor = '#4E5969';
|
||||
|
||||
// 暗色文本色
|
||||
const darkTextColor = 'rgba(255, 255, 255, 0.7)';
|
||||
|
||||
// 亮色线色
|
||||
const lightLineColor = '#F2F3F5';
|
||||
|
||||
// 暗色线色
|
||||
const darkLineColor = '#2E2E30';
|
||||
|
||||
export default function useChartOption(sourceOption: OptionsFn) {
|
||||
const { isDark } = useThemes();
|
||||
|
||||
// 配置
|
||||
const chartOption = computed<EChartsOption>(() => {
|
||||
return sourceOption(isDark.value);
|
||||
return sourceOption(isDark.value,
|
||||
isDark.value ? darkTextColor : lightTextColor,
|
||||
isDark.value ? darkLineColor : lightLineColor);
|
||||
});
|
||||
|
||||
return {
|
||||
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 interface LineSingleChartData {
|
||||
x: string[];
|
||||
data: Array<number>;
|
||||
}
|
||||
|
||||
@@ -211,11 +211,12 @@
|
||||
import { deleteTerminalConnectLog, getTerminalConnectLogPage, hostForceOffline } from '@/api/asset/terminal-connect-log';
|
||||
import { connectStatusKey, connectTypeKey, TerminalConnectStatus } from '../types/const';
|
||||
import { useTablePagination, useRowSelection } from '@/hooks/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { useDictStore, useUserStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import columns from '../types/table.columns';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { dateFormat } from '@/utils';
|
||||
import { openNewRoute } from '@/router';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
@@ -223,6 +224,7 @@
|
||||
|
||||
const emits = defineEmits(['openClear', 'openDetail']);
|
||||
|
||||
const route = useRoute();
|
||||
const pagination = useTablePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
@@ -316,6 +318,16 @@
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</a-doption>
|
||||
<!-- SSH -->
|
||||
<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' } })">
|
||||
<span class="more-doption normal">
|
||||
<icon-thunderbolt /> SSH
|
||||
@@ -206,7 +206,7 @@
|
||||
</a-doption>
|
||||
<!-- SFTP -->
|
||||
<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' } })">
|
||||
<span class="more-doption normal">
|
||||
<icon-folder /> SFTP
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
</a-doption>
|
||||
<!-- SSH -->
|
||||
<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' } })">
|
||||
<span class="more-doption normal">
|
||||
SSH
|
||||
@@ -242,7 +242,7 @@
|
||||
</a-doption>
|
||||
<!-- SFTP -->
|
||||
<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' } })">
|
||||
<span class="more-doption normal">
|
||||
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>
|
||||
<a-card class="general-card"
|
||||
title="快捷操作"
|
||||
:body-style="{ padding: '0 20px' }">
|
||||
<a-row :gutter="8">
|
||||
<a-col v-for="link in links"
|
||||
:key="link.meta.locale as string"
|
||||
:span="8"
|
||||
class="wrapper"
|
||||
@click="openRoute($event, link)">
|
||||
<div class="icon">
|
||||
<component v-if="link.meta.icon" :is="link.meta.icon" />
|
||||
</div>
|
||||
<a-typography-paragraph class="text">
|
||||
{{ link.meta.locale }}
|
||||
</a-typography-paragraph>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-col :span="4">
|
||||
<div class="card full">
|
||||
<div class="card-title">
|
||||
<p class="card-title-left">快捷操作</p>
|
||||
</div>
|
||||
<a-row :gutter="[12, 18]" class="pb12">
|
||||
<a-col v-for="{ locale, icon, name, newWindow} in links"
|
||||
:key="locale as string"
|
||||
:span="8"
|
||||
:title="locale"
|
||||
class="wrapper"
|
||||
@click="openRoute($event, name, newWindow)">
|
||||
<div class="icon">
|
||||
<component v-if="icon" :is="icon" />
|
||||
</div>
|
||||
<div class="text usn">
|
||||
<span>{{ locale }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -34,24 +38,80 @@
|
||||
.filter(s => s.meta)
|
||||
.map(s => s as RouteRecordNormalized)
|
||||
.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({
|
||||
name: route.name as string,
|
||||
name: name as string,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 触发跳转
|
||||
router.push({
|
||||
name: route.name as string,
|
||||
name: name as string,
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<div class="layout-container">
|
||||
<!-- 顶部 -->
|
||||
<div class="top-side">
|
||||
<!-- 提示 -->
|
||||
<div class="panel">
|
||||
<banner />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-wrapper">
|
||||
<div class="left-side">
|
||||
<!-- 操作日志 -->
|
||||
<a-card class="general-card"
|
||||
title="操作日志"
|
||||
:body-style="{ padding: '0 20px 8px 20px' }">
|
||||
<operator-log-simple-table :current="true"
|
||||
:handle-column="false" />
|
||||
</a-card>
|
||||
</div>
|
||||
<a-grid class="right-side"
|
||||
:cols="24"
|
||||
:row-gap="16">
|
||||
<!-- 快捷操作 -->
|
||||
<a-grid-item class="card-wrapper" :span="24">
|
||||
<quick-operation />
|
||||
</a-grid-item>
|
||||
<!-- 文档 -->
|
||||
<a-grid-item class="panel" :span="24">
|
||||
<docs />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 头部 -->
|
||||
<workplace-header class="mb16"
|
||||
:data="statisticsData" />
|
||||
<!-- 统计信息 -->
|
||||
<workplace-statistics class="mb16"
|
||||
:data="statisticsData" />
|
||||
<a-row :gutter="16" class="mb16" align="stretch">
|
||||
<!-- 最近终端连接表格 -->
|
||||
<terminal-connect-table :loading="assetLoading"
|
||||
:data="statisticsData" />
|
||||
<!-- 最近批量执行表格 -->
|
||||
<batch-exec-table :loading="assetLoading"
|
||||
:data="statisticsData" />
|
||||
<!-- 快捷操作 -->
|
||||
<quick-operation />
|
||||
</a-row>
|
||||
<a-row :gutter="16" align="stretch">
|
||||
<!-- 每日操作数量图表 -->
|
||||
<operator-log-chart :data="statisticsData" />
|
||||
<!-- 用户登录日志 -->
|
||||
<user-login-table :loading="infraLoading"
|
||||
:data="statisticsData" />
|
||||
</a-row>
|
||||
</div>
|
||||
</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">
|
||||
export default {
|
||||
name: 'workplace',
|
||||
};
|
||||
</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>
|
||||
.top-side {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
margin-top: 16px;
|
||||
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 {
|
||||
:deep(.card) {
|
||||
padding: 16px 20px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-bg-2);
|
||||
|
||||
:deep(.text) {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: rgb(var(--gray-8));
|
||||
}
|
||||
&-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
user-select: none;
|
||||
|
||||
:deep(.wrapper) {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
.text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
background-color: #E8F3FF;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
&-left {
|
||||
margin: 0;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.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;
|
||||
&-body {
|
||||
height: calc(100% - 36px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-table-empty) {
|
||||
.arco-table-td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
</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
|
||||
} from '@/api/exec/exec-command-log';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { tableColumns } from '../types/table.columns';
|
||||
import { ExecStatus, execStatusKey } from '@/components/exec/log/const';
|
||||
import { useExpandable, useTablePagination, useRowSelection } from '@/hooks/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { useDictStore, useUserStore } from '@/store';
|
||||
import { dateFormat, formatDuration } from '@/utils';
|
||||
import { reExecCommand } from '@/api/exec/exec-command';
|
||||
import { interruptExecCommand } from '@/api/exec/exec-command-log';
|
||||
@@ -230,6 +231,7 @@
|
||||
|
||||
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
|
||||
|
||||
const route = useRoute();
|
||||
const pagination = useTablePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const expandable = useExpandable();
|
||||
@@ -406,6 +408,11 @@
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 当前用户
|
||||
const action = route.query.action as string;
|
||||
if (action === 'self') {
|
||||
formModel.userId = useUserStore().id;
|
||||
}
|
||||
// 加载数据
|
||||
fetchTableData();
|
||||
// 注册状态轮询
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from '@/components/exec/log/const';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { openNewRoute } from '@/router';
|
||||
import ExecCommandLogTable from './components/exec-command-log-table.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 ExecLogPanelModal from '@/components/exec/log/panel-modal/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const render = ref(false);
|
||||
const tableRef = ref();
|
||||
const logModal = ref();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="tabs-container" v-if="render">
|
||||
<a-tabs type="rounded"
|
||||
<a-tabs v-model:active-key="activeTab"
|
||||
type="rounded"
|
||||
size="medium"
|
||||
position="left"
|
||||
:lazy-load="true"
|
||||
@@ -64,6 +65,7 @@
|
||||
const userStore = useUserStore();
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
const activeTab = ref<string>('mineInfo');
|
||||
const render = ref<boolean>(false);
|
||||
const userId = ref<number>();
|
||||
const user = ref<UserQueryResponse>();
|
||||
@@ -72,11 +74,18 @@
|
||||
const clickTab = (key: string) => {
|
||||
if (key === 'back') {
|
||||
router.back();
|
||||
} else {
|
||||
activeTab.value = key;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
// 设置当前面板
|
||||
const tab = route.query.tab as string;
|
||||
if (tab) {
|
||||
activeTab.value = tab;
|
||||
}
|
||||
// 获取 userId
|
||||
const queryUserId = route.query.id as string;
|
||||
if (!queryUserId) {
|
||||
|
||||
Reference in New Issue
Block a user