From bc4da1c7a480fabf7895bd30635d5e182f9fdbaf Mon Sep 17 00:00:00 2001 From: Yuhang Wu Date: Wed, 14 Oct 2026 10:40:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Markdown=20?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E7=BB=84=E4=BB=B6=E5=B9=B6=E9=80=82=E9=85=8D?= =?UTF-8?q?=20macOS=20=E6=A0=87=E9=A2=98=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MarkdownBlock 组件实现更新日志的 Markdown 渲染 - 新增 markdown 工具函数和样式 - 适配 macOS 标题栏,隐藏最小化/关闭按钮并禁用最大化按钮 - 新增 1.0.0 版本更新日志文档 --- docs/CHANGELOG.md | 39 +++++++ package.json | 3 +- pnpm-lock.yaml | 50 +++++++++ src/assets/main.css | 162 +++++++++++++++++++++++++++++ src/components/MacTitleBar.vue | 21 ++-- src/components/MarkdownBlock.vue | 29 ++++++ src/components/TitleBarActions.vue | 8 +- src/components/UpdateModal.vue | 12 ++- src/components/WindowsTitleBar.vue | 2 +- src/i18n/locales/en-US.json | 1 + src/i18n/locales/zh-CN.json | 1 + src/types/components.d.ts | 1 + src/utils/markdown.ts | 31 ++++++ 13 files changed, 344 insertions(+), 16 deletions(-) create mode 100644 src/components/MarkdownBlock.vue create mode 100644 src/utils/markdown.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e69de29..7864017 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -0,0 +1,39 @@ +# 更新日志 + +本文件记录 Frpilot 各版本的更新内容。 + +--- + +## v1.0.0(2026-10-14) + +### 🎉 首个正式版本发布! + +Frpilot 迎来 1.0.0 正式版 —— 从「手写 frpc.toml」到「点几下就能跑起来」,frp 的日常管理终于有了顺手的图形界面。 + +### ✨ 新增功能 + +- **仪表盘**:一屏总览系统信息(主机名 / 系统 / 架构 / IP)与服务器、隧道数量;选择 frpc 版本后「一键启动 / 停止」,并实时回显运行日志与进程退出信息。 +- **服务器管理**:frps 服务器的新增 / 编辑 / 删除,支持地址、端口、鉴权 Token、传输协议、TLS、置顶与排序;同一时间仅允许启用一台服务器。 +- **隧道管理**:覆盖 TCP / UDP / HTTP / HTTPS / STCP / XTCP / SUDP / TCPMUX 等代理类型,支持自定义域名、子域名、URL 前缀、密钥、加密与压缩、带宽限制、Proxy Protocol、健康检查、负载均衡等高级配置,并统一管理启用 / 置顶 / 排序。 +- **Frpc 下载管理**:在线拉取 Frp 官方版本列表,按平台与架构筛选;支持下载(含进度与解压状态)、导入本地已下载的 frpc / frps、删除与复制下载直链。 +- **一键启动与实时日志**:依据所选版本、已启用的服务器与隧道自动生成 frpc.toml 并拉起独立进程,通过 `frpc-log` / `frpc-exit` 事件实时推送日志。 +- **应用内自动更新**:接入 Tauri updater 与 process 插件,启动时静默检查更新;支持手动检查、「忽略该版本」与下载进度展示,更新说明以 **Markdown** 渲染,安装完成后自动重启。 +- **关于页面**:集中展示项目介绍、技术栈、使用流程、开源地址与版本信息,并提供「检查更新」入口。 +- **国际化**:内置简体中文 / English,语言选择持久化,并与 Ant Design Vue 组件语言包联动。 +- **明暗主题**:基于 `useDark` 一键切换,Tailwind 暗色变体与 Ant Design Vue 暗色算法同步,选择持久化。 +- **自定义标题栏**:Windows 与 macOS 双套方案(macOS 还原原生红绿灯),支持拖拽移动窗口;顶部栏集成语言 / 主题 / 检查更新 / GitHub / 设置与窗口控制按钮。 + +### 🎨 优化 + +- **透明磨砂玻璃视觉**:侧边栏、内容区与卡片统一为圆角 + 背景模糊的半透明面板,明暗主题下均有细腻的层次与投影表现。 +- **统一弹窗布局**:表单弹窗与更新弹窗统一为「标题 / 底部按钮固定、内容区滚动」的结构,内容过多时仅弹窗主体滚动,并限制最大高度。 +- **macOS 标题栏细节**:隐藏右侧冗余的最小化 / 关闭按钮,禁用 macOS 下并不需要的最大化(绿灯)按钮。 +- **下载卡片交互**:状态标签、大小 / 下载次数 / 发布日期等元信息与操作按钮重新排布,下载与解压进度直观可见。 + +### 🚀 工程 + +- 后端采用 **Rust + Tauri 2**,使用 `sqlx`(SQLite)持久化服务器、隧道与 Frpc 下载记录,并通过 `tauri-helper` 统一注册命令。 +- 前端技术栈:**Vue 3 + TypeScript + Vite 8 + Pinia + Vue Router + Vue I18n 11**,UI 使用 **Ant Design Vue 4 + Tailwind CSS 4**,借助 `unplugin-auto-import` / `unplugin-vue-components` 实现按需自动导入。 +- 开启 `createUpdaterArtifacts` 并结合签名密钥生成更新产物与 `latest.json`,打通应用内自动更新链路。 +- 新增 GitHub Actions 发布工作流,覆盖 **Windows x64 / macOS Apple Silicon / Linux x64** 多平台构建,并自动从本文件提取对应版本说明作为 Release 正文。 +- 补充多平台应用图标,项目以 **Apache License 2.0** 开源。 diff --git a/package.json b/package.json index 98d0286..b75c60a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frpilot", "private": true, - "version": "0.1.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -18,6 +18,7 @@ "@vueuse/core": "^15.0.0", "ant-design-vue": "^4.2.6", "dayjs": "^1.11.23", + "markdown-it": "^15.0.2", "pinia": "^4.0.3", "vue": "^3.5.13", "vue-i18n": "^11.4.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6436d0..ece1cc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: dayjs: specifier: ^1.11.23 version: 1.11.23 + markdown-it: + specifier: ^15.0.2 + version: 15.0.2 pinia: specifier: ^4.0.3 version: 4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.43(typescript@6.0.3)) @@ -652,6 +655,9 @@ packages: peerDependencies: vue: '>=3.2.0' + argparse@3.0.2: + resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==} + array-tree-filter@2.1.0: resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} @@ -713,6 +719,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -919,6 +929,9 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + linkify-it@6.1.0: + resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==} + local-pkg@1.2.1: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} @@ -943,6 +956,13 @@ packages: magic-string@1.4.3: resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + markdown-it@15.0.2: + resolution: {integrity: sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==} + hasBin: true + + mdurl@2.1.0: + resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1003,6 +1023,10 @@ packages: resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -1062,6 +1086,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@3.0.0: + resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==} + ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} @@ -1723,6 +1750,8 @@ snapshots: vue-types: 3.0.2(vue@3.5.43(typescript@6.0.3)) warning: 4.0.3 + argparse@3.0.2: {} + array-tree-filter@2.1.0: {} ast-kit@2.2.0: @@ -1775,6 +1804,8 @@ snapshots: entities@7.0.1: {} + entities@8.1.0: {} + escape-string-regexp@5.0.0: {} estree-walker@2.0.2: {} @@ -1912,6 +1943,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + linkify-it@6.1.0: + dependencies: + uc.micro: 3.0.0 + local-pkg@1.2.1: dependencies: mlly: 1.8.2 @@ -1938,6 +1973,17 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + markdown-it@15.0.2: + dependencies: + argparse: 3.0.2 + entities: 8.1.0 + linkify-it: 6.1.0 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 3.0.0 + + mdurl@2.1.0: {} + mlly@1.8.2: dependencies: acorn: 8.19.0 @@ -1994,6 +2040,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + punycode.js@2.3.1: {} + quansync@0.2.11: {} readdirp@5.1.1: {} @@ -2058,6 +2106,8 @@ snapshots: typescript@6.0.3: {} + uc.micro@3.0.0: {} + ufo@1.6.4: {} undici-types@8.9.0: {} diff --git a/src/assets/main.css b/src/assets/main.css index 5ee9d50..aec953f 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -208,6 +208,168 @@ body { overflow-y: auto; } +/* + * Markdown 渲染内容(更新日志等)的排版。类名唯一,使用全局样式; + * 内容由 v-html 注入,不参与组件 scoped 样式,故放在此处统一维护。 + */ +.markdown-body { + font-size: 13px; + line-height: 1.75; + word-break: break-word; +} + +.markdown-body > :first-child { + margin-top: 0 !important; +} + +.markdown-body > :last-child { + margin-bottom: 0 !important; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin: 14px 0 8px; + font-weight: 600; + line-height: 1.4; +} + +.markdown-body h1 { + font-size: 17px; +} + +.markdown-body h2 { + font-size: 15px; +} + +.markdown-body h3 { + font-size: 14px; +} + +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + font-size: 13px; +} + +.markdown-body p { + margin: 8px 0; +} + +.markdown-body ul, +.markdown-body ol { + margin: 8px 0; + padding-left: 20px; +} + +.markdown-body ul { + list-style: disc; +} + +.markdown-body ol { + list-style: decimal; +} + +.markdown-body li { + margin: 4px 0; +} + +.markdown-body li > ul, +.markdown-body li > ol { + margin: 4px 0; +} + +.markdown-body a { + color: #6366f1; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body code { + padding: 1px 5px; + border-radius: 5px; + background: rgba(148, 163, 184, 0.22); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin: 10px 0; + padding: 10px 12px; + border-radius: 8px; + background: rgba(15, 23, 42, 0.06); + overflow-x: auto; +} + +.markdown-body pre code { + padding: 0; + background: transparent; + font-size: 12px; +} + +.markdown-body blockquote { + margin: 8px 0; + padding: 2px 12px; + border-left: 3px solid rgba(99, 102, 241, 0.5); + opacity: 0.85; +} + +.markdown-body hr { + margin: 12px 0; + border: none; + border-top: 1px solid rgba(15, 23, 42, 0.1); +} + +.markdown-body table { + width: 100%; + margin: 10px 0; + border-collapse: collapse; + font-size: 12px; +} + +.markdown-body th, +.markdown-body td { + padding: 6px 10px; + border: 1px solid rgba(15, 23, 42, 0.12); +} + +.markdown-body th { + background: rgba(148, 163, 184, 0.14); + font-weight: 600; +} + +.markdown-body img { + max-width: 100%; + border-radius: 6px; +} + +.dark .markdown-body a { + color: #a5b4fc; +} + +.dark .markdown-body pre { + background: rgba(255, 255, 255, 0.06); +} + +.dark .markdown-body hr { + border-top-color: rgba(255, 255, 255, 0.12); +} + +.dark .markdown-body th, +.dark .markdown-body td { + border-color: rgba(255, 255, 255, 0.14); +} + +.dark .markdown-body th { + background: rgba(255, 255, 255, 0.08); +} + /* 细滚动条 */ ::-webkit-scrollbar { width: 6px; diff --git a/src/components/MacTitleBar.vue b/src/components/MacTitleBar.vue index 4f66a4d..18a603e 100644 --- a/src/components/MacTitleBar.vue +++ b/src/components/MacTitleBar.vue @@ -27,18 +27,17 @@ - + @@ -46,7 +45,7 @@ diff --git a/src/components/TitleBarActions.vue b/src/components/TitleBarActions.vue index 80ffd9d..fc47811 100644 --- a/src/components/TitleBarActions.vue +++ b/src/components/TitleBarActions.vue @@ -68,8 +68,9 @@ - + - + () const { t } = useI18n() const appStore = useAppStore() const updateStore = useUpdateStore() +const { isMac } = usePlatform() const onLanguageClick: MenuProps["onClick"] = ({ key }) => { appStore.setLanguage(key as Language) diff --git a/src/components/UpdateModal.vue b/src/components/UpdateModal.vue index 0610326..9db245e 100644 --- a/src/components/UpdateModal.vue +++ b/src/components/UpdateModal.vue @@ -31,7 +31,10 @@ {{ t("update.log") }} -
{{ logText }}
+
+ +

