feat: 新增 frpc 状态与日志管理的 Pinia store
实现 frpc 状态、运行版本和实时日志的全局管理,日志在组件切换后仍保留,订阅事件在应用根组件初始化一次。同时调整仪表盘页面使用 store 中的状态,启动后禁用版本选择下拉框,解决切换页面后日志清空的问题。
This commit is contained in:
@@ -104,4 +104,11 @@ Layout布局content区域的磨砂玻璃卡片圆角样式我要改动一下:
|
|||||||
|
|
||||||
- 将frpc的运行日志实时显示在仪表盘页面的日志区域内,并且可以清楚等
|
- 将frpc的运行日志实时显示在仪表盘页面的日志区域内,并且可以清楚等
|
||||||
- 仪表盘页面的停止按钮也可以停止frpc服务进程
|
- 仪表盘页面的停止按钮也可以停止frpc服务进程
|
||||||
- frpc启动避免出现frpc运行的黑色窗口
|
- frpc启动避免出现frpc运行的黑色窗口
|
||||||
|
|
||||||
|
##### 3.5.1 仪表盘功能优化调整
|
||||||
|
|
||||||
|
- frpc服务启动后,还可以在下拉列表修改frpc版本,启动后就不能再修改版本,而且下拉列表里面要显示正在使用的frpc版本
|
||||||
|
- 切换菜单之后再切回仪表盘页面,实时日志内容没了,需要保持实时日志内容不会随页面切换而清空
|
||||||
|
|
||||||
|
#### 3.6 自动更新功能开发
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { theme as antdTheme } from "ant-design-vue"
|
import { theme as antdTheme } from "ant-design-vue"
|
||||||
import { useAppStore } from "@/stores/app"
|
import { useAppStore } from "@/stores/app"
|
||||||
|
import { useFrpcStore } from "@/stores/frpc"
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const frpcStore = useFrpcStore()
|
||||||
|
|
||||||
/** 跟随明暗主题切换 Ant Design Vue 的算法 */
|
/** 跟随明暗主题切换 Ant Design Vue 的算法 */
|
||||||
const themeConfig = computed(() => ({
|
const themeConfig = computed(() => ({
|
||||||
@@ -18,6 +20,11 @@ const themeConfig = computed(() => ({
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 应用根组件初始化一次:订阅 frpc 日志/退出事件,离开仪表盘后仍持续收集
|
||||||
|
onMounted(() => {
|
||||||
|
frpcStore.init()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { defineStore } from "pinia"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { listen } from "@tauri-apps/api/event"
|
||||||
|
import { getFrpcStatus, startFrpc, stopFrpc } from "@/api/frp"
|
||||||
|
import { translate } from "@/i18n"
|
||||||
|
import { useAppStore } from "@/stores/app"
|
||||||
|
import type { FrpcExitEvent, FrpcLogEvent, FrpcStatus } from "@/types/frp"
|
||||||
|
import { isTauri } from "@/utils/window"
|
||||||
|
|
||||||
|
/** 日志级别 */
|
||||||
|
export type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG"
|
||||||
|
|
||||||
|
/** 一行实时日志 */
|
||||||
|
export interface LogLine {
|
||||||
|
time: string
|
||||||
|
level: LogLevel
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 日志最大保留行数 */
|
||||||
|
const MAX_LOGS = 500
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frpc 运行状态与实时日志。
|
||||||
|
*
|
||||||
|
* - 日志、选中版本放在 store 中,切换路由(组件卸载)后依然保留;
|
||||||
|
* - 事件监听在应用根组件初始化一次,离开仪表盘后仍持续收集日志;
|
||||||
|
* - 运行中时下拉框只展示当前使用的版本(由组件禁用下拉)。
|
||||||
|
*/
|
||||||
|
export const useFrpcStore = defineStore("frpc", () => {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const logs = ref<LogLine[]>([])
|
||||||
|
/** 下拉框选中的版本记录 ID;运行时即正在使用的版本 */
|
||||||
|
const selectedId = ref<string | undefined>(undefined)
|
||||||
|
/** 正在运行的 Frpc 版本号(用于日志区标签) */
|
||||||
|
const runningVersion = ref("")
|
||||||
|
const starting = ref(false)
|
||||||
|
const stopping = ref(false)
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
/** 是否正在运行 */
|
||||||
|
const isRunning = computed(() => appStore.runStatus === "running")
|
||||||
|
|
||||||
|
/** 追加一行日志(最多保留 MAX_LOGS 行) */
|
||||||
|
function pushLog(level: LogLevel, message: string, time?: string) {
|
||||||
|
logs.value.push({ time: time || dayjs().format("HH:mm:ss"), level, message })
|
||||||
|
if (logs.value.length > MAX_LOGS) {
|
||||||
|
logs.value.splice(0, logs.value.length - MAX_LOGS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
logs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将后端日志级别归一化为 UI 级别 */
|
||||||
|
function normalizeLevel(level: string): LogLevel {
|
||||||
|
switch (level.toUpperCase()) {
|
||||||
|
case "WARN":
|
||||||
|
case "WARNING":
|
||||||
|
return "WARN"
|
||||||
|
case "ERROR":
|
||||||
|
case "FATAL":
|
||||||
|
return "ERROR"
|
||||||
|
case "DEBUG":
|
||||||
|
case "TRACE":
|
||||||
|
return "DEBUG"
|
||||||
|
default:
|
||||||
|
return "INFO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端实时日志事件(frpc-log) */
|
||||||
|
function onBackendLog(event: FrpcLogEvent) {
|
||||||
|
pushLog(normalizeLevel(event.level), event.message, event.time ?? undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端进程退出事件(frpc-exit) */
|
||||||
|
function onBackendExit(event: FrpcExitEvent) {
|
||||||
|
runningVersion.value = ""
|
||||||
|
if (event.reason === "stopped") {
|
||||||
|
appStore.runStatus = "stopped"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appStore.runStatus = event.code != null && event.code !== 0 ? "error" : "stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用启动时调用一次:订阅后端事件并同步运行状态 */
|
||||||
|
async function init() {
|
||||||
|
if (initialized.value) return
|
||||||
|
initialized.value = true
|
||||||
|
|
||||||
|
pushLog("INFO", translate("dashboard.logReady"))
|
||||||
|
|
||||||
|
if (!isTauri()) return
|
||||||
|
|
||||||
|
// 监听在应用根组件初始化一次,长期有效(应用运行期间无需取消订阅)
|
||||||
|
await listen<FrpcLogEvent>("frpc-log", (event) => onBackendLog(event.payload))
|
||||||
|
await listen<FrpcExitEvent>("frpc-exit", (event) => onBackendExit(event.payload))
|
||||||
|
|
||||||
|
// 同步当前运行状态
|
||||||
|
try {
|
||||||
|
const status = await getFrpcStatus()
|
||||||
|
appStore.runStatus = status.running ? "running" : "stopped"
|
||||||
|
runningVersion.value = status.running ? status.version ?? "" : ""
|
||||||
|
} catch {
|
||||||
|
// 读取失败时保持默认状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一键启动:由后端生成 frpc.toml 并拉起独立进程 */
|
||||||
|
async function start(id: number): Promise<FrpcStatus> {
|
||||||
|
starting.value = true
|
||||||
|
try {
|
||||||
|
const status = await startFrpc(id)
|
||||||
|
appStore.runStatus = "running"
|
||||||
|
selectedId.value = String(id)
|
||||||
|
runningVersion.value = status.version ?? ""
|
||||||
|
return status
|
||||||
|
} finally {
|
||||||
|
starting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止 Frpc 服务进程 */
|
||||||
|
async function stop(): Promise<FrpcStatus> {
|
||||||
|
stopping.value = true
|
||||||
|
try {
|
||||||
|
const status = await stopFrpc()
|
||||||
|
appStore.runStatus = "stopped"
|
||||||
|
runningVersion.value = ""
|
||||||
|
return status
|
||||||
|
} finally {
|
||||||
|
stopping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs,
|
||||||
|
selectedId,
|
||||||
|
runningVersion,
|
||||||
|
starting,
|
||||||
|
stopping,
|
||||||
|
isRunning,
|
||||||
|
pushLog,
|
||||||
|
clearLogs,
|
||||||
|
init,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
}
|
||||||
|
})
|
||||||
+34
-125
@@ -88,9 +88,10 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="shrink-0 text-sm opacity-70">{{ t("dashboard.frpcVersion") }}</span>
|
<span class="shrink-0 text-sm opacity-70">{{ t("dashboard.frpcVersion") }}</span>
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="selectedId"
|
v-model:value="frpcStore.selectedId"
|
||||||
:options="versionOptions"
|
:options="versionOptions"
|
||||||
:loading="versionsLoading"
|
:loading="versionsLoading"
|
||||||
|
:disabled="frpcStore.isRunning || frpcStore.starting"
|
||||||
:placeholder="t('dashboard.frpcVersionEmpty')"
|
:placeholder="t('dashboard.frpcVersionEmpty')"
|
||||||
class="w-64"
|
class="w-64"
|
||||||
show-search
|
show-search
|
||||||
@@ -102,8 +103,8 @@
|
|||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:icon="h(PlayCircleOutlined)"
|
:icon="h(PlayCircleOutlined)"
|
||||||
:loading="starting"
|
:loading="frpcStore.starting"
|
||||||
:disabled="isRunning || !selectedId"
|
:disabled="frpcStore.isRunning || !frpcStore.selectedId"
|
||||||
@click="start"
|
@click="start"
|
||||||
>
|
>
|
||||||
{{ t("dashboard.start") }}
|
{{ t("dashboard.start") }}
|
||||||
@@ -111,8 +112,8 @@
|
|||||||
<a-button
|
<a-button
|
||||||
danger
|
danger
|
||||||
:icon="h(PoweroffOutlined)"
|
:icon="h(PoweroffOutlined)"
|
||||||
:loading="stopping"
|
:loading="frpcStore.stopping"
|
||||||
:disabled="!isRunning"
|
:disabled="!frpcStore.isRunning"
|
||||||
@click="stop"
|
@click="stop"
|
||||||
>
|
>
|
||||||
{{ t("dashboard.stop") }}
|
{{ t("dashboard.stop") }}
|
||||||
@@ -127,12 +128,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h3 class="text-sm font-semibold">{{ t("dashboard.logs") }}</h3>
|
<h3 class="text-sm font-semibold">{{ t("dashboard.logs") }}</h3>
|
||||||
<a-tag v-if="runningVersion" class="!me-0">{{ runningVersion }}</a-tag>
|
<a-tag v-if="frpcStore.runningVersion" class="!me-0">
|
||||||
|
{{ frpcStore.runningVersion }}
|
||||||
|
</a-tag>
|
||||||
<a-tag :color="statusTagColor" class="!me-0">{{ statusText }}</a-tag>
|
<a-tag :color="statusTagColor" class="!me-0">{{ statusText }}</a-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-tooltip :title="t('dashboard.clear')">
|
<a-tooltip :title="t('dashboard.clear')">
|
||||||
<a-button size="small" :icon="h(ClearOutlined)" @click="clearLogs" />
|
<a-button size="small" :icon="h(ClearOutlined)" @click="frpcStore.clearLogs" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -140,8 +143,8 @@
|
|||||||
ref="logBox"
|
ref="logBox"
|
||||||
class="min-h-0 flex-1 overflow-y-auto px-5 py-3 font-mono text-xs leading-relaxed"
|
class="min-h-0 flex-1 overflow-y-auto px-5 py-3 font-mono text-xs leading-relaxed"
|
||||||
>
|
>
|
||||||
<p v-if="!logs.length" class="opacity-50">{{ t("dashboard.logEmpty") }}</p>
|
<p v-if="!frpcStore.logs.length" class="opacity-50">{{ t("dashboard.logEmpty") }}</p>
|
||||||
<div v-for="(line, index) in logs" :key="index" class="flex gap-2">
|
<div v-for="(line, index) in frpcStore.logs" :key="index" class="flex gap-2">
|
||||||
<span class="shrink-0 opacity-50">{{ line.time }}</span>
|
<span class="shrink-0 opacity-50">{{ line.time }}</span>
|
||||||
<span class="shrink-0" :class="levelClass(line.level)">[{{ line.level }}]</span>
|
<span class="shrink-0" :class="levelClass(line.level)">[{{ line.level }}]</span>
|
||||||
<span class="break-all">{{ line.message }}</span>
|
<span class="break-all">{{ line.message }}</span>
|
||||||
@@ -153,7 +156,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from "vue"
|
import { h } from "vue"
|
||||||
import dayjs from "dayjs"
|
|
||||||
import {
|
import {
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
@@ -163,20 +165,21 @@ import {
|
|||||||
PoweroffOutlined,
|
PoweroffOutlined,
|
||||||
} from "@ant-design/icons-vue"
|
} from "@ant-design/icons-vue"
|
||||||
import { message } from "ant-design-vue"
|
import { message } from "ant-design-vue"
|
||||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
|
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
import { getFrpDownloads, getFrpcStatus, startFrpc, stopFrpc } from "@/api/frp"
|
import { getFrpDownloads } from "@/api/frp"
|
||||||
import { getServerList } from "@/api/server"
|
import { getServerList } from "@/api/server"
|
||||||
import { getSystemDetails } from "@/api/system"
|
import { getSystemDetails } from "@/api/system"
|
||||||
import { getTunnelList } from "@/api/tunnel"
|
import { getTunnelList } from "@/api/tunnel"
|
||||||
import { useAppStore } from "@/stores/app"
|
import { useAppStore } from "@/stores/app"
|
||||||
|
import { useFrpcStore, type LogLevel } from "@/stores/frpc"
|
||||||
import { useSystemStats } from "@/composables/useSystemStats"
|
import { useSystemStats } from "@/composables/useSystemStats"
|
||||||
import type { FrpDownload, FrpcExitEvent, FrpcLogEvent } from "@/types/frp"
|
import type { FrpDownload } from "@/types/frp"
|
||||||
import type { SystemDetails } from "@/types/system"
|
import type { SystemDetails } from "@/types/system"
|
||||||
import { isTauri } from "@/utils/window"
|
import { isTauri } from "@/utils/window"
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const frpcStore = useFrpcStore()
|
||||||
const { cpu, memory } = useSystemStats()
|
const { cpu, memory } = useSystemStats()
|
||||||
|
|
||||||
/** 服务器 / 隧道数量(来自后端,未连接 Tauri 时保持 0) */
|
/** 服务器 / 隧道数量(来自后端,未连接 Tauri 时保持 0) */
|
||||||
@@ -200,8 +203,6 @@ const statValueStyle = { fontSize: "20px", fontWeight: 600, lineHeight: "1.2" }
|
|||||||
/** 已下载 / 已导入的 Frp 版本(来自 frp_downloads) */
|
/** 已下载 / 已导入的 Frp 版本(来自 frp_downloads) */
|
||||||
const frpDownloads = ref<FrpDownload[]>([])
|
const frpDownloads = ref<FrpDownload[]>([])
|
||||||
const versionsLoading = ref(false)
|
const versionsLoading = ref(false)
|
||||||
/** 当前选中的版本记录 ID */
|
|
||||||
const selectedId = ref<string | undefined>(undefined)
|
|
||||||
|
|
||||||
/** 仅展示具备 frpc 可执行文件的记录 */
|
/** 仅展示具备 frpc 可执行文件的记录 */
|
||||||
const versionOptions = computed(() =>
|
const versionOptions = computed(() =>
|
||||||
@@ -210,116 +211,44 @@ const versionOptions = computed(() =>
|
|||||||
.map((item) => ({ label: `${item.version} · ${item.platform}`, value: String(item.id) })),
|
.map((item) => ({ label: `${item.version} · ${item.platform}`, value: String(item.id) })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedDownload = computed(
|
/** 日志容器与自动滚动 */
|
||||||
() => frpDownloads.value.find((item) => String(item.id) === selectedId.value) ?? null,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 选中的版本号(用于日志展示) */
|
|
||||||
const selectedVersion = computed(() => selectedDownload.value?.version ?? "-")
|
|
||||||
|
|
||||||
const isRunning = computed(() => appStore.runStatus === "running")
|
|
||||||
|
|
||||||
/** 当前运行的 Frpc 版本(用于日志区标签) */
|
|
||||||
const runningVersion = ref("")
|
|
||||||
/** 启动 / 停止按钮的 loading 状态 */
|
|
||||||
const starting = ref(false)
|
|
||||||
const stopping = ref(false)
|
|
||||||
|
|
||||||
type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG"
|
|
||||||
|
|
||||||
interface LogLine {
|
|
||||||
time: string
|
|
||||||
level: LogLevel
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const logs = ref<LogLine[]>([])
|
|
||||||
const logBox = ref<HTMLElement | null>(null)
|
const logBox = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
/** 追加一行日志并自动滚动到底部(最多保留 500 行) */
|
function scrollLogsToBottom() {
|
||||||
function pushLog(level: LogLevel, message: string, time?: string) {
|
|
||||||
logs.value.push({ time: time || dayjs().format("HH:mm:ss"), level, message })
|
|
||||||
if (logs.value.length > 500) {
|
|
||||||
logs.value.splice(0, logs.value.length - 500)
|
|
||||||
}
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = logBox.value
|
const el = logBox.value
|
||||||
if (el) el.scrollTop = el.scrollHeight
|
if (el) el.scrollTop = el.scrollHeight
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLogs() {
|
// 日志追加时自动滚动到底部(日志保存在 store 中,切换页面不会清空)
|
||||||
logs.value = []
|
watch(() => frpcStore.logs.length, scrollLogsToBottom)
|
||||||
}
|
|
||||||
|
|
||||||
function errMsg(e: unknown) {
|
function errMsg(e: unknown) {
|
||||||
return e instanceof Error ? e.message : String(e)
|
return e instanceof Error ? e.message : String(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将后端日志级别归一化为 UI 级别 */
|
|
||||||
function normalizeLevel(level: string): LogLevel {
|
|
||||||
switch (level.toUpperCase()) {
|
|
||||||
case "WARN":
|
|
||||||
case "WARNING":
|
|
||||||
return "WARN"
|
|
||||||
case "ERROR":
|
|
||||||
case "FATAL":
|
|
||||||
return "ERROR"
|
|
||||||
case "DEBUG":
|
|
||||||
case "TRACE":
|
|
||||||
return "DEBUG"
|
|
||||||
default:
|
|
||||||
return "INFO"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 后端实时日志事件(frpc-log) */
|
|
||||||
function onBackendLog(event: FrpcLogEvent) {
|
|
||||||
pushLog(normalizeLevel(event.level), event.message, event.time ?? undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 后端进程退出事件(frpc-exit) */
|
|
||||||
function onBackendExit(event: FrpcExitEvent) {
|
|
||||||
runningVersion.value = ""
|
|
||||||
if (event.reason === "stopped") {
|
|
||||||
appStore.runStatus = "stopped"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appStore.runStatus = event.code != null && event.code !== 0 ? "error" : "stopped"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 一键启动:由后端生成 frpc.toml 并拉起独立进程 */
|
/** 一键启动:由后端生成 frpc.toml 并拉起独立进程 */
|
||||||
async function start() {
|
async function start() {
|
||||||
if (isRunning.value || starting.value) return
|
if (frpcStore.isRunning || frpcStore.starting) return
|
||||||
if (!selectedId.value) return
|
if (!frpcStore.selectedId) return
|
||||||
|
|
||||||
starting.value = true
|
|
||||||
try {
|
try {
|
||||||
const status = await startFrpc(Number(selectedId.value))
|
await frpcStore.start(Number(frpcStore.selectedId))
|
||||||
appStore.runStatus = "running"
|
|
||||||
runningVersion.value = status.version ?? selectedVersion.value
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appStore.runStatus = "error"
|
appStore.runStatus = "error"
|
||||||
runningVersion.value = ""
|
frpcStore.pushLog("ERROR", t("dashboard.logStartFailed", { message: errMsg(e) }))
|
||||||
pushLog("ERROR", t("dashboard.logStartFailed", { message: errMsg(e) }))
|
|
||||||
message.error(errMsg(e))
|
message.error(errMsg(e))
|
||||||
} finally {
|
|
||||||
starting.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止 Frpc 服务进程 */
|
/** 停止 Frpc 服务进程 */
|
||||||
async function stop() {
|
async function stop() {
|
||||||
if (!isRunning.value || stopping.value) return
|
if (!frpcStore.isRunning || frpcStore.stopping) return
|
||||||
stopping.value = true
|
|
||||||
try {
|
try {
|
||||||
await stopFrpc()
|
await frpcStore.stop()
|
||||||
appStore.runStatus = "stopped"
|
|
||||||
runningVersion.value = ""
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(errMsg(e))
|
message.error(errMsg(e))
|
||||||
} finally {
|
|
||||||
stopping.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,9 +280,12 @@ async function loadFrpDownloads() {
|
|||||||
versionsLoading.value = true
|
versionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
frpDownloads.value = (await getFrpDownloads()) ?? []
|
frpDownloads.value = (await getFrpDownloads()) ?? []
|
||||||
// 默认选中最新一个可用版本
|
const options = versionOptions.value
|
||||||
if (!selectedId.value && versionOptions.value.length) {
|
if (!options.length) return
|
||||||
selectedId.value = versionOptions.value[0].value
|
// 运行中保持当前版本;否则已选版本失效时回退到最新一个
|
||||||
|
const valid = options.some((option) => option.value === frpcStore.selectedId)
|
||||||
|
if (!valid && !frpcStore.isRunning) {
|
||||||
|
frpcStore.selectedId = options[0].value
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 读取失败时保持空列表
|
// 读取失败时保持空列表
|
||||||
@@ -362,34 +294,11 @@ async function loadFrpDownloads() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let unlistenLog: UnlistenFn | null = null
|
onMounted(() => {
|
||||||
let unlistenExit: UnlistenFn | null = null
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
pushLog("INFO", t("dashboard.logReady"))
|
|
||||||
loadCounts()
|
loadCounts()
|
||||||
loadSystemDetails()
|
loadSystemDetails()
|
||||||
loadFrpDownloads()
|
loadFrpDownloads()
|
||||||
|
scrollLogsToBottom()
|
||||||
if (!isTauri()) return
|
|
||||||
|
|
||||||
// 订阅后端实时日志与进程退出事件
|
|
||||||
unlistenLog = await listen<FrpcLogEvent>("frpc-log", (event) => onBackendLog(event.payload))
|
|
||||||
unlistenExit = await listen<FrpcExitEvent>("frpc-exit", (event) => onBackendExit(event.payload))
|
|
||||||
|
|
||||||
// 同步当前运行状态(避免页面切换后状态丢失)
|
|
||||||
try {
|
|
||||||
const status = await getFrpcStatus()
|
|
||||||
appStore.runStatus = status.running ? "running" : "stopped"
|
|
||||||
runningVersion.value = status.running ? status.version ?? "" : ""
|
|
||||||
} catch {
|
|
||||||
// 读取失败时保持默认状态
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unlistenLog?.()
|
|
||||||
unlistenExit?.()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 进度条颜色:低=蓝,中=橙,高=红 */
|
/** 进度条颜色:低=蓝,中=橙,高=红 */
|
||||||
|
|||||||
Reference in New Issue
Block a user