!101 feat: 新增机器操作菜单

* feat: 新增机器操作菜单
This commit is contained in:
zongyangleo
2024-02-07 06:37:59 +00:00
committed by Coder慌
parent 9c20bdef39
commit 20eb06fb28
8 changed files with 616 additions and 245 deletions

View File

@@ -13,6 +13,7 @@
tagSelectRef.validate();
}
"
:tag-path="form.tagPath"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Machine.value"
style="width: 100%"
@@ -153,6 +154,7 @@ const state = reactive({
form: {
id: null,
code: '',
tagPath: '',
ip: null,
port: 22,
name: null,

View File

@@ -1,207 +1,191 @@
<template>
<div class="flex-all-center">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Machine.value" :tag-path-node-type="NodeTypeTagPath" />
</Pane>
<div>
<page-table
ref="pageTableRef"
:page-api="machineApi.list"
:before-query-fn="checkRouteTagPath"
:search-items="searchItems"
v-model:query-form="params"
:show-selection="true"
v-model:selection-data="state.selectionData"
:columns="columns"
>
<template #tableHeader>
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 </el-button>
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button>
</template>
<Pane>
<div>
<page-table :data="state.pageData" :pageable="false" :show-selection="true" v-model:selection-data="state.selectionData" :columns="columns">
<template #tableHeader>
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 </el-button>
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete"
>删除</el-button
>
</template>
<template #ipPort="{ data }">
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
{{ `${data.ip}:${data.port}` }}
</el-link>
</template>
<template #ipPort="{ data }">
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
{{ `${data.ip}:${data.port}` }}
</el-link>
</template>
<template #stat="{ data }">
<span v-if="!data.stat">-</span>
<div v-else>
<el-row>
<el-text size="small" style="font-size: 10px">
内存(可用/):
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
</span>
</el-text>
</el-row>
<el-row>
<el-text style="font-size: 10px" size="small">
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
</el-text>
</el-row>
</div>
</template>
<template #fs="{ data }">
<span v-if="!data.stat?.fsInfos">-</span>
<div v-else>
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
<!-- 展示剩余的磁盘信息 -->
<el-popover
:show-after="300"
v-if="data.stat.fsInfos.length > 2 && idx == 1"
placement="top-start"
width="230"
trigger="hover"
>
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
</el-row>
</el-popover>
</el-row>
</div>
</template>
<template #status="{ data }">
<el-switch
v-auth:disabled="'machine:update'"
:width="52"
v-model="data.status"
:active-value="1"
:inactive-value="-1"
inline-prompt
active-text="启用"
inactive-text="停用"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="changeStatus(data)"
></el-switch>
</template>
<template #action="{ data }">
<span v-auth="'machine:terminal'">
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<template #stat="{ data }">
<span v-if="!data.stat">-</span>
<div v-else>
<el-row>
<el-text size="small" style="font-size: 10px">
内存(可用/):
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
</span>
<span v-auth="'machine:file'">
<el-button type="success" :disabled="data.status == -1" @click="showFileManage(data)" link>文件</el-button>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-button :disabled="data.status == -1" type="warning" @click="serviceManager(data)" link>脚本</el-button>
<el-divider direction="vertical" border-style="dashed" />
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link-machine-list">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> 编辑 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'process', data }" :disabled="data.status == -1"> 进程 </el-dropdown-item>
<el-dropdown-item
:command="{ type: 'terminalRec', data }"
v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1"
>
终端回放
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'closeCli', data }"
v-if="actionBtns[perms.closeCli]"
:disabled="!data.hasCli || data.status == -1"
>
关闭连接
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<terminal-dialog ref="terminalDialogRef" :visibleMinimize="true">
<template #headerTitle="{ terminalInfo }">
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
<el-divider direction="vertical" />
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
<el-divider direction="vertical" />
{{ terminalInfo.meta.name }}
</template>
</terminal-dialog>
<machine-edit
:title="machineEditDialog.title"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
@valChange="submitSuccess"
></machine-edit>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<machine-stats
v-model:visible="machineStatsDialog.visible"
:machineId="machineStatsDialog.machineId"
:title="machineStatsDialog.title"
></machine-stats>
<machine-rec
v-model:visible="machineRecDialog.visible"
:machineId="machineRecDialog.machineId"
:title="machineRecDialog.title"
></machine-rec>
</el-text>
</el-row>
<el-row>
<el-text style="font-size: 10px" size="small">
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
</el-text>
</el-row>
</div>
</Pane>
</Splitpanes>
</template>
<template #fs="{ data }">
<span v-if="!data.stat?.fsInfos">-</span>
<div v-else>
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
<!-- 展示剩余的磁盘信息 -->
<el-popover :show-after="300" v-if="data.stat.fsInfos.length > 2 && idx == 1" placement="top-start" width="230" trigger="hover">
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
</el-row>
</el-popover>
</el-row>
</div>
</template>
<template #status="{ data }">
<el-switch
v-auth:disabled="'machine:update'"
:width="52"
v-model="data.status"
:active-value="1"
:inactive-value="-1"
inline-prompt
active-text="启用"
inactive-text="停用"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="changeStatus(data)"
></el-switch>
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
</template>
<template #action="{ data }">
<span v-auth="'machine:terminal'">
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
</span>
<span v-auth="'machine:file'">
<el-button type="success" :disabled="data.status == -1" @click="showFileManage(data)" link>文件</el-button>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-button :disabled="data.status == -1" type="warning" @click="serviceManager(data)" link>脚本</el-button>
<el-divider direction="vertical" border-style="dashed" />
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link-machine-list">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> 编辑 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'process', data }" :disabled="data.status == -1"> 进程 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
终端回放
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'closeCli', data }"
v-if="actionBtns[perms.closeCli]"
:disabled="!data.hasCli || data.status == -1"
>
关闭连接
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</page-table>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<terminal-dialog ref="terminalDialogRef" :visibleMinimize="true">
<template #headerTitle="{ terminalInfo }">
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
<el-divider direction="vertical" />
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
<el-divider direction="vertical" />
{{ terminalInfo.meta.name }}
</template>
</terminal-dialog>
<machine-edit
:title="machineEditDialog.title"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
@valChange="submitSuccess"
></machine-edit>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title"></machine-rec>
</div>
</template>
@@ -218,9 +202,8 @@ import { hasPerms } from '@/components/auth/auth';
import { formatByteSize } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { SearchItem } from '@/components/SearchForm';
import { getTagPathSearchItem, NodeType, TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { Splitpanes, Pane } from 'splitpanes';
import { getTagPathSearchItem } from '../component/tag';
// 组件
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
const MachineEdit = defineAsyncComponent(() => import('./MachineEdit.vue'));
@@ -252,6 +235,7 @@ const columns = [
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
TableColumn.new('username', '用户名'),
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
];
@@ -259,10 +243,6 @@ const columns = [
// 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
class MachineNodeType {
static Machine = 1;
}
const state = reactive({
params: {
pageNum: 1,
@@ -271,7 +251,6 @@ const state = reactive({
name: null,
tagPath: '',
},
pageData: [] as any[],
infoDialog: {
visible: false,
data: null as any,
@@ -312,21 +291,7 @@ const state = reactive({
const { params, infoDialog, selectionData, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineEditDialog, machineRecDialog } = toRefs(state);
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
// 加载标签树下的机器列表
state.params.tagPath = node.key;
state.params.pageNum = 1;
state.params.pageSize = 1000;
const res = await search();
// 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
state.pageData = res.list;
return res.list.map((x: any) => new TagTreeNode(x.id, x.name, NodeTypeMachine).withParams(x).withIsLeaf(true));
});
const NodeTypeMachine = new NodeType(MachineNodeType.Machine).withNodeClickFunc((node: any) => {
state.pageData = [node.params];
});
onMounted(async () => {});
const checkRouteTagPath = (query: any) => {
if (route.query.tagPath) {
@@ -456,9 +421,7 @@ const showMachineStats = async (machine: any) => {
};
const search = async () => {
const res = await machineApi.list.request(state.params);
state.pageData = res.list;
return res;
pageTableRef.value.search();
};
const submitSuccess = () => {

View File

@@ -0,0 +1,406 @@
<template>
<div class="flex-all-center">
<!-- 文档 https://antoniandre.github.io/splitpanes/ -->
<Splitpanes class="default-theme" @resized="onResizeTagTree">
<Pane size="20" max-size="30">
<tag-tree
class="machine-terminal-tree"
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.Machine.value"
:tag-path-node-type="NodeTypeTagPath"
/>
</Pane>
<Pane>
<div class="card pd5">
<el-tabs
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeTermName"
class="h100 machine-terminal-tabs"
>
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label>
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
<template #reference>
<el-icon class="mr2" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
><Connection />
</el-icon>
</template>
</el-popconfirm>
<el-popover placement="bottom-start" trigger="hover" :width="250">
<template #reference>
<div>
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
</div>
</template>
<template #default>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</template>
<div class="terminal-wrapper" :style="{ height: `calc(100vh - 155px)` }">
<TerminalBody
@status-change="terminalStatusChange(dt.key, $event)"
:ref="(el) => setTerminalRef(el, dt.key)"
:socket-url="dt.terminalInfo.socketUrl"
/>
</div>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<terminal-dialog ref="terminalDialogRef" :visibleMinimize="true">
<template #headerTitle="{ terminalInfo }">
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
<el-divider direction="vertical" />
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
<el-divider direction="vertical" />
{{ terminalInfo.meta.name }}
</template>
</terminal-dialog>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
</div>
</Pane>
</Splitpanes>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { machineApi, getMachineTerminalSocketUrl } from './api';
import { dateFormat } from '@/common/utils/date';
import { hasPerms } from '@/components/auth/auth';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { Splitpanes, Pane } from 'splitpanes';
import { ContextmenuItem } from '@/components/contextmenu/index';
// 组件
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { TerminalStatus } from '@/components/terminal/common';
const router = useRouter();
const terminalDialogRef: any = ref(null);
const perms = {
addMachine: 'machine:add',
updateMachine: 'machine:update',
delMachine: 'machine:del',
terminal: 'machine:terminal',
closeCli: 'machine:close-cli',
};
// 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
class MachineNodeType {
static Machine = 1;
}
const state = reactive({
params: {
pageNum: 1,
pageSize: 0,
ip: null,
name: null,
tagPath: '',
},
pageData: [] as any[],
infoDialog: {
visible: false,
data: null as any,
},
// 当前选中数据
selectionData: [],
serviceDialog: {
visible: false,
machineId: 0,
title: '',
},
processDialog: {
visible: false,
machineId: 0,
},
fileDialog: {
visible: false,
machineId: 0,
title: '',
},
machineStatsDialog: {
visible: false,
stats: null,
title: '',
machineId: 0,
},
machineRecDialog: {
visible: false,
machineId: 0,
title: '',
},
activeTermName: '',
tabs: new Map(),
});
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
const tagTreeRef: any = ref(null);
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
// 加载标签树下的机器列表
state.params.tagPath = node.key;
state.params.pageNum = 1;
state.params.pageSize = 1000;
const res = await search();
// 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
state.pageData = res.list;
return res.list.map((x: any) => new TagTreeNode(x.id, x.name, NodeTypeMachine(x)).withParams(x).withIsLeaf(true));
});
let termIndex = 0;
let openIds = {};
const NodeTypeMachine = (machine: any) => {
let contextMenuItems = [];
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => showTerminal(machine)));
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => showTerminal(machine, true)));
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
}
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeClickFunc(() => {
state.pageData = [machine];
showTerminal(machine);
});
};
const showTerminal = (machine: any, ex?: boolean) => {
// 按住ctrl点击则新建标签页打开, metaKey对应mac command键
if (ex) {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: machine.id,
name: machine.name,
},
});
window.open(href, '_blank');
return;
}
let { name, id, username, ip, port } = machine;
// 同一个机器的终端打开多次key后添加下划线和数字区分
openIds[id] = openIds[id] ? ++openIds[id] : 1;
let sameIndex = openIds[id];
const terminalId = Date.now();
let key = name + '_' + id + '_' + sameIndex + '_' + terminalId;
// 只保留name的10个字超出部分只保留前后4个字符中间用省略号代替
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
state.tabs.set(key, {
key,
label: `${++termIndex}.${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
params: machine,
terminalInfo: {
terminalId: terminalId,
status: TerminalStatus.NoConnected,
socketUrl: getMachineTerminalSocketUrl(id),
minTitle: `${name} [${(terminalId + '').slice(-2)}]`, // 截取terminalId最后两位区分多个terminal
minDesc: `${username}@${ip}:${port} (${name})`,
meta: machine,
},
});
state.activeTermName = key;
fitTerminal();
};
const serviceManager = (row: any) => {
state.serviceDialog.machineId = row.id;
state.serviceDialog.visible = true;
state.serviceDialog.title = `${row.name} => ${row.ip}`;
};
/**
* 显示机器状态统计信息
*/
const showMachineStats = async (machine: any) => {
state.machineStatsDialog.machineId = machine.id;
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
state.machineStatsDialog.visible = true;
};
const search = async () => {
const res = await machineApi.list.request(state.params);
state.pageData = res.list;
return res;
};
const showFileManage = (selectionData: any) => {
state.fileDialog.visible = true;
state.fileDialog.machineId = selectionData.id;
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
};
const getStatsFontClass = (available: number, total: number) => {
const p = available / total;
if (p < 0.1) {
return 'color-danger';
}
if (p < 0.2) {
return 'color-warning';
}
return 'color-success';
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;
};
const showProcess = (row: any) => {
state.processDialog.machineId = row.id;
state.processDialog.visible = true;
};
const showRec = (row: any) => {
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
state.machineRecDialog.machineId = row.id;
state.machineRecDialog.visible = true;
};
const onRemoveTab = (targetName: string) => {
let activeTermName = state.activeTermName;
const tabNames = [...state.tabs.keys()];
for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i];
if (tabName !== targetName) {
continue;
}
const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) {
activeTermName = nextTab;
} else {
activeTermName = '';
}
// 关闭连接
machineApi.closeCli.request({ id: state.tabs.get(targetName).params.id });
state.tabs.delete(targetName);
state.activeTermName = activeTermName;
onTabChange();
}
};
const terminalStatusChange = (key: string, status: TerminalStatus) => {
state.tabs.get(key).status = status;
};
const terminalRefs: any = {};
const setTerminalRef = (el: any, key: any) => {
if (key) {
terminalRefs[key] = el;
}
};
const onResizeTagTree = (a) => {
fitTerminal();
};
const onTabChange = () => {
fitTerminal();
};
const fitTerminal = () => {
setTimeout(() => {
let info = state.tabs.get(state.activeTermName);
if (info) {
terminalRefs[info.key]?.resize();
}
}, 500);
};
const handleReconnect = (key: string) => {
terminalRefs[key].init();
};
</script>
<style lang="scss">
.machine-terminal-tabs {
--el-tabs-header-height: 30px;
.machine-terminal-tab-label {
font-size: 12px;
}
.el-tabs__header {
margin-bottom: 5px;
}
.el-tabs__item {
padding: 0 5px !important;
}
}
.machine-terminal-tree {
.el-tree-node__content {
font-size: 12px;
}
}
</style>