273 lines
8.0 KiB
JavaScript
273 lines
8.0 KiB
JavaScript
import { defineStore } from "pinia";
|
|
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
|
import {
|
|
getInstances,
|
|
getInstanceInfo,
|
|
getConfigList,
|
|
getVersion,
|
|
startVntApi,
|
|
stopVntApi,
|
|
restartVntApi,
|
|
deleteInstance,
|
|
} from "../api";
|
|
import { useUiStore } from "./ui";
|
|
import { useStartLogStore } from "./startLog";
|
|
import { getAccessToken, isDesktop } from "../auth";
|
|
|
|
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({});
|
|
// 停止中的实例:key=file_name;点击停止后置位,实例从列表消失或变为 stopped 时清除
|
|
const stoppingMap = 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;
|
|
});
|
|
// 客户端版本号,来自 /api/version,与组网状态无关
|
|
const version = ref("");
|
|
|
|
const fetchVersion = async () => {
|
|
try {
|
|
version.value = (await getVersion()) || "";
|
|
} catch (e) {
|
|
console.error("Fetch version error", e);
|
|
}
|
|
};
|
|
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];
|
|
}
|
|
}
|
|
// 停止已生效(实例消失或进入 stopped)时清除停止中标记
|
|
for (const key of Object.keys(stoppingMap.value)) {
|
|
const inst = list.find((i) => i.file_name === key);
|
|
if (!inst || inst.status === "stopped") {
|
|
delete stoppingMap.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;
|
|
stoppingMap.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);
|
|
// 停止失败,恢复可操作状态
|
|
delete stoppingMap.value[fileName];
|
|
} 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 () => {
|
|
if (!isDesktop && !getAccessToken()) return;
|
|
document.addEventListener("visibilitychange", visibilityHandler);
|
|
fetchVersion();
|
|
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,
|
|
stoppingMap,
|
|
isPageVisible,
|
|
runningCount,
|
|
startingCount,
|
|
headerStatusText,
|
|
selectedInfo,
|
|
selectedConfigName,
|
|
version,
|
|
isServerConnected,
|
|
serverStatusText,
|
|
infoOf,
|
|
fetchInstances,
|
|
fetchInstanceInfo,
|
|
fetchConfigList,
|
|
startVnt,
|
|
stopVnt,
|
|
restartVnt,
|
|
dismissInstance,
|
|
};
|
|
});
|