refactor(vnt-web): 前端重构为 Vite+Vue3+Pinia 工程并重新设计 UI
- 单文件 index.html 拆分为 vnt-web/ui(pnpm+Vite+Vue3 SFC+Pinia+Tailwind v4),构建产物输出到 static/ - UI 重新设计:浅色简洁风(可切换深色模式)、顶部导航、新增总览仪表盘首页 - 自定义弹窗/toast/确认框替换全部原生 alert/confirm,交互与过渡统一 - 响应式布局:移动端汉堡导航,表格与卡片自适应 - 实例卡片移除日志按钮;启动组网默认选中第一个配置
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
||||
import {
|
||||
getInstances,
|
||||
getInstanceInfo,
|
||||
getConfigList,
|
||||
startVntApi,
|
||||
stopVntApi,
|
||||
restartVntApi,
|
||||
deleteInstance,
|
||||
} from "../api";
|
||||
import { useUiStore } from "./ui";
|
||||
import { useStartLogStore } from "./startLog";
|
||||
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
const ui = useUiStore();
|
||||
const startLog = useStartLogStore();
|
||||
|
||||
// 多实例状态:key=file_name, value=该实例 info
|
||||
const instances = ref({});
|
||||
const instanceList = ref([]);
|
||||
const selectedInstance = ref(null);
|
||||
const configList = ref([]);
|
||||
const loadingMap = ref({});
|
||||
|
||||
// 页面可见性
|
||||
const isPageVisible = ref(!document.hidden);
|
||||
const visibilityHandler = () => {
|
||||
isPageVisible.value = !document.hidden;
|
||||
};
|
||||
|
||||
let infoTimer = null;
|
||||
|
||||
const runningCount = computed(
|
||||
() => instanceList.value.filter((i) => i.status === "running").length,
|
||||
);
|
||||
const startingCount = computed(
|
||||
() => instanceList.value.filter((i) => i.status === "starting").length,
|
||||
);
|
||||
const headerStatusText = computed(() => {
|
||||
if (runningCount.value > 0) return `运行中 x${runningCount.value}`;
|
||||
if (startingCount.value > 0) return "启动中...";
|
||||
return "未启动";
|
||||
});
|
||||
const selectedInfo = computed(() =>
|
||||
selectedInstance.value ? instances.value[selectedInstance.value] || null : null,
|
||||
);
|
||||
const selectedConfigName = computed(() => {
|
||||
if (!selectedInstance.value) return "";
|
||||
const inst = instanceList.value.find(
|
||||
(i) => i.file_name === selectedInstance.value,
|
||||
);
|
||||
return inst ? inst.config_name || inst.file_name : selectedInstance.value;
|
||||
});
|
||||
const version = computed(() => {
|
||||
for (const key in instances.value) {
|
||||
const item = instances.value[key];
|
||||
if (item && item.version) return item.version;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const isServerConnected = computed(
|
||||
() =>
|
||||
!!(
|
||||
selectedInfo.value &&
|
||||
selectedInfo.value.server_info &&
|
||||
selectedInfo.value.server_info.some((s) => s.connected)
|
||||
),
|
||||
);
|
||||
const serverStatusText = computed(() => {
|
||||
const si = selectedInfo.value;
|
||||
if (!si || !si.server_info || !si.server_info.length) return "未配置服务器";
|
||||
return `${si.server_info.filter((s) => s.connected).length} / ${si.server_info.length} 已连接`;
|
||||
});
|
||||
|
||||
const infoOf = (fileName) => instances.value[fileName] || {};
|
||||
|
||||
const fetchInstanceInfo = async (fileName) => {
|
||||
try {
|
||||
instances.value[fileName] = await getInstanceInfo(fileName);
|
||||
} catch (e) {
|
||||
console.error("Fetch info error", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchInstances = async () => {
|
||||
try {
|
||||
const list = (await getInstances()) || [];
|
||||
instanceList.value = list;
|
||||
// 清理已消失实例的 info 缓存
|
||||
for (const key of Object.keys(instances.value)) {
|
||||
if (!list.some((i) => i.file_name === key)) {
|
||||
delete instances.value[key];
|
||||
}
|
||||
}
|
||||
// 默认选中逻辑:当前选中失效时优先第一个 running,否则第一个,否则 null
|
||||
if (
|
||||
!selectedInstance.value ||
|
||||
!list.some((i) => i.file_name === selectedInstance.value)
|
||||
) {
|
||||
const running = list.find((i) => i.status === "running");
|
||||
selectedInstance.value = running
|
||||
? running.file_name
|
||||
: list.length > 0
|
||||
? list[0].file_name
|
||||
: null;
|
||||
}
|
||||
// 拉取 running 实例的详情
|
||||
for (const inst of list) {
|
||||
if (inst.status === "running") {
|
||||
fetchInstanceInfo(inst.file_name);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fetch instances error", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConfigList = async () => {
|
||||
try {
|
||||
configList.value = (await getConfigList()) || [];
|
||||
} catch (e) {
|
||||
console.error("Fetch list error", e);
|
||||
}
|
||||
};
|
||||
|
||||
const startVnt = async (fileName) => {
|
||||
if (!fileName) {
|
||||
ui.toast.error("请先选择一个配置");
|
||||
return;
|
||||
}
|
||||
if (loadingMap.value[fileName]) return;
|
||||
loadingMap.value[fileName] = true;
|
||||
try {
|
||||
await startVntApi(fileName);
|
||||
startLog.openStartLog(fileName);
|
||||
fetchInstances();
|
||||
} catch (e) {
|
||||
ui.toast.error("启动失败: " + e.message);
|
||||
} finally {
|
||||
loadingMap.value[fileName] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopVnt = async (fileName) => {
|
||||
if (!fileName || loadingMap.value[fileName]) return;
|
||||
loadingMap.value[fileName] = true;
|
||||
try {
|
||||
await stopVntApi(fileName);
|
||||
ui.toast.success("已停止");
|
||||
if (startLog.logFileName === fileName) {
|
||||
startLog.stopPolling();
|
||||
startLog.showStartLog = false;
|
||||
}
|
||||
fetchInstances();
|
||||
} catch (e) {
|
||||
ui.toast.error("停止失败: " + e.message);
|
||||
console.error(e);
|
||||
} finally {
|
||||
loadingMap.value[fileName] = false;
|
||||
}
|
||||
};
|
||||
|
||||
const restartVnt = async (fileName) => {
|
||||
if (!fileName || loadingMap.value[fileName]) return;
|
||||
loadingMap.value[fileName] = true;
|
||||
try {
|
||||
await restartVntApi(fileName);
|
||||
startLog.openStartLog(fileName);
|
||||
fetchInstances();
|
||||
} catch (e) {
|
||||
ui.toast.error("重启失败: " + e.message);
|
||||
} finally {
|
||||
loadingMap.value[fileName] = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 移除已停止(启动失败残留)的实例条目
|
||||
const dismissInstance = async (fileName) => {
|
||||
if (!fileName || loadingMap.value[fileName]) return;
|
||||
loadingMap.value[fileName] = true;
|
||||
try {
|
||||
await deleteInstance(fileName);
|
||||
ui.toast.success("已移除");
|
||||
if (startLog.logFileName === fileName) {
|
||||
startLog.stopPolling();
|
||||
startLog.showStartLog = false;
|
||||
}
|
||||
if (selectedInstance.value === fileName) {
|
||||
selectedInstance.value = null;
|
||||
}
|
||||
fetchInstances();
|
||||
} catch (e) {
|
||||
ui.toast.error("移除失败: " + e.message);
|
||||
} finally {
|
||||
loadingMap.value[fileName] = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 注入依赖,供启动日志 store 刷新实例与解析配置名
|
||||
startLog.bindApp({ fetchInstances, instanceList, configList });
|
||||
|
||||
const init = async () => {
|
||||
document.addEventListener("visibilitychange", visibilityHandler);
|
||||
await fetchInstances();
|
||||
fetchConfigList();
|
||||
// 页面加载时若有正在启动的实例,恢复其日志弹窗
|
||||
const starting = instanceList.value.find((i) => i.status === "starting");
|
||||
if (starting) startLog.openStartLog(starting.file_name);
|
||||
// 全局 3s 轮询实例列表及 running 实例详情(仅页面可见)
|
||||
infoTimer = setInterval(() => {
|
||||
if (isPageVisible.value) fetchInstances();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
document.removeEventListener("visibilitychange", visibilityHandler);
|
||||
startLog.stopPolling();
|
||||
if (infoTimer) clearInterval(infoTimer);
|
||||
};
|
||||
|
||||
// store 在组件 setup 中被使用时自动挂接生命周期
|
||||
if (getCurrentInstance()) {
|
||||
onMounted(init);
|
||||
onUnmounted(destroy);
|
||||
}
|
||||
|
||||
return {
|
||||
instances,
|
||||
instanceList,
|
||||
selectedInstance,
|
||||
configList,
|
||||
loadingMap,
|
||||
isPageVisible,
|
||||
runningCount,
|
||||
startingCount,
|
||||
headerStatusText,
|
||||
selectedInfo,
|
||||
selectedConfigName,
|
||||
version,
|
||||
isServerConnected,
|
||||
serverStatusText,
|
||||
infoOf,
|
||||
fetchInstances,
|
||||
fetchInstanceInfo,
|
||||
fetchConfigList,
|
||||
startVnt,
|
||||
stopVnt,
|
||||
restartVnt,
|
||||
dismissInstance,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user