feat: 新增 Frp 下载管理功能,支持版本拉取、下载、导入和删除
新增 Frp 下载相关的后端命令、前端组件和类型定义,包括: - 后端:添加 frp 模块,实现版本拉取、下载、导入、删除逻辑,新增数据库表 - 前端:新增 FrpCard 组件、下载页面,集成版本选择和下载进度展示 - 国际化:新增中英双语的 Frp 相关文案 - 依赖:添加 reqwest、futures-util、tokio 等网络和异步处理依赖
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import type { ApiResponse } from "@/types/api"
|
||||
import type { FrpDownload, FrpDownloadPayload, FrpRelease } from "@/types/frp"
|
||||
|
||||
/**
|
||||
* 调用后端命令并统一解包 ApiResponse。
|
||||
*
|
||||
* 后端所有 Frp 下载相关命令均返回 `ApiResponse<T>`,业务失败时
|
||||
* `success === false`,此处统一抛出异常,交由调用方提示。
|
||||
*/
|
||||
async function call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
const res = await invoke<ApiResponse<T>>(command, args)
|
||||
if (!res?.success) {
|
||||
throw new Error(res?.msg || "操作失败")
|
||||
}
|
||||
return res.data as T
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Frp 版本列表(返回所有架构,前端按开关过滤)
|
||||
*/
|
||||
export function getFrpReleases() {
|
||||
return call<FrpRelease[]>("get_frp_releases")
|
||||
}
|
||||
|
||||
/** 下载并解压指定 Frp 发行包 */
|
||||
export function downloadFrp(payload: FrpDownloadPayload) {
|
||||
return call<FrpDownload>("download_frp", { payload })
|
||||
}
|
||||
|
||||
/** 获取本机已下载 / 已导入的 Frp 版本列表(仪表盘选择 frpc 版本用) */
|
||||
export function getFrpDownloads() {
|
||||
return call<FrpDownload[]>("get_frp_downloads")
|
||||
}
|
||||
|
||||
/** 删除已下载 / 已导入的版本 */
|
||||
export function deleteFrp(id: number) {
|
||||
return call<null>("delete_frp", { id })
|
||||
}
|
||||
|
||||
/** 导入本地已下载的 frpc / frps(返回导入后的记录) */
|
||||
export function importFrp() {
|
||||
return call<FrpDownload[]>("import_frp")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<a-card class="frp-card" :body-style="{ padding: '16px' }" hoverable>
|
||||
<!-- 头部:版本 + 状态标签 -->
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="truncate text-base font-semibold">{{ release.version }}</span>
|
||||
<a-tag v-if="release.downloaded" color="green" class="!me-0">{{ t("frp.downloaded") }}</a-tag>
|
||||
<a-tag v-if="release.local_only" color="purple" class="!me-0">{{ t("frp.local") }}</a-tag>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2">
|
||||
<a-tag color="blue" class="!me-0">{{ release.platform }}</a-tag>
|
||||
<a-tag class="!me-0">frpc</a-tag>
|
||||
<a-tag v-if="showFrps" color="cyan" class="!me-0">frps</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 元信息:大小 / 下载次数 / 发布日期 -->
|
||||
<div class="mt-3 grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="opacity-60">{{ t("frp.size") }}</span>
|
||||
<span class="font-medium tabular-nums">{{ formatSize(release.size) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="opacity-60">{{ t("frp.downloadCount") }}</span>
|
||||
<span class="font-medium tabular-nums">{{ formatCount(release.download_count) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="opacity-60">{{ t("frp.publishedAt") }}</span>
|
||||
<span class="font-medium">{{ formatDate(release.published_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider class="!my-3" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a-button
|
||||
v-if="!release.local_only"
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="h(DownloadOutlined)"
|
||||
:loading="downloading"
|
||||
:disabled="release.downloaded"
|
||||
@click="emit('download')"
|
||||
>
|
||||
{{ release.downloaded ? t("frp.downloaded") : t("frp.download") }}
|
||||
</a-button>
|
||||
|
||||
<a-tooltip v-if="!release.local_only" :title="t('frp.copyLink')">
|
||||
<a-button size="small" :icon="h(LinkOutlined)" @click="emit('copy')" />
|
||||
</a-tooltip>
|
||||
|
||||
<a-popconfirm
|
||||
v-if="release.downloaded"
|
||||
:title="t('frp.deleteConfirm', { version: release.version, platform: release.platform })"
|
||||
:ok-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
@confirm="emit('delete')"
|
||||
>
|
||||
<a-button size="small" danger :icon="h(DeleteOutlined)" />
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
|
||||
<!-- 下载进度(卡片下方) -->
|
||||
<div v-if="progress" class="mt-3">
|
||||
<div class="mb-1 flex items-center justify-between text-xs">
|
||||
<span :class="statusClass">{{ statusText }}</span>
|
||||
<span class="tabular-nums opacity-70">{{ progress.percent }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="progress.percent"
|
||||
:status="progressStatus"
|
||||
:stroke-color="progressColor"
|
||||
:show-info="false"
|
||||
size="small"
|
||||
/>
|
||||
<p v-if="progress.status === 'error' && progress.message" class="mt-1 line-clamp-2 text-xs text-red-500">
|
||||
{{ progress.message }}
|
||||
</p>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import { h } from "vue"
|
||||
import { DeleteOutlined, DownloadOutlined, LinkOutlined } from "@ant-design/icons-vue"
|
||||
import { useI18n } from "vue-i18n"
|
||||
import type { FrpDownloadProgress, FrpRelease } from "@/types/frp"
|
||||
|
||||
const props = defineProps<{
|
||||
release: FrpRelease
|
||||
progress?: FrpDownloadProgress | null
|
||||
showFrps?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
download: []
|
||||
copy: []
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const downloading = computed(
|
||||
() => props.progress?.status === "downloading" || props.progress?.status === "extracting",
|
||||
)
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
switch (props.progress?.status) {
|
||||
case "error":
|
||||
return "exception"
|
||||
case "done":
|
||||
return "success"
|
||||
default:
|
||||
return "normal"
|
||||
}
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
switch (props.progress?.status) {
|
||||
case "error":
|
||||
return "#ef4444"
|
||||
case "done":
|
||||
return "#22c55e"
|
||||
default:
|
||||
return "#6366f1"
|
||||
}
|
||||
})
|
||||
|
||||
const statusClass = computed(() =>
|
||||
props.progress?.status === "error" ? "text-red-500" : "opacity-70",
|
||||
)
|
||||
|
||||
const statusText = computed(() =>
|
||||
props.progress ? t(`frp.status.${props.progress.status}`) : "",
|
||||
)
|
||||
|
||||
/** 字节数格式化 */
|
||||
function formatSize(bytes: number) {
|
||||
if (!bytes) return "-"
|
||||
const units = ["B", "KB", "MB", "GB"]
|
||||
let value = bytes
|
||||
let index = 0
|
||||
while (value >= 1024 && index < units.length - 1) {
|
||||
value /= 1024
|
||||
index += 1
|
||||
}
|
||||
return `${value.toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
||||
}
|
||||
|
||||
/** 下载次数格式化(过万显示为 w) */
|
||||
function formatCount(count: number) {
|
||||
if (!count) return "-"
|
||||
return count >= 10000 ? `${(count / 10000).toFixed(1)}w` : String(count)
|
||||
}
|
||||
|
||||
/** 发布日期格式化 */
|
||||
function formatDate(value: string) {
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.frp-card {
|
||||
border-color: rgba(15, 23, 42, 0.08);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.dark .frp-card {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -41,6 +41,7 @@
|
||||
"arch": "Architecture",
|
||||
"ip": "IP address",
|
||||
"frpcVersion": "Frpc version",
|
||||
"frpcVersionEmpty": "No downloaded version yet. Download one from the Frpc download page first.",
|
||||
"start": "One-click start",
|
||||
"stop": "Stop",
|
||||
"logs": "Live logs",
|
||||
@@ -180,6 +181,34 @@
|
||||
"portRange": "Port must be between 1 and 65535"
|
||||
}
|
||||
},
|
||||
"frp": {
|
||||
"refresh": "Refresh",
|
||||
"import": "Import local",
|
||||
"showAllArchFrpc": "All-arch Frpc",
|
||||
"showAllArchFrps": "All-arch Frps",
|
||||
"empty": "No Frp release data. Click Refresh to retry.",
|
||||
"downloaded": "Downloaded",
|
||||
"local": "Local",
|
||||
"download": "Download",
|
||||
"copyLink": "Copy download link",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete {version} ({platform})?",
|
||||
"size": "Size",
|
||||
"downloadCount": "Downloads",
|
||||
"publishedAt": "Published",
|
||||
"downloadSuccess": "Download completed",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"copySuccess": "Download link copied",
|
||||
"copyFailed": "Copy failed, please copy manually",
|
||||
"importSuccess": "Imported {count} file(s)",
|
||||
"importEmpty": "No file selected",
|
||||
"status": {
|
||||
"downloading": "Downloading",
|
||||
"extracting": "Extracting",
|
||||
"done": "Done",
|
||||
"error": "Download failed"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"developing": "This feature is under construction",
|
||||
"confirm": "OK",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"arch": "系统架构",
|
||||
"ip": "IP 地址",
|
||||
"frpcVersion": "Frpc 版本",
|
||||
"frpcVersionEmpty": "暂无已下载版本,请先前往 Frpc 下载页面下载",
|
||||
"start": "一键启动",
|
||||
"stop": "停止",
|
||||
"logs": "实时日志",
|
||||
@@ -180,6 +181,34 @@
|
||||
"portRange": "端口号必须在 1 - 65535 之间"
|
||||
}
|
||||
},
|
||||
"frp": {
|
||||
"refresh": "刷新",
|
||||
"import": "导入本地",
|
||||
"showAllArchFrpc": "所有架构 Frpc",
|
||||
"showAllArchFrps": "所有架构 Frps",
|
||||
"empty": "暂无 Frp 版本数据,点击刷新重试",
|
||||
"downloaded": "已下载",
|
||||
"local": "本地",
|
||||
"download": "下载",
|
||||
"copyLink": "复制下载链接",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": "确定要删除已下载的 {version}({platform})吗?",
|
||||
"size": "大小",
|
||||
"downloadCount": "下载次数",
|
||||
"publishedAt": "发布日期",
|
||||
"downloadSuccess": "下载完成",
|
||||
"deleteSuccess": "删除成功",
|
||||
"copySuccess": "下载链接已复制",
|
||||
"copyFailed": "复制失败,请手动复制",
|
||||
"importSuccess": "已导入 {count} 个文件",
|
||||
"importEmpty": "未选择任何文件",
|
||||
"status": {
|
||||
"downloading": "下载中",
|
||||
"extracting": "解压中",
|
||||
"done": "已完成",
|
||||
"error": "下载失败"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"developing": "功能开发中,敬请期待",
|
||||
"confirm": "确定",
|
||||
|
||||
Vendored
+1
-1
@@ -30,7 +30,6 @@ declare module 'vue' {
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
AResult: typeof import('ant-design-vue/es')['Result']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASegmented: typeof import('ant-design-vue/es')['Segmented']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
@@ -41,6 +40,7 @@ declare module 'vue' {
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
FrpCard: typeof import('./../components/frp/FrpCard.vue')['default']
|
||||
MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Frp 下载相关的类型定义,字段与 Rust 端 `models::FrpRelease` / `models::FrpDownload`
|
||||
* 等一一对应。
|
||||
*/
|
||||
|
||||
/** GitHub 上的 Frp 发行包(卡片展示用) */
|
||||
export interface FrpRelease {
|
||||
/** 版本号,如 v0.71.0 */
|
||||
version: string
|
||||
/** 操作系统,如 windows / darwin / linux */
|
||||
os: string
|
||||
/** CPU 架构,如 amd64 / arm64 */
|
||||
arch: string
|
||||
/** 平台标识(os_arch),如 windows_amd64 */
|
||||
platform: string
|
||||
/** 发行包文件名 */
|
||||
file_name: string
|
||||
/** 文件大小(字节) */
|
||||
size: number
|
||||
/** GitHub 下载次数 */
|
||||
download_count: number
|
||||
/** 发布日期(ISO 8601) */
|
||||
published_at: string
|
||||
/** GitHub 原始下载地址 */
|
||||
download_url: string
|
||||
/** 镜像加速后的下载地址 */
|
||||
mirror_url: string
|
||||
/** 是否已下载 / 已导入 */
|
||||
downloaded: boolean
|
||||
/** 是否匹配当前运行平台(用于前端默认过滤) */
|
||||
is_current: boolean
|
||||
/** 是否为仅本机存在的记录(未匹配到发行包) */
|
||||
local_only: boolean
|
||||
/** 本机记录 ID(用于删除) */
|
||||
local_id: number | null
|
||||
/** 本机 frpc 可执行文件路径 */
|
||||
frpc_path: string | null
|
||||
/** 本机 frps 可执行文件路径 */
|
||||
frps_path: string | null
|
||||
}
|
||||
|
||||
/** 已下载 / 已导入的 Frp 版本 */
|
||||
export interface FrpDownload {
|
||||
id: number
|
||||
version: string
|
||||
os: string
|
||||
arch: string
|
||||
platform: string
|
||||
file_name: string
|
||||
archive_path: string | null
|
||||
frpc_path: string | null
|
||||
frps_path: string | null
|
||||
file_size: number
|
||||
/** 来源:download(应用内下载)/ import(本地导入) */
|
||||
source: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** 下载请求 */
|
||||
export interface FrpDownloadPayload {
|
||||
version: string
|
||||
os: string
|
||||
arch: string
|
||||
platform: string
|
||||
file_name: string
|
||||
/** GitHub 原始下载地址(后端会拼接镜像前缀) */
|
||||
download_url: string
|
||||
}
|
||||
|
||||
/** 下载进度状态 */
|
||||
export type FrpDownloadStatus = "downloading" | "extracting" | "done" | "error"
|
||||
|
||||
/** Frp 下载进度事件 */
|
||||
export interface FrpDownloadProgress {
|
||||
/** 事件键,与发行包文件名一致 */
|
||||
key: string
|
||||
version: string
|
||||
platform: string
|
||||
/** 已下载字节数 */
|
||||
downloaded: number
|
||||
/** 总字节数(0 表示未知) */
|
||||
total: number
|
||||
/** 进度百分比(0 - 100) */
|
||||
percent: number
|
||||
status: FrpDownloadStatus
|
||||
/** 附加信息(错误原因等) */
|
||||
message: string | null
|
||||
}
|
||||
+49
-9
@@ -87,14 +87,22 @@
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="shrink-0 text-sm opacity-70">{{ t("dashboard.frpcVersion") }}</span>
|
||||
<a-select v-model:value="selectedVersion" :options="versionOptions" class="w-56" />
|
||||
<a-select
|
||||
v-model:value="selectedId"
|
||||
:options="versionOptions"
|
||||
:loading="versionsLoading"
|
||||
:placeholder="t('dashboard.frpcVersionEmpty')"
|
||||
class="w-64"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:icon="h(PlayCircleOutlined)"
|
||||
:disabled="isRunning"
|
||||
:disabled="isRunning || !selectedId"
|
||||
@click="start"
|
||||
>
|
||||
{{ t("dashboard.start") }}
|
||||
@@ -148,11 +156,13 @@ import {
|
||||
PoweroffOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import { useI18n } from "vue-i18n"
|
||||
import { getFrpDownloads } from "@/api/frp"
|
||||
import { getServerList } from "@/api/server"
|
||||
import { getSystemDetails } from "@/api/system"
|
||||
import { getTunnelList } from "@/api/tunnel"
|
||||
import { useAppStore } from "@/stores/app"
|
||||
import { useSystemStats } from "@/composables/useSystemStats"
|
||||
import type { FrpDownload } from "@/types/frp"
|
||||
import type { SystemDetails } from "@/types/system"
|
||||
import { isTauri } from "@/utils/window"
|
||||
|
||||
@@ -178,13 +188,25 @@ const ipFull = computed(() => (details.value?.ips?.length ? details.value.ips.jo
|
||||
/** 合并卡片中两个统计数值的样式(更紧凑) */
|
||||
const statValueStyle = { fontSize: "20px", fontWeight: 600, lineHeight: "1.2" }
|
||||
|
||||
/** 模拟:已下载的 Frpc 版本列表(下载功能尚未实现) */
|
||||
const versionOptions = [
|
||||
{ label: "v0.61.1 · windows_amd64", value: "v0.61.1" },
|
||||
{ label: "v0.60.0 · windows_amd64", value: "v0.60.0" },
|
||||
{ label: "v0.58.2 · windows_amd64", value: "v0.58.2" },
|
||||
]
|
||||
const selectedVersion = ref(versionOptions[0].value)
|
||||
/** 已下载 / 已导入的 Frp 版本(来自 frp_downloads) */
|
||||
const frpDownloads = ref<FrpDownload[]>([])
|
||||
const versionsLoading = ref(false)
|
||||
/** 当前选中的版本记录 ID */
|
||||
const selectedId = ref<string | undefined>(undefined)
|
||||
|
||||
/** 仅展示具备 frpc 可执行文件的记录 */
|
||||
const versionOptions = computed(() =>
|
||||
frpDownloads.value
|
||||
.filter((item) => item.frpc_path)
|
||||
.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")
|
||||
|
||||
@@ -265,10 +287,28 @@ async function loadSystemDetails() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取已下载 / 已导入的 Frp 版本,供版本下拉选择 */
|
||||
async function loadFrpDownloads() {
|
||||
if (!isTauri()) return
|
||||
versionsLoading.value = true
|
||||
try {
|
||||
frpDownloads.value = (await getFrpDownloads()) ?? []
|
||||
// 默认选中最新一个可用版本
|
||||
if (!selectedId.value && versionOptions.value.length) {
|
||||
selectedId.value = versionOptions.value[0].value
|
||||
}
|
||||
} catch {
|
||||
// 读取失败时保持空列表
|
||||
} finally {
|
||||
versionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
pushLog("INFO", t("dashboard.logReady"))
|
||||
loadCounts()
|
||||
loadSystemDetails()
|
||||
loadFrpDownloads()
|
||||
})
|
||||
|
||||
/** 进度条颜色:低=蓝,中=橙,高=红 */
|
||||
|
||||
+195
-4
@@ -1,19 +1,210 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-3">
|
||||
<!-- 顶部操作栏(独立磨砂卡片) -->
|
||||
<div class="glass-panel glass-panel-flat flex shrink-0 items-center justify-between gap-3 px-5 py-3">
|
||||
<h2 class="text-lg font-semibold">{{ t("menu.download") }}</h2>
|
||||
<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-result status="info" :sub-title="t('common.developing')" />
|
||||
<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()
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user