Files
Frpilot/src/views/Download.vue
T
Yuhang Wu 57b4d7be5a feat: 为Frpc下载页面添加运行时缓存,避免重复请求触发限流
扩展路由元信息类型以支持页面缓存开关,在Layout中实现基于路由元的keep-alive逻辑,为Download组件设置对应名称和缓存标识,仅在应用运行时缓存页面,刷新仍需手动触发更新。
2026-10-16 13:44:20 +08:00

214 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex h-full flex-col gap-3">
<!-- 顶部操作栏(独立磨砂卡片) -->
<div
class="glass-panel glass-panel-flat flex shrink-0 flex-wrap items-center justify-between gap-3 px-5 py-3"
>
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">{{ t("menu.download") }}</h2>
<a-tag v-if="releases.length" class="!me-0">{{ releases.length }}</a-tag>
</div>
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
<label class="flex cursor-pointer items-center gap-2 text-xs">
<a-switch v-model:checked="showAllArchFrpc" size="small" />
<span>{{ t("frp.showAllArchFrpc") }}</span>
</label>
<label class="flex cursor-pointer items-center gap-2 text-xs">
<a-switch v-model:checked="showAllArchFrps" size="small" />
<span>{{ t("frp.showAllArchFrps") }}</span>
</label>
<a-space>
<a-button :loading="loading" :icon="h(ReloadOutlined)" @click="refresh()">
{{ t("frp.refresh") }}
</a-button>
<a-button :loading="importing" :icon="h(ImportOutlined)" @click="onImport">
{{ t("frp.import") }}
</a-button>
</a-space>
</div>
</div>
<!-- 内容区(独立磨砂卡片,内部滚动) -->
<div class="glass-panel glass-panel-flat min-h-0 flex-1 overflow-y-auto p-5">
<a-spin :spinning="loading">
<a-empty v-if="!releases.length" class="mt-20" :description="t('frp.empty')" />
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<FrpCard
v-for="item in releases"
:key="`${item.version}-${item.platform}-${item.file_name}`"
:release="item"
:progress="progressMap[item.file_name]"
:show-frps="showAllArchFrps"
@download="onDownload(item)"
@copy="onCopy(item)"
@delete="onDelete(item)"
/>
</div>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { h } from "vue"
import { ImportOutlined, ReloadOutlined } from "@ant-design/icons-vue"
import { message } from "ant-design-vue"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { useI18n } from "vue-i18n"
import { deleteFrp, downloadFrp, getFrpReleases, importFrp } from "@/api/frp"
import FrpCard from "@/components/frp/FrpCard.vue"
import type { FrpDownloadProgress, FrpRelease } from "@/types/frp"
import { isTauri } from "@/utils/window"
const { t } = useI18n()
// 组件名需与路由 name 一致,以便被 Layout 的 <keep-alive :include> 缓存
defineOptions({ name: "Download" })
const loading = ref(false)
const importing = ref(false)
/** 是否展示所有架构的 Frpc */
const showAllArchFrpc = useStorage("frpilot:frp:allArchFrpc", false)
/** 是否作为 frp 下载器展示所有架构的 Frps */
const showAllArchFrps = useStorage("frpilot:frp:allArchFrps", false)
/** 后端返回的全部发行包(含所有架构) */
const allReleases = ref<FrpRelease[]>([])
/**
* 按开关过滤后的展示列表:
* 默认仅展示当前运行平台(以及已下载的记录);开启任一开关后展示所有架构。
*/
const releases = computed(() => {
if (showAllArchFrpc.value || showAllArchFrps.value) return allReleases.value
return allReleases.value.filter((item) => item.is_current || item.downloaded)
})
/** 各发行包的下载进度(以文件名为键) */
const progressMap = ref<Record<string, FrpDownloadProgress>>({})
function errMsg(e: unknown) {
return e instanceof Error ? e.message : String(e)
}
/** 拉取 Frp 版本列表,silent 为 true 时静默刷新(不显示 loading) */
async function refresh(silent = false) {
if (!isTauri()) return
if (!silent) loading.value = true
try {
allReleases.value = (await getFrpReleases()) ?? []
} catch (e) {
message.error(errMsg(e))
} finally {
if (!silent) loading.value = false
}
}
async function onDownload(item: FrpRelease) {
if (!item.download_url) return
progressMap.value[item.file_name] = {
key: item.file_name,
version: item.version,
platform: item.platform,
downloaded: 0,
total: item.size,
percent: 0,
status: "downloading",
message: null,
}
try {
const row = await downloadFrp({
version: item.version,
os: item.os,
arch: item.arch,
platform: item.platform,
file_name: item.file_name,
download_url: item.download_url,
})
// 直接更新本地状态,避免重复请求 GitHub API
item.downloaded = true
item.local_id = row.id
item.frpc_path = row.frpc_path
item.frps_path = row.frps_path
message.success(t("frp.downloadSuccess"))
} catch (e) {
message.error(errMsg(e))
}
}
async function onCopy(item: FrpRelease) {
const link = item.mirror_url || item.download_url
if (!link) return
try {
await navigator.clipboard.writeText(link)
message.success(t("frp.copySuccess"))
} catch {
message.error(t("frp.copyFailed"))
}
}
async function onDelete(item: FrpRelease) {
if (item.local_id == null) return
try {
await deleteFrp(item.local_id)
message.success(t("frp.deleteSuccess"))
if (item.local_only) {
// 仅本机的记录直接移除
allReleases.value = allReleases.value.filter((it) => it !== item)
} else {
item.downloaded = false
item.local_id = null
item.frpc_path = null
item.frps_path = null
}
} catch (e) {
message.error(errMsg(e))
}
}
async function onImport() {
importing.value = true
try {
const rows = await importFrp()
if (rows?.length) {
message.success(t("frp.importSuccess", { count: rows.length }))
} else {
message.info(t("frp.importEmpty"))
}
await refresh(true)
} catch (e) {
message.error(errMsg(e))
} finally {
importing.value = false
}
}
/** 下载进度事件:更新对应卡片的进度条 */
function onProgress(p: FrpDownloadProgress) {
progressMap.value[p.key] = p
if (p.status === "done") {
setTimeout(() => {
delete progressMap.value[p.key]
}, 1500)
}
}
let unlisten: UnlistenFn | null = null
onMounted(async () => {
refresh()
if (isTauri()) {
unlisten = await listen<FrpDownloadProgress>("frp-download-progress", (event) =>
onProgress(event.payload),
)
}
})
onUnmounted(() => unlisten?.())
</script>