From 9ca10fdd2f76dcebd25ca50046eda3307e5193b8 Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Mon, 12 Oct 2026 17:54:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20Frpilot=20?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E5=BA=94=E7=94=A8=E6=A0=B8=E5=BF=83=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=8E=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现内容: - 新增多语言支持(中文/英文),包含完整翻译文件 - 适配 Windows 和 macOS 自定义标题栏,支持窗口控制与主题切换 - 新增侧边栏菜单,显示系统 CPU/内存占用与运行状态 - 配置 Tauri 窗口属性与权限,支持窗口拖拽、最大化等操作 - 新增 Pinia 状态管理,处理主题、语言与应用状态 - 新增系统统计数据获取与模拟逻辑,支持 Tauri 后端调用回退 - 新增路由配置,完成页面布局与视图组件初始化 --- docs/Frpilot开发提示词.md | 25 ----- src-tauri/capabilities/default.json | 12 ++- src-tauri/tauri.conf.json | 9 +- src/App.vue | 19 +++- src/assets/main.css | 53 ++++++++++- src/components/MacTitleBar.vue | 87 ++++++++++++++++- src/components/TitleBarActions.vue | 128 +++++++++++++++++++++++++ src/components/WindowsTitleBar.vue | 34 ++++++- src/composables/usePlatform.ts | 23 +++++ src/composables/useSystemStats.ts | 51 ++++++++++ src/i18n/index.ts | 40 ++++++++ src/i18n/locales/en-US.json | 42 +++++++++ src/i18n/locales/zh-CN.json | 42 +++++++++ src/layout/Layout.vue | 84 +++++++++-------- src/layout/aside/Aside.vue | 141 ++++++++++++++++++++++++++++ src/layout/header/Header.vue | 17 ++++ src/main.ts | 5 + src/router/router.ts | 31 +++++- src/stores/app.ts | 56 +++++++++++ src/types/components.d.ts | 8 ++ src/utils/window.ts | 44 +++++++++ src/views/Dashboard.vue | 12 +++ src/views/Download.vue | 12 +++ src/views/Servers.vue | 12 +++ src/views/Tunnels.vue | 12 +++ 25 files changed, 907 insertions(+), 92 deletions(-) create mode 100644 src/components/TitleBarActions.vue create mode 100644 src/composables/usePlatform.ts create mode 100644 src/composables/useSystemStats.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales/en-US.json create mode 100644 src/i18n/locales/zh-CN.json create mode 100644 src/layout/header/Header.vue create mode 100644 src/stores/app.ts create mode 100644 src/utils/window.ts create mode 100644 src/views/Dashboard.vue create mode 100644 src/views/Download.vue create mode 100644 src/views/Servers.vue create mode 100644 src/views/Tunnels.vue diff --git a/docs/Frpilot开发提示词.md b/docs/Frpilot开发提示词.md index e0a4598..7b25993 100644 --- a/docs/Frpilot开发提示词.md +++ b/docs/Frpilot开发提示词.md @@ -1,27 +1,2 @@ # Frpilot开发提示词 -## 背景 - -> 该项目使用Tauri2 + Vue3为主要技术栈,主要解决Frpc配置繁琐的问题,方便用户快速配置frpc版本、隧道、服务器等 -> -> UI库:Antdv - -## 阶段提示词 - -### 1. Layout布局 - -本项目打算做一个Frpc的可视化客户端,方便用户快速配置frpc版本、隧道、服务器等,现在帮我完成以下功能: - -1. i18n国际化初始化配置 - - 在locales目录中写多语言翻译json文件,初步支持中文和英文 -2. TitleBar页面开发 - - 开发不同操作系统类型的自定义titlebar - - Windows系统的TitleBar最左侧显示Frpilot标题,右侧显示语言切换、明暗切换、设置、最小化、关闭,按照顺序展示; - - Mac系统的TitleBar最左侧显示红绿灯操作,右侧显示语言切换、明暗切换、设置、最小化、关闭,按照顺序展示; -3. Layout页面布局开发 - - Aside侧边栏开发,在Aside.vue文件里面编写,左侧菜单初步暂定为仪表盘、服务器、隧道列表、Frpc下载 - - Aside侧边栏最底部显示CPU占用(百分比)、内存占用(百分比)、运行状态(绿色点运行正常,红色点运行异常,黄色点未启动),跟上方的菜单有间隔 - - Header顶部栏,根据操作系统类型引入components里面的WindowsTitleBar.vue或MacTitleBar.vue -4. Layout布局的side、content区域全部采用圆角矩形、类似于悬浮磨砂玻璃效果 - -开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发 \ No newline at end of file diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c1572c3..896755b 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,10 +6,16 @@ "permissions": [ "core:default", "opener:default", - "core:window:default", - "core:window:allow-close", + "core:window:allow-start-dragging", "core:window:allow-minimize", + "core:window:allow-unminimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", "core:window:allow-toggle-maximize", - "core:window:allow-start-dragging" + "core:window:allow-is-maximized", + "core:window:allow-close", + "core:window:allow-hide", + "core:window:allow-show", + "core:window:allow-set-focus" ] } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 62a4573..54bac56 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,9 +13,12 @@ "windows": [ { "title": "Frpilot", - "width": 800, - "height": 500, - "decorations": false + "width": 1100, + "height": 720, + "minWidth": 900, + "minHeight": 600, + "decorations": false, + "transparent": false } ], "security": { diff --git a/src/App.vue b/src/App.vue index 6d8ef9c..0bbfbee 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,14 +1,23 @@ - \ No newline at end of file + diff --git a/src/assets/main.css b/src/assets/main.css index 51583b9..c347e20 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -1,6 +1,49 @@ -@import 'tailwindcss' +@import 'tailwindcss'; -:root { - /* 全局颜色 */ - --color-bg: #f5f5f5; -} \ No newline at end of file +/* 使用 class 方式控制暗色模式(配合 @vueuse/core 的 useDark) */ +@custom-variant dark (&:where(.dark, .dark *)); + +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +body { + overflow: hidden; + font-family: 'Inter', 'HarmonyOS Sans SC', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* 悬浮磨砂玻璃面板 */ +.glass-panel { + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.55); + background: rgba(255, 255, 255, 0.55); + backdrop-filter: blur(18px) saturate(160%); + -webkit-backdrop-filter: blur(18px) saturate(160%); + box-shadow: 0 8px 32px rgba(15, 23, 42, 0.12); +} + +.dark .glass-panel { + border-color: rgba(255, 255, 255, 0.08); + background: rgba(15, 23, 42, 0.55); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +/* 细滚动条 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-thumb { + border-radius: 9999px; + background: rgba(100, 116, 139, 0.35); +} + +::-webkit-scrollbar-track { + background: transparent; +} diff --git a/src/components/MacTitleBar.vue b/src/components/MacTitleBar.vue index 2f7fe5c..4f66a4d 100644 --- a/src/components/MacTitleBar.vue +++ b/src/components/MacTitleBar.vue @@ -1,13 +1,92 @@ - \ No newline at end of file + &:active { + filter: brightness(0.85); + } +} + +/* 窗口失焦时红绿灯置灰(模拟 macOS 行为) */ +.traffic-light--idle { + background-color: #cbd5e1 !important; +} + +.light-icon { + height: 8px; + width: 8px; + stroke: rgba(0, 0, 0, 0.6); + stroke-width: 1.4; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + transition: opacity 0.15s ease; +} + diff --git a/src/components/TitleBarActions.vue b/src/components/TitleBarActions.vue new file mode 100644 index 0000000..cd92528 --- /dev/null +++ b/src/components/TitleBarActions.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/components/WindowsTitleBar.vue b/src/components/WindowsTitleBar.vue index 2f7fe5c..b5a87b5 100644 --- a/src/components/WindowsTitleBar.vue +++ b/src/components/WindowsTitleBar.vue @@ -1,13 +1,37 @@ - \ No newline at end of file + diff --git a/src/composables/usePlatform.ts b/src/composables/usePlatform.ts new file mode 100644 index 0000000..efff9ff --- /dev/null +++ b/src/composables/usePlatform.ts @@ -0,0 +1,23 @@ +/** 操作系统类型 */ +export type Platform = "windows" | "macos" | "linux" | "unknown" + +/** + * 判断当前运行的操作系统类型。 + * 通过 navigator.userAgent 推断,避免额外引入 @tauri-apps/plugin-os。 + */ +export function usePlatform() { + const ua = (navigator.userAgent || "").toLowerCase() + + const platform = computed(() => { + if (ua.includes("mac") || ua.includes("darwin")) return "macos" + if (ua.includes("win")) return "windows" + if (ua.includes("linux") || ua.includes("x11")) return "linux" + return "unknown" + }) + + const isMac = computed(() => platform.value === "macos") + const isWindows = computed(() => platform.value === "windows") + const isLinux = computed(() => platform.value === "linux") + + return { platform, isMac, isWindows, isLinux } +} diff --git a/src/composables/useSystemStats.ts b/src/composables/useSystemStats.ts new file mode 100644 index 0000000..bff73fe --- /dev/null +++ b/src/composables/useSystemStats.ts @@ -0,0 +1,51 @@ +import { invoke } from "@tauri-apps/api/core" +import { isTauri } from "@/utils/window" + +interface SystemInfo { + cpu: number + memory: number +} + +/** + * 系统资源占用(CPU / 内存 百分比)。 + * + * 优先调用后端命令 `get_system_info`(返回 { cpu, memory }), + * 后端尚未实现时回退到模拟数据,保证界面可用。 + */ +export function useSystemStats(interval = 2000) { + const cpu = ref(0) + const memory = ref(0) + const isMock = ref(true) + + // 模拟数据:随机游走,避免界面显示为静态 0 + let mockCpu = 18 + let mockMem = 42 + function mock() { + mockCpu = Math.min(95, Math.max(3, mockCpu + (Math.random() - 0.5) * 8)) + mockMem = Math.min(95, Math.max(10, mockMem + (Math.random() - 0.5) * 4)) + cpu.value = Math.round(mockCpu) + memory.value = Math.round(mockMem) + isMock.value = true + } + + async function refresh() { + if (isTauri()) { + try { + const res = await invoke("get_system_info") + if (res) { + cpu.value = Math.round(res.cpu) + memory.value = Math.round(res.memory) + isMock.value = false + return + } + } catch { + // 后端命令未实现,回退到模拟数据 + } + } + mock() + } + + const { pause, resume } = useIntervalFn(refresh, interval, { immediate: true }) + + return { cpu, memory, isMock, refresh, pause, resume } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..20542e7 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,40 @@ +import { createI18n } from "vue-i18n" +import zhCN from "./locales/zh-CN.json" +import enUS from "./locales/en-US.json" + +/** 支持的语言类型 */ +export type Language = "zh-CN" | "en-US" + +/** 全部语言包 */ +export const messages = { + "zh-CN": zhCN, + "en-US": enUS, +} + +/** 可选语言列表(用于语言切换下拉) */ +export const languageOptions: { key: Language; label: string }[] = [ + { key: "zh-CN", label: "简体中文" }, + { key: "en-US", label: "English" }, +] + +export const DEFAULT_LANGUAGE: Language = "zh-CN" + +export const i18n = createI18n({ + legacy: false, // 使用组合式 API + globalInjection: true, // 允许在模板中使用 $t + locale: DEFAULT_LANGUAGE, + fallbackLocale: "en-US", + messages, +}) + +/** 切换全局语言 */ +export function setI18nLanguage(language: Language) { + i18n.global.locale.value = language +} + +/** 在非组件环境(如路由守卫)中获取翻译函数 */ +export function translate(key: string) { + return i18n.global.t(key) +} + +export default i18n diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json new file mode 100644 index 0000000..13ee604 --- /dev/null +++ b/src/i18n/locales/en-US.json @@ -0,0 +1,42 @@ +{ + "app": { + "name": "Frpilot", + "description": "A visual client for frpc" + }, + "titlebar": { + "language": "Language", + "theme": "Theme", + "lightMode": "Light mode", + "darkMode": "Dark mode", + "settings": "Settings", + "minimize": "Minimize", + "maximize": "Maximize", + "restore": "Restore", + "close": "Close" + }, + "language": { + "zhCN": "简体中文", + "enUS": "English" + }, + "menu": { + "dashboard": "Dashboard", + "servers": "Servers", + "tunnels": "Tunnels", + "download": "Download frpc" + }, + "system": { + "cpu": "CPU usage", + "memory": "Memory usage", + "status": "Status", + "statusRunning": "Running", + "statusError": "Error", + "statusStopped": "Stopped" + }, + "settings": { + "title": "Settings", + "placeholder": "The settings page is under construction." + }, + "common": { + "developing": "This feature is under construction" + } +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json new file mode 100644 index 0000000..9214e31 --- /dev/null +++ b/src/i18n/locales/zh-CN.json @@ -0,0 +1,42 @@ +{ + "app": { + "name": "Frpilot", + "description": "Frpc 可视化客户端" + }, + "titlebar": { + "language": "语言", + "theme": "主题", + "lightMode": "浅色模式", + "darkMode": "深色模式", + "settings": "设置", + "minimize": "最小化", + "maximize": "最大化", + "restore": "还原", + "close": "关闭" + }, + "language": { + "zhCN": "简体中文", + "enUS": "English" + }, + "menu": { + "dashboard": "仪表盘", + "servers": "服务器", + "tunnels": "隧道列表", + "download": "Frpc 下载" + }, + "system": { + "cpu": "CPU 占用", + "memory": "内存占用", + "status": "运行状态", + "statusRunning": "运行正常", + "statusError": "运行异常", + "statusStopped": "未启动" + }, + "settings": { + "title": "设置", + "placeholder": "设置页面开发中,敬请期待。" + }, + "common": { + "developing": "功能开发中,敬请期待" + } +} diff --git a/src/layout/Layout.vue b/src/layout/Layout.vue index ee04717..64a7dd5 100644 --- a/src/layout/Layout.vue +++ b/src/layout/Layout.vue @@ -1,47 +1,51 @@ - \ No newline at end of file + diff --git a/src/layout/aside/Aside.vue b/src/layout/aside/Aside.vue index e69de29..eabbbcc 100644 --- a/src/layout/aside/Aside.vue +++ b/src/layout/aside/Aside.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/layout/header/Header.vue b/src/layout/header/Header.vue new file mode 100644 index 0000000..1ae3af1 --- /dev/null +++ b/src/layout/header/Header.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/main.ts b/src/main.ts index d51c813..4ee5b9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { createApp } from "vue" +import { createPinia } from "pinia" import App from "./App.vue" import "@/assets/main.css" // 引入 Ant Design Vue 组件库 @@ -6,8 +7,12 @@ import Antd from "ant-design-vue" import "ant-design-vue/dist/reset.css" // 引入路由 import router from "@/router/router" +// 引入国际化 +import i18n from "@/i18n" const app = createApp(App) +app.use(createPinia()) app.use(Antd) +app.use(i18n) app.use(router) app.mount("#app") diff --git a/src/router/router.ts b/src/router/router.ts index 28f2373..1a7e48c 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -6,7 +6,34 @@ const routes: RouteRecordRaw[] = [ path: "/", name: "Layout", component: () => import("@/layout/Layout.vue"), - } + redirect: "/dashboard", + children: [ + { + path: "dashboard", + name: "Dashboard", + component: () => import("@/views/Dashboard.vue"), + meta: { title: "menu.dashboard" }, + }, + { + path: "servers", + name: "Servers", + component: () => import("@/views/Servers.vue"), + meta: { title: "menu.servers" }, + }, + { + path: "tunnels", + name: "Tunnels", + component: () => import("@/views/Tunnels.vue"), + meta: { title: "menu.tunnels" }, + }, + { + path: "download", + name: "Download", + component: () => import("@/views/Download.vue"), + meta: { title: "menu.download" }, + }, + ], + }, ] const router = createRouter({ @@ -14,4 +41,4 @@ const router = createRouter({ routes, }) -export default router \ No newline at end of file +export default router diff --git a/src/stores/app.ts b/src/stores/app.ts new file mode 100644 index 0000000..9dbbcfb --- /dev/null +++ b/src/stores/app.ts @@ -0,0 +1,56 @@ +import { defineStore } from "pinia" +import zhCN from "ant-design-vue/es/locale/zh_CN" +import enUS from "ant-design-vue/es/locale/en_US" +import { setI18nLanguage, type Language } from "@/i18n" + +/** Frpc 运行状态 */ +export type RunStatus = "running" | "error" | "stopped" + +/** + * 全局应用配置:语言、主题、Frpc 运行状态 + */ +export const useAppStore = defineStore("app", () => { + // 语言(持久化到 localStorage) + const language = useStorage("frpilot:language", "zh-CN") + + // 明暗主题:useDark 会在 上切换 .dark 类名 + const isDark = useDark({ + storageKey: "frpilot:theme", + selector: "html", + attribute: "class", + valueDark: "dark", + valueLight: "", + }) + const toggleDark = useToggle(isDark) + + // Frpc 运行状态(后续由后端驱动) + const runStatus = ref("stopped") + + // Ant Design Vue 组件库语言包 + const antdLocale = computed(() => (language.value === "zh-CN" ? zhCN : enUS)) + + // 语言变化时同步到 vue-i18n + watch( + language, + (value) => setI18nLanguage(value), + { immediate: true }, + ) + + function setLanguage(value: Language) { + language.value = value + } + + function toggleLanguage() { + language.value = language.value === "zh-CN" ? "en-US" : "zh-CN" + } + + return { + language, + isDark, + toggleDark, + runStatus, + antdLocale, + setLanguage, + toggleLanguage, + } +}) diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 75f5706..f6768f0 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -11,16 +11,24 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AButton: typeof import('ant-design-vue/es')['Button'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] + ADropdown: typeof import('ant-design-vue/es')['Dropdown'] ALayout: typeof import('ant-design-vue/es')['Layout'] ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] + AMenu: typeof import('ant-design-vue/es')['Menu'] + AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] + AModal: typeof import('ant-design-vue/es')['Modal'] + AProgress: typeof import('ant-design-vue/es')['Progress'] + AResult: typeof import('ant-design-vue/es')['Result'] ASpace: typeof import('ant-design-vue/es')['Space'] MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + TitleBarActions: typeof import('./../components/TitleBarActions.vue')['default'] WindowsTitleBar: typeof import('./../components/WindowsTitleBar.vue')['default'] } } diff --git a/src/utils/window.ts b/src/utils/window.ts new file mode 100644 index 0000000..d589bd5 --- /dev/null +++ b/src/utils/window.ts @@ -0,0 +1,44 @@ +/** + * Tauri 窗口控制封装。 + * 在纯浏览器环境(非 Tauri)下调用会被安全忽略,方便 `pnpm dev` 调试。 + */ + +export function isTauri() { + return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window +} + +async function currentWindow() { + if (!isTauri()) return null + const { getCurrentWindow } = await import("@tauri-apps/api/window") + return getCurrentWindow() +} + +/** 最小化窗口 */ +export async function minimizeWindow() { + const win = await currentWindow() + await win?.minimize() +} + +/** 最大化 / 还原窗口 */ +export async function toggleMaximizeWindow() { + const win = await currentWindow() + await win?.toggleMaximize() +} + +/** 关闭窗口 */ +export async function closeWindow() { + const win = await currentWindow() + await win?.close() +} + +/** 开始拖拽窗口(配合 data-tauri-drag-region 使用) */ +export async function startDragging() { + const win = await currentWindow() + await win?.startDragging() +} + +/** 是否已最大化 */ +export async function isWindowMaximized() { + const win = await currentWindow() + return (await win?.isMaximized()) ?? false +} diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue new file mode 100644 index 0000000..e1c2436 --- /dev/null +++ b/src/views/Dashboard.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/views/Download.vue b/src/views/Download.vue new file mode 100644 index 0000000..2c7d5e3 --- /dev/null +++ b/src/views/Download.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/views/Servers.vue b/src/views/Servers.vue new file mode 100644 index 0000000..8552bbd --- /dev/null +++ b/src/views/Servers.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/views/Tunnels.vue b/src/views/Tunnels.vue new file mode 100644 index 0000000..ccbba95 --- /dev/null +++ b/src/views/Tunnels.vue @@ -0,0 +1,12 @@ + + +