{{ t("update.emptyLog") }}

+
@@ -62,6 +65,7 @@ import dayjs from "dayjs" import { ArrowRightOutlined, BulbOutlined } from "@ant-design/icons-vue" import { useI18n } from "vue-i18n" +import MarkdownBlock from "@/components/MarkdownBlock.vue" import { useUpdateStore } from "@/stores/update" const { t } = useI18n() @@ -75,7 +79,8 @@ const releaseDate = computed(() => info.value?.date ? dayjs(info.value.date).format("YYYY-MM-DD") : "", ) -const logText = computed(() => info.value?.body?.trim() || t("update.emptyLog")) +/** 是否存在可渲染的更新日志内容 */ +const hasLog = computed(() => Boolean(info.value?.body?.trim())) function onCancel() { store.cancel() @@ -125,14 +130,11 @@ function onConfirm() { } .update-log { - white-space: pre-wrap; word-break: break-word; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(15, 23, 42, 0.08); background: rgba(148, 163, 184, 0.12); - font-size: 13px; - line-height: 1.7; opacity: 0.9; } diff --git a/src/components/WindowsTitleBar.vue b/src/components/WindowsTitleBar.vue index b5a87b5..3bf03dd 100644 --- a/src/components/WindowsTitleBar.vue +++ b/src/components/WindowsTitleBar.vue @@ -10,7 +10,7 @@ Frpilot
- + diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index ead2eb7..0bd08c5 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -13,6 +13,7 @@ "settings": "Settings", "minimize": "Minimize", "maximize": "Maximize", + "maximizeDisabled": "Maximizing is not needed on macOS", "restore": "Restore", "close": "Close" }, diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 985674b..d295542 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -13,6 +13,7 @@ "settings": "设置", "minimize": "最小化", "maximize": "最大化", + "maximizeDisabled": "macOS 下无需最大化窗口", "restore": "还原", "close": "关闭" }, diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 09021ec..8a0af41 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -43,6 +43,7 @@ declare module 'vue' { ATooltip: typeof import('ant-design-vue/es')['Tooltip'] FrpCard: typeof import('./../components/frp/FrpCard.vue')['default'] MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default'] + MarkdownBlock: typeof import('./../components/MarkdownBlock.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ServerCard: typeof import('./../components/server/ServerCard.vue')['default'] diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts new file mode 100644 index 0000000..1dba44d --- /dev/null +++ b/src/utils/markdown.ts @@ -0,0 +1,31 @@ +import MarkdownIt from "markdown-it" + +/** + * 更新日志(Release Notes)Markdown 渲染器。 + * + * 安全约束: + * - `html: false`:禁用原始 HTML,杜绝注入; + * - markdown-it 默认的 `validateLink` 会拦截 `javascript:` 等危险链接。 + */ +const md = new MarkdownIt({ + html: false, + linkify: true, + breaks: true, +}) + +// 外链在新窗口打开(链接点击在组件中已被接管,统一交由系统浏览器处理) +const defaultLinkOpen = md.renderer.rules.link_open +md.renderer.rules.link_open = (tokens, idx, options, env, self) => { + const token = tokens[idx] + token.attrSet("target", "_blank") + token.attrSet("rel", "noopener noreferrer") + return defaultLinkOpen + ? defaultLinkOpen(tokens, idx, options, env, self) + : self.renderToken(tokens, idx, options) +} + +/** 将 Markdown 文本渲染为 HTML 字符串(空内容返回空串) */ +export function renderMarkdown(source?: string | null): string { + if (!source || !source.trim()) return "" + return md.render(source) +}