This commit is contained in:
akrike
2025-09-07 11:10:09 +08:00
parent ab7ef546ea
commit 593185a431
12 changed files with 408 additions and 70 deletions

View File

@@ -71,7 +71,7 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close()
log.Logger.Infow("ws连接成功") log.Logger.Infow("ws连接成功")
wsCtx, cancel := context.WithCancel(context.Background()) wsCtx, cancel := context.WithCancel(context.Background())
@@ -79,8 +79,10 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
WsConnect: conn, WsConnect: conn,
CancelFunc: cancel, CancelFunc: cancel,
wsLock: sync.Mutex{}, wsLock: sync.Mutex{},
}
if err := proc.ReadCache(wci); err != nil {
return nil
} }
proc.ReadCache(wci)
if proc.State.State == eum.ProcessStateRunning { if proc.State.State == eum.ProcessStateRunning {
proc.SetTerminalSize(req.Cols, req.Rows) proc.SetTerminalSize(req.Cols, req.Rows)
w.startWsConnect(wci, cancel, proc, hasOprPermission(ctx, req.Uuid, eum.OperationTerminalWrite)) 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(): case <-wsCtx.Done():
log.Logger.Infow("ws连接断开", "操作类型", "tcp连接建立已被关闭") log.Logger.Infow("ws连接断开", "操作类型", "tcp连接建立已被关闭")
} }
conn.Close()
return return
} }
@@ -129,7 +130,7 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
if err != nil { if err != nil {
return err return err
} }
defer conn.Close()
log.Logger.Infow("ws连接成功") log.Logger.Infow("ws连接成功")
data.UpdatedAt = time.Now() data.UpdatedAt = time.Now()
repository.WsShare.Edit(data) repository.WsShare.Edit(data)
@@ -141,7 +142,9 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
CancelFunc: cancel, CancelFunc: cancel,
wsLock: sync.Mutex{}, wsLock: sync.Mutex{},
} }
proc.ReadCache(wci) if err := proc.ReadCache(wci); err != nil {
return nil
}
w.startWsConnect(wci, cancel, proc, data.Write) w.startWsConnect(wci, cancel, proc, data.Write)
proc.AddConn(guestName, wci) proc.AddConn(guestName, wci)
defer proc.DeleteConn(guestName) 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)): case <-time.After(time.Until(data.ExpireTime)):
log.Logger.Infow("ws连接断开", "操作类型", "分享时间已结束") log.Logger.Infow("ws连接断开", "操作类型", "分享时间已结束")
} }
conn.Close()
return return
} }

View File

@@ -21,7 +21,7 @@ import (
) )
type Process interface { type Process interface {
ReadCache(ConnectInstance) ReadCache(ConnectInstance) error
Write(string) error Write(string) error
WriteBytes([]byte) error WriteBytes([]byte) error
readInit() readInit()

View File

@@ -2,6 +2,7 @@ package logic
import ( import (
"bytes" "bytes"
"errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@@ -113,10 +114,12 @@ func (p *ProcessPty) readInit() {
} }
} }
func (p *ProcessPty) ReadCache(ws ConnectInstance) { func (p *ProcessPty) ReadCache(ws ConnectInstance) error {
if p.cacheBytesBuf != nil { if p.cacheBytesBuf == nil {
ws.Write(p.cacheBytesBuf.Bytes()) return errors.New("cache is null")
} }
ws.Write(p.cacheBytesBuf.Bytes())
return nil
} }
func (p *ProcessPty) bufHanle(b []byte) { func (p *ProcessPty) bufHanle(b []byte) {

View File

@@ -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()) ws.Write(p.cacheBytesBuf.Bytes())
return nil
} }
func (p *ProcessPty) bufHanle(b []byte) { func (p *ProcessPty) bufHanle(b []byte) {

View File

@@ -87,10 +87,14 @@ func (p *ProcessStd) doOnInit() {
p.cacheLine = make([]string, config.CF.ProcessMsgCacheLinesLimit) 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 { for _, line := range p.cacheLine {
ws.WriteString(line) ws.WriteString(line)
} }
return nil
} }
func (p *ProcessStd) doOnKilled() { func (p *ProcessStd) doOnKilled() {

View File

@@ -9,12 +9,12 @@ export function getProcessListWait() {
return api.get<ProcessItem[]>("/process/wait", undefined).then((res) => res); return api.get<ProcessItem[]>("/process/wait", undefined).then((res) => res);
} }
export function killProcessAll(uuid) { export function killProcessAll() {
return api.delete("/process/all", { uuid }).then((res) => res); return api.delete("/process/all", { }).then((res) => res);
} }
export function startProcessAll(uuid) { export function startProcessAll() {
return api.put("/process/all", { uuid }).then((res) => res); return api.put("/process/all", { }).then((res) => res);
} }
export function killProcess(uuid) { export function killProcess(uuid) {

View 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>

View File

@@ -2,7 +2,12 @@
import { ProcessItem } from "~/src/types/process/process"; import { ProcessItem } from "~/src/types/process/process";
import { init } from "echarts"; import { init } from "echarts";
import TerminalPty from "./TerminalPty.vue"; 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 { useSnackbarStore } from "~/src/stores/snackbarStore";
import ProcessConfig from "./ProcessConfig.vue"; import ProcessConfig from "./ProcessConfig.vue";
let chartInstance; let chartInstance;
@@ -17,7 +22,7 @@ const initEChart = () => {
); );
const cpu = props.data.usage.cpu[props.data.usage.cpu.length - 1] ?? "-"; const cpu = props.data.usage.cpu[props.data.usage.cpu.length - 1] ?? "-";
const mem = props.data.usage.mem[props.data.usage.mem.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 = { var option = {
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
@@ -95,7 +100,7 @@ const initEChart = () => {
name: "CPU限制%", name: "CPU限制%",
type: "line", type: "line",
yAxisIndex: 0, yAxisIndex: 0,
data: new Array(props.data.usage.time!.length).fill( data: new Array(props.data.usage.time?.length ?? 0).fill(
props.data.cpuLimit props.data.cpuLimit
), ),
lineStyle: { lineStyle: {
@@ -110,7 +115,7 @@ const initEChart = () => {
name: "内存限制MB", name: "内存限制MB",
type: "line", type: "line",
yAxisIndex: 1, yAxisIndex: 1,
data: new Array(props.data.usage.time!.length).fill( data: new Array(props.data.usage.time?.length ?? 0).fill(
props.data.memoryLimit props.data.memoryLimit
), ),
lineStyle: { lineStyle: {
@@ -132,7 +137,6 @@ const terminalComponent = ref<WsHandle | null>(null);
type ConfigHandle = { type ConfigHandle = {
openConfigDialog: () => void; openConfigDialog: () => void;
test: () => void;
}; };
const processConfigComponent = ref<ConfigHandle | null>(null); const processConfigComponent = ref<ConfigHandle | null>(null);
@@ -182,8 +186,23 @@ onMounted(() => {
const props = defineProps<{ const props = defineProps<{
data: ProcessItem; 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> </script>
<template> <template>
<div class="chart-container"> <div class="chart-container">
@@ -231,8 +250,8 @@ const props = defineProps<{
</template> </template>
<v-list nav dense> <v-list nav dense>
<v-list-item @click=""> 获取控制权 </v-list-item> <v-list-item @click="control"> 获取控制权 </v-list-item>
<v-list-item @click=""> 删除进程 </v-list-item> <v-list-item @click="del"> 删除进程 </v-list-item>
<v-list-item @click=""> 创建分享链接 </v-list-item> <v-list-item @click=""> 创建分享链接 </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
@@ -240,7 +259,7 @@ const props = defineProps<{
</div> </div>
<!-- 中间ECharts --> <!-- 中间ECharts -->
<div :id="'echarts' + props.index" class="chart"></div> <div :id="'echarts' + props.data.uuid" class="chart"></div>
<!-- 底部按钮组 + 时间 --> <!-- 底部按钮组 + 时间 -->
<div class="footer"> <div class="footer">
@@ -274,7 +293,10 @@ const props = defineProps<{
ref="terminalComponent" ref="terminalComponent"
></TerminalPty> ></TerminalPty>
<TerminalPty v-else :data="props.data"></TerminalPty> <TerminalPty v-else :data="props.data"></TerminalPty>
<ProcessConfig :data="props.data" ref="processConfigComponent"></ProcessConfig> <ProcessConfig
:data="props.data"
ref="processConfigComponent"
></ProcessConfig>
</div> </div>
</template> </template>

View File

@@ -105,9 +105,6 @@ const initPushItem = () => {
density="compact" density="compact"
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row>
<v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select <v-select
label="终端类型" label="终端类型"
@@ -118,22 +115,21 @@ const initPushItem = () => {
density="compact" density="compact"
></v-select> ></v-select>
</v-col> </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-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-divider class="my-4"></v-divider>
<v-row align="center"> <v-row align="center">

View 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>

View File

@@ -5,8 +5,8 @@ import { ProcessItem } from "~/src/types/process/process";
import { Terminal } from "xterm"; import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach"; import { AttachAddon } from "xterm-addon-attach";
import { CanvasAddon } from '@xterm/addon-canvas'; import { CanvasAddon } from "@xterm/addon-canvas";
import 'xterm/css/xterm.css'; import "xterm/css/xterm.css";
const snackbarStore = useSnackbarStore(); const snackbarStore = useSnackbarStore();
const dialog = ref(false); const dialog = ref(false);
@@ -35,7 +35,6 @@ watch(dialog, (newValue) => {
} }
}); });
const initWebSocketPty = () => { const initWebSocketPty = () => {
if (!xtermEl.value) { if (!xtermEl.value) {
snackbarStore.showErrorMessage("终端容器初始化失败"); snackbarStore.showErrorMessage("终端容器初始化失败");
@@ -46,7 +45,9 @@ const initWebSocketPty = () => {
const initialRows = Math.floor(xtermEl.value.clientHeight / 19); const initialRows = Math.floor(xtermEl.value.clientHeight / 19);
const baseUrl = `ws://${window.location.hostname}:8797/api/ws`; 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); initSocket(url);
}; };
@@ -61,6 +62,7 @@ const initSocket = (url: string) => {
socket.onclose = () => { socket.onclose = () => {
snackbarStore.showErrorMessage("终端连接断开"); snackbarStore.showErrorMessage("终端连接断开");
dialog.value = false;
}; };
socket.onerror = (err) => { socket.onerror = (err) => {
@@ -71,15 +73,15 @@ const initSocket = (url: string) => {
const initTerm = () => { const initTerm = () => {
if (!socket || !xtermEl.value) return; if (!socket || !xtermEl.value) return;
const showCursor = props.data.state.state === 3;
term = new Terminal({ term = new Terminal({
// rendererType: "canvas", // 已通过插件方式加载,此处无需设置
convertEol: true, convertEol: true,
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: showCursor,
cursorStyle: "block",
theme: { theme: {
foreground: "#ECECEC", foreground: "#ECECEC",
cursor: "help", cursor: "help"
}, },
}); });
@@ -106,6 +108,13 @@ const wsClose = () => {
cleanup(); cleanup();
}; };
const toolbarColor = computed(() => {
if (props.data.state.state == 3) {
return;
}
return "red";
});
const cleanup = () => { const cleanup = () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
if (term) { if (term) {
@@ -129,7 +138,7 @@ onUnmounted(() => {
hide-overlay hide-overlay
transition="dialog-bottom-transition" transition="dialog-bottom-transition"
v-model="dialog" v-model="dialog"
@update:modelValue="val => !val && cleanup()" @update:modelValue="(val) => !val && cleanup()"
> >
<v-card <v-card
style=" style="
@@ -141,8 +150,8 @@ onUnmounted(() => {
> >
<v-toolbar <v-toolbar
dense dense
:color="toolbarColor"
dark dark
color="blue-grey darken-4"
style="height: 35px; flex-grow: 0" style="height: 35px; flex-grow: 0"
> >
<v-toolbar-title style="height: 100%" <v-toolbar-title style="height: 100%"
@@ -155,7 +164,11 @@ onUnmounted(() => {
</v-btn> </v-btn>
</v-toolbar-items> </v-toolbar-items>
</v-toolbar> </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-card>
</v-dialog> </v-dialog>
</template> </template>

View File

@@ -1,22 +1,48 @@
<template> <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> <v-container>
<!-- 顶部工具栏 -->
<!-- 主体网格 -->
<div class="flex-grid"> <div class="flex-grid">
<div v-for="(i, v) in processData" class="responsive-box"> <div v-for="(i, v) in processData" :key="i.uuid" class="responsive-box">
<ProcessCard :data="i" :index="v"></ProcessCard> <ProcessCard :data="i" :index="v" />
</div> </div>
</div> </div>
</v-container> </v-container>
<ProcessCreate ref="processCreateComponent"></ProcessCreate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ProcessCard from "@/components/process/ProcessCard.vue"; import ProcessCard from "@/components/process/ProcessCard.vue";
import axios from "axios"; 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"; import { ProcessItem } from "~/src/types/process/process";
type CreateHandle = {
createProcessDialog: () => void;
test: () => void;
};
const processCreateComponent = ref<CreateHandle | null>(null);
const processData = ref<ProcessItem[]>(); const processData = ref<ProcessItem[]>();
const uuid: string = crypto.randomUUID(); const uuid: string = crypto.randomUUID();
const snackbarStore = useSnackbarStore();
const initProcessData = () => { const initProcessData = () => {
getProcessList().then((e) => { getProcessList().then((e) => {
processData.value = e.data!.sort((a, b) => a.name.localeCompare(b.name)); 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 = () => { const getProcessListWait = () => {
cancelTokenSource = axios.CancelToken.source(); cancelTokenSource = axios.CancelToken.source();
axios axios
@@ -51,26 +93,36 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
/* 工具栏样式 */
.toolbar {
display: flex;
justify-content: flex-end; /* 靠右对齐 */
gap: 10px;
margin-bottom: 20px;
padding: 4px 0;
border-bottom: 1px solid #eee; /* 轻量分隔线 */
}
/* 原来的网格样式 */
.flex-grid { .flex-grid {
display: flex; display: flex;
flex-wrap: wrap; /* 自动换行 */ flex-wrap: wrap;
justify-content: space-between; /* 两边与中间间距均匀 */ justify-content: space-between;
gap: 80px; /* 每个 div 之间的间距 */ gap: 80px;
} }
.responsive-box { .responsive-box {
flex: 1 1 300px; /* 最小宽度 300px */ flex: 1 1 300px;
min-width: 300px; min-width: 300px;
max-width: 100%; max-width: 100%;
background: #ffffff; /* 改为白色背景 */ background: #ffffff;
border-radius: 16px; /* 圆角 */ border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* 柔和阴影 */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
text-align: center; text-align: center;
padding: 10px; /* 内边距,让内容不贴边 */ padding: 10px;
transition: transform 0.2s ease, box-shadow 0.2s ease; /* 交互动画 */ transition: transform 0.2s ease, box-shadow 0.2s ease;
} }
/* 悬停效果 */
.responsive-box:hover { .responsive-box:hover {
transform: translateY(-6px); transform: translateY(-6px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);