Files
vnt/vnt-web/ui/src/components/InstanceCard.vue
T
lbl 7513465b61 refactor(vnt-web): 前端重构为 Vite+Vue3+Pinia 工程并重新设计 UI
- 单文件 index.html 拆分为 vnt-web/ui(pnpm+Vite+Vue3 SFC+Pinia+Tailwind v4),构建产物输出到 static/
- UI 重新设计:浅色简洁风(可切换深色模式)、顶部导航、新增总览仪表盘首页
- 自定义弹窗/toast/确认框替换全部原生 alert/confirm,交互与过渡统一
- 响应式布局:移动端汉堡导航,表格与卡片自适应
- 实例卡片移除日志按钮;启动组网默认选中第一个配置
2026-08-22 00:53:51 +08:00

131 lines
3.8 KiB
Vue

<script setup>
import { computed } from "vue";
import { useAppStore } from "../stores/app";
import { useUiStore } from "../stores/ui";
const props = defineProps({
inst: { type: Object, required: true },
// 是否显示选中高亮(实例页用)
selectable: { type: Boolean, default: false },
});
const app = useAppStore();
const ui = useUiStore();
const info = computed(() => app.infoOf(props.inst.file_name));
const loading = computed(() => !!app.loadingMap[props.inst.file_name]);
const statusBadgeClass = (status) =>
status === "running" ? "badge-green" : status === "starting" ? "badge-blue" : "badge-gray";
const statusText = (status) =>
status === "running" ? "运行中" : status === "starting" ? "启动中" : "已停止";
const select = () => {
if (props.selectable) app.selectedInstance = props.inst.file_name;
};
const confirmStop = async () => {
const ok = await ui.confirm({
title: "停止组网",
message: `确定要停止 ${props.inst.file_name} 吗?`,
danger: true,
confirmText: "停止",
});
if (ok) app.stopVnt(props.inst.file_name);
};
const confirmRestart = async () => {
const ok = await ui.confirm({
title: "重启组网",
message: `确定要重启 ${props.inst.file_name} 吗?`,
});
if (ok) app.restartVnt(props.inst.file_name);
};
const confirmDismiss = async () => {
const ok = await ui.confirm({
title: "移除实例",
message: `确定要移除已停止的实例 ${props.inst.file_name} 吗?`,
danger: true,
confirmText: "移除",
});
if (ok) app.dismissInstance(props.inst.file_name);
};
</script>
<template>
<div
class="card"
:class="[
selectable ? 'cursor-pointer' : '',
selectable && app.selectedInstance === inst.file_name
? 'ring-2 ring-indigo-500 dark:ring-indigo-400'
: '',
]"
@click="select"
>
<div class="flex items-center justify-between gap-2">
<h3 class="truncate text-base font-bold text-slate-900 dark:text-white" :title="inst.file_name">
{{ inst.config_name || inst.file_name }}
</h3>
<span class="shrink-0" :class="statusBadgeClass(inst.status)">{{ statusText(inst.status) }}</span>
</div>
<div class="mt-2 text-sm">
<span class="muted">虚拟 IP:</span>
<span class="ml-1 font-mono tabular-nums text-indigo-600 dark:text-indigo-400">{{
info.ip || "-"
}}</span>
</div>
<div class="mt-3 flex gap-4 text-xs muted">
<span>
在线
<span class="font-bold tabular-nums text-blue-600 dark:text-blue-400">{{
info.online_client_num || 0
}}</span>
</span>
<span>
直连
<span class="font-bold tabular-nums text-green-600 dark:text-green-400">{{
info.direct_client_num || 0
}}</span>
</span>
<span>
离线
<span class="font-bold tabular-nums text-slate-400">{{ info.offline_client_num || 0 }}</span>
</span>
</div>
<div class="mt-4 flex flex-wrap justify-end gap-2">
<button
v-if="inst.status === 'running' || inst.status === 'stopped'"
class="btn-primary btn-sm"
:disabled="loading"
@click.stop="confirmRestart"
>
<span v-if="loading" class="animate-spin"></span>
{{ inst.status === "stopped" ? "重新启动" : "重启" }}
</button>
<button
v-if="inst.status !== 'stopped'"
class="btn-danger btn-sm"
:disabled="loading"
@click.stop="confirmStop"
>
<span v-if="loading" class="animate-spin"></span>
停止
</button>
<button
v-if="inst.status === 'stopped'"
class="btn-ghost btn-sm"
:disabled="loading"
@click.stop="confirmDismiss"
>
<span v-if="loading" class="animate-spin"></span>
移除
</button>
</div>
</div>
</template>