mirror of
https://github.com/lzh-1625/go_process_manager.git
synced 2025-10-07 08:51:06 +08:00
update
This commit is contained in:
@@ -71,7 +71,7 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
log.Logger.Infow("ws连接成功")
|
||||
|
||||
wsCtx, cancel := context.WithCancel(context.Background())
|
||||
@@ -79,8 +79,10 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
WsConnect: conn,
|
||||
CancelFunc: cancel,
|
||||
wsLock: sync.Mutex{},
|
||||
}
|
||||
if err := proc.ReadCache(wci); err != nil {
|
||||
return nil
|
||||
}
|
||||
proc.ReadCache(wci)
|
||||
if proc.State.State == eum.ProcessStateRunning {
|
||||
proc.SetTerminalSize(req.Cols, req.Rows)
|
||||
w.startWsConnect(wci, cancel, proc, hasOprPermission(ctx, req.Uuid, eum.OperationTerminalWrite))
|
||||
@@ -99,7 +101,6 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
case <-wsCtx.Done():
|
||||
log.Logger.Infow("ws连接断开", "操作类型", "tcp连接建立已被关闭")
|
||||
}
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
log.Logger.Infow("ws连接成功")
|
||||
data.UpdatedAt = time.Now()
|
||||
repository.WsShare.Edit(data)
|
||||
@@ -141,7 +142,9 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
CancelFunc: cancel,
|
||||
wsLock: sync.Mutex{},
|
||||
}
|
||||
proc.ReadCache(wci)
|
||||
if err := proc.ReadCache(wci); err != nil {
|
||||
return nil
|
||||
}
|
||||
w.startWsConnect(wci, cancel, proc, data.Write)
|
||||
proc.AddConn(guestName, wci)
|
||||
defer proc.DeleteConn(guestName)
|
||||
@@ -159,7 +162,6 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
case <-time.After(time.Until(data.ExpireTime)):
|
||||
log.Logger.Infow("ws连接断开", "操作类型", "分享时间已结束")
|
||||
}
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
type Process interface {
|
||||
ReadCache(ConnectInstance)
|
||||
ReadCache(ConnectInstance) error
|
||||
Write(string) error
|
||||
WriteBytes([]byte) error
|
||||
readInit()
|
||||
|
@@ -2,6 +2,7 @@ package logic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -113,10 +114,12 @@ func (p *ProcessPty) readInit() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) {
|
||||
if p.cacheBytesBuf != nil {
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) error {
|
||||
if p.cacheBytesBuf == nil {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessPty) bufHanle(b []byte) {
|
||||
|
@@ -122,8 +122,12 @@ func (p *ProcessPty) readInit() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) {
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) error {
|
||||
if p.cacheBytesBuf == nil {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessPty) bufHanle(b []byte) {
|
||||
|
@@ -87,10 +87,14 @@ func (p *ProcessStd) doOnInit() {
|
||||
p.cacheLine = make([]string, config.CF.ProcessMsgCacheLinesLimit)
|
||||
}
|
||||
|
||||
func (p *ProcessStd) ReadCache(ws ConnectInstance) {
|
||||
func (p *ProcessStd) ReadCache(ws ConnectInstance) error {
|
||||
if len(p.cacheLine) == 0 {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
for _, line := range p.cacheLine {
|
||||
ws.WriteString(line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessStd) doOnKilled() {
|
||||
|
@@ -9,12 +9,12 @@ export function getProcessListWait() {
|
||||
return api.get<ProcessItem[]>("/process/wait", undefined).then((res) => res);
|
||||
}
|
||||
|
||||
export function killProcessAll(uuid) {
|
||||
return api.delete("/process/all", { uuid }).then((res) => res);
|
||||
export function killProcessAll() {
|
||||
return api.delete("/process/all", { }).then((res) => res);
|
||||
}
|
||||
|
||||
export function startProcessAll(uuid) {
|
||||
return api.put("/process/all", { uuid }).then((res) => res);
|
||||
export function startProcessAll() {
|
||||
return api.put("/process/all", { }).then((res) => res);
|
||||
}
|
||||
|
||||
export function killProcess(uuid) {
|
||||
|
45
resources/src/components/ConfirmButton.vue
Normal file
45
resources/src/components/ConfirmButton.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 触发按钮 -->
|
||||
<v-btn :color="color" size="small" variant="tonal" @click="dialog = true">
|
||||
<slot>{{ label }}</slot>
|
||||
</v-btn>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<v-dialog v-model="dialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
{{ message }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialog = false">取消</v-btn>
|
||||
<v-btn :color="color" variant="flat" @click="confirm"> 确认 </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "操作" },
|
||||
title: { type: String, default: "确认" },
|
||||
message: { type: String, default: "确定要执行此操作吗?" },
|
||||
color: { type: String, default: "primary" },
|
||||
});
|
||||
|
||||
const emits = defineEmits(["confirm"]);
|
||||
|
||||
const dialog = ref(false);
|
||||
|
||||
const confirm = () => {
|
||||
dialog.value = false;
|
||||
emits("confirm");
|
||||
};
|
||||
</script>
|
@@ -2,7 +2,12 @@
|
||||
import { ProcessItem } from "~/src/types/process/process";
|
||||
import { init } from "echarts";
|
||||
import TerminalPty from "./TerminalPty.vue";
|
||||
import { killProcess, startProcess } from "~/src/api/process";
|
||||
import {
|
||||
deleteProcessConfig,
|
||||
getContorl,
|
||||
killProcess,
|
||||
startProcess,
|
||||
} from "~/src/api/process";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import ProcessConfig from "./ProcessConfig.vue";
|
||||
let chartInstance;
|
||||
@@ -17,7 +22,7 @@ const initEChart = () => {
|
||||
);
|
||||
const cpu = props.data.usage.cpu[props.data.usage.cpu.length - 1] ?? "-";
|
||||
const mem = props.data.usage.mem[props.data.usage.mem.length - 1] ?? "-";
|
||||
var myChart = init(document.getElementById("echarts" + props.index));
|
||||
var myChart = init(document.getElementById("echarts" + props.data.uuid));
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
@@ -95,7 +100,7 @@ const initEChart = () => {
|
||||
name: "CPU限制(%)",
|
||||
type: "line",
|
||||
yAxisIndex: 0,
|
||||
data: new Array(props.data.usage.time!.length).fill(
|
||||
data: new Array(props.data.usage.time?.length ?? 0).fill(
|
||||
props.data.cpuLimit
|
||||
),
|
||||
lineStyle: {
|
||||
@@ -110,7 +115,7 @@ const initEChart = () => {
|
||||
name: "内存限制(MB)",
|
||||
type: "line",
|
||||
yAxisIndex: 1,
|
||||
data: new Array(props.data.usage.time!.length).fill(
|
||||
data: new Array(props.data.usage.time?.length ?? 0).fill(
|
||||
props.data.memoryLimit
|
||||
),
|
||||
lineStyle: {
|
||||
@@ -132,7 +137,6 @@ const terminalComponent = ref<WsHandle | null>(null);
|
||||
|
||||
type ConfigHandle = {
|
||||
openConfigDialog: () => void;
|
||||
test: () => void;
|
||||
};
|
||||
const processConfigComponent = ref<ConfigHandle | null>(null);
|
||||
|
||||
@@ -182,8 +186,23 @@ onMounted(() => {
|
||||
|
||||
const props = defineProps<{
|
||||
data: ProcessItem;
|
||||
index: Number;
|
||||
}>();
|
||||
|
||||
const control = () => {
|
||||
getContorl(props.data.uuid).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const del = () => {
|
||||
deleteProcessConfig(props.data.uuid).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
@@ -231,8 +250,8 @@ const props = defineProps<{
|
||||
</template>
|
||||
|
||||
<v-list nav dense>
|
||||
<v-list-item @click=""> 获取控制权 </v-list-item>
|
||||
<v-list-item @click=""> 删除进程 </v-list-item>
|
||||
<v-list-item @click="control"> 获取控制权 </v-list-item>
|
||||
<v-list-item @click="del"> 删除进程 </v-list-item>
|
||||
<v-list-item @click=""> 创建分享链接 </v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -240,7 +259,7 @@ const props = defineProps<{
|
||||
</div>
|
||||
|
||||
<!-- 中间:ECharts -->
|
||||
<div :id="'echarts' + props.index" class="chart"></div>
|
||||
<div :id="'echarts' + props.data.uuid" class="chart"></div>
|
||||
|
||||
<!-- 底部:按钮组 + 时间 -->
|
||||
<div class="footer">
|
||||
@@ -274,7 +293,10 @@ const props = defineProps<{
|
||||
ref="terminalComponent"
|
||||
></TerminalPty>
|
||||
<TerminalPty v-else :data="props.data"></TerminalPty>
|
||||
<ProcessConfig :data="props.data" ref="processConfigComponent"></ProcessConfig>
|
||||
<ProcessConfig
|
||||
:data="props.data"
|
||||
ref="processConfigComponent"
|
||||
></ProcessConfig>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -105,9 +105,6 @@ const initPushItem = () => {
|
||||
density="compact"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
label="终端类型"
|
||||
@@ -118,22 +115,21 @@ const initPushItem = () => {
|
||||
density="compact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="pushSelectedValues"
|
||||
@change="updateJsonString"
|
||||
:items="pushItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
chips
|
||||
label="状态推送"
|
||||
multiple
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-select
|
||||
v-model="pushSelectedValues"
|
||||
@change="updateJsonString"
|
||||
:items="pushItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
chips
|
||||
label="状态推送"
|
||||
multiple
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<v-row align="center">
|
||||
|
197
resources/src/components/process/ProcessCreate.vue
Normal file
197
resources/src/components/process/ProcessCreate.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { postProcessConfig } from "~/src/api/process";
|
||||
import { getPushList } from "~/src/api/push";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import { ProcessConfig } from "~/src/types/process/process";
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const dialog = ref(false);
|
||||
const configForm = ref<Partial<ProcessConfig>>({});
|
||||
const pushItems = ref<{ value: any; label: string }[]>([]);
|
||||
const pushSelectedValues = ref([]);
|
||||
|
||||
watch(
|
||||
pushSelectedValues,
|
||||
(newValues) => {
|
||||
configForm.value.pushIds = JSON.stringify(newValues);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
defineExpose({
|
||||
createProcessDialog: () => {
|
||||
initPushItem();
|
||||
dialog.value = true;
|
||||
},
|
||||
});
|
||||
|
||||
const updateJsonString = () => {
|
||||
configForm.value.pushIds = JSON.stringify(pushSelectedValues);
|
||||
};
|
||||
|
||||
const initPushItem = () => {
|
||||
getPushList().then((resp) => {
|
||||
// 3. 更新 ref 的 .value
|
||||
if (resp.data) {
|
||||
pushItems.value = resp.data.map((e) => ({
|
||||
value: e.id,
|
||||
label: `${e.remark} [${e.id}]`,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const create = () => {
|
||||
postProcessConfig(configForm.value).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
dialog.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="dialog" width="700">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 grey lighten-2">
|
||||
<v-icon left>mdi-cog</v-icon>
|
||||
添加进程
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
label="进程名称"
|
||||
v-model="configForm.name"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
label="终端类型"
|
||||
v-model="configForm.termType"
|
||||
:items="['pty', 'std']"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-text-field
|
||||
label="工作目录"
|
||||
v-model="configForm.cwd"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="12">
|
||||
<v-textarea
|
||||
label="启动命令"
|
||||
rows="2"
|
||||
v-model="configForm.cmd"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-select
|
||||
v-model="pushSelectedValues"
|
||||
@change="updateJsonString"
|
||||
:items="pushItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
chips
|
||||
label="状态推送"
|
||||
multiple
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="3">
|
||||
<v-switch
|
||||
v-model="configForm.cgroupEnable"
|
||||
label="资源限制"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
:disabled="!configForm.cgroupEnable"
|
||||
label="CPU 限制 (%)"
|
||||
type="number"
|
||||
v-model.number="configForm.cpuLimit"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
:disabled="!configForm.cgroupEnable"
|
||||
label="内存限制 (MB)"
|
||||
type="number"
|
||||
v-model.number="configForm.memoryLimit"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-switch
|
||||
v-model="configForm.autoRestart"
|
||||
label="自动重启"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-switch
|
||||
:disabled="!configForm.autoRestart"
|
||||
v-model="configForm.compulsoryRestart"
|
||||
label="强制重启"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-switch
|
||||
v-model="configForm.logReport"
|
||||
label="日志上报"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" color="grey-darken-1" @click="dialog = false">
|
||||
<v-icon left>mdi-close</v-icon>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn variant="flat" color="primary" @click="create">
|
||||
<v-icon left>mdi-check</v-icon>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
@@ -5,8 +5,8 @@ import { ProcessItem } from "~/src/types/process/process";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { AttachAddon } from "xterm-addon-attach";
|
||||
import { CanvasAddon } from '@xterm/addon-canvas';
|
||||
import 'xterm/css/xterm.css';
|
||||
import { CanvasAddon } from "@xterm/addon-canvas";
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const dialog = ref(false);
|
||||
@@ -35,7 +35,6 @@ watch(dialog, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const initWebSocketPty = () => {
|
||||
if (!xtermEl.value) {
|
||||
snackbarStore.showErrorMessage("终端容器初始化失败");
|
||||
@@ -46,7 +45,9 @@ const initWebSocketPty = () => {
|
||||
const initialRows = Math.floor(xtermEl.value.clientHeight / 19);
|
||||
|
||||
const baseUrl = `ws://${window.location.hostname}:8797/api/ws`;
|
||||
const url = `${baseUrl}?uuid=${props.data.uuid}&token=${localStorage.getItem("token")}&cols=${initialCols}&rows=${initialRows}`;
|
||||
const url = `${baseUrl}?uuid=${props.data.uuid}&token=${localStorage.getItem(
|
||||
"token"
|
||||
)}&cols=${initialCols}&rows=${initialRows}`;
|
||||
|
||||
initSocket(url);
|
||||
};
|
||||
@@ -61,6 +62,7 @@ const initSocket = (url: string) => {
|
||||
|
||||
socket.onclose = () => {
|
||||
snackbarStore.showErrorMessage("终端连接断开");
|
||||
dialog.value = false;
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
@@ -71,15 +73,15 @@ const initSocket = (url: string) => {
|
||||
|
||||
const initTerm = () => {
|
||||
if (!socket || !xtermEl.value) return;
|
||||
|
||||
const showCursor = props.data.state.state === 3;
|
||||
term = new Terminal({
|
||||
// rendererType: "canvas", // 已通过插件方式加载,此处无需设置
|
||||
convertEol: true,
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorBlink: showCursor,
|
||||
cursorStyle: "block",
|
||||
theme: {
|
||||
foreground: "#ECECEC",
|
||||
cursor: "help",
|
||||
cursor: "help"
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,6 +108,13 @@ const wsClose = () => {
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const toolbarColor = computed(() => {
|
||||
if (props.data.state.state == 3) {
|
||||
return;
|
||||
}
|
||||
return "red";
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (term) {
|
||||
@@ -129,7 +138,7 @@ onUnmounted(() => {
|
||||
hide-overlay
|
||||
transition="dialog-bottom-transition"
|
||||
v-model="dialog"
|
||||
@update:modelValue="val => !val && cleanup()"
|
||||
@update:modelValue="(val) => !val && cleanup()"
|
||||
>
|
||||
<v-card
|
||||
style="
|
||||
@@ -141,8 +150,8 @@ onUnmounted(() => {
|
||||
>
|
||||
<v-toolbar
|
||||
dense
|
||||
:color="toolbarColor"
|
||||
dark
|
||||
color="blue-grey darken-4"
|
||||
style="height: 35px; flex-grow: 0"
|
||||
>
|
||||
<v-toolbar-title style="height: 100%"
|
||||
@@ -155,7 +164,11 @@ onUnmounted(() => {
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<div id="xterm" ref="xtermEl" style="flex-grow: 1; height: 100%; width: 100%;"></div>
|
||||
<div
|
||||
id="xterm"
|
||||
ref="xtermEl"
|
||||
style="flex-grow: 1; height: 100%; width: 100%"
|
||||
></div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
@@ -1,22 +1,48 @@
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<ConfirmButton @confirm="startAll" color="#3CB371">全部启动</ConfirmButton>
|
||||
<ConfirmButton @confirm="killAll" color="#CD5555">全部停止</ConfirmButton>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="blue"
|
||||
@click="processCreateComponent?.createProcessDialog()"
|
||||
>创建<v-icon dark right> mdi-plus-circle </v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-container>
|
||||
<!-- 顶部工具栏 -->
|
||||
|
||||
<!-- 主体网格 -->
|
||||
<div class="flex-grid">
|
||||
<div v-for="(i, v) in processData" class="responsive-box">
|
||||
<ProcessCard :data="i" :index="v"></ProcessCard>
|
||||
<div v-for="(i, v) in processData" :key="i.uuid" class="responsive-box">
|
||||
<ProcessCard :data="i" :index="v" />
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
<ProcessCreate ref="processCreateComponent"></ProcessCreate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProcessCard from "@/components/process/ProcessCard.vue";
|
||||
import axios from "axios";
|
||||
import { getProcessList } from "~/src/api/process";
|
||||
import {
|
||||
getProcessList,
|
||||
killProcessAll,
|
||||
startProcessAll,
|
||||
} from "~/src/api/process";
|
||||
import ConfirmButton from "~/src/components/ConfirmButton.vue";
|
||||
import ProcessCreate from "~/src/components/process/ProcessCreate.vue";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import { ProcessItem } from "~/src/types/process/process";
|
||||
|
||||
type CreateHandle = {
|
||||
createProcessDialog: () => void;
|
||||
test: () => void;
|
||||
};
|
||||
const processCreateComponent = ref<CreateHandle | null>(null);
|
||||
const processData = ref<ProcessItem[]>();
|
||||
const uuid: string = crypto.randomUUID();
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const initProcessData = () => {
|
||||
getProcessList().then((e) => {
|
||||
processData.value = e.data!.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -24,7 +50,23 @@ const initProcessData = () => {
|
||||
});
|
||||
};
|
||||
|
||||
var cancelTokenSource;
|
||||
const startAll = () => {
|
||||
startProcessAll().then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const killAll = () => {
|
||||
killProcessAll().then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let cancelTokenSource: any;
|
||||
const getProcessListWait = () => {
|
||||
cancelTokenSource = axios.CancelToken.source();
|
||||
axios
|
||||
@@ -51,26 +93,36 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 工具栏样式 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 靠右对齐 */
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #eee; /* 轻量分隔线 */
|
||||
}
|
||||
|
||||
/* 原来的网格样式 */
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 自动换行 */
|
||||
justify-content: space-between; /* 两边与中间间距均匀 */
|
||||
gap: 80px; /* 每个 div 之间的间距 */
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.responsive-box {
|
||||
flex: 1 1 300px; /* 最小宽度 300px */
|
||||
flex: 1 1 300px;
|
||||
min-width: 300px;
|
||||
max-width: 100%;
|
||||
background: #ffffff; /* 改为白色背景 */
|
||||
border-radius: 16px; /* 圆角 */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* 柔和阴影 */
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
text-align: center;
|
||||
padding: 10px; /* 内边距,让内容不贴边 */
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease; /* 交互动画 */
|
||||
padding: 10px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.responsive-box:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
|
Reference in New Issue
Block a user