feat: 新增 Markdown 渲染组件并适配 macOS 标题栏
- 新增 MarkdownBlock 组件实现更新日志的 Markdown 渲染 - 新增 markdown 工具函数和样式 - 适配 macOS 标题栏,隐藏最小化/关闭按钮并禁用最大化按钮 - 新增 1.0.0 版本更新日志文档
This commit is contained in:
@@ -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** 开源。
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
Generated
+50
@@ -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: {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,18 +27,17 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="traffic-light bg-[#28c840]"
|
||||
:class="{ 'traffic-light--idle': !focused }"
|
||||
:title="t('titlebar.maximize')"
|
||||
@click="toggleMaximizeWindow()"
|
||||
class="traffic-light traffic-light--disabled bg-[#28c840]"
|
||||
:title="t('titlebar.maximizeDisabled')"
|
||||
disabled
|
||||
>
|
||||
<svg viewBox="0 0 12 12" class="light-icon opacity-0 group-hover/lights:opacity-100">
|
||||
<svg viewBox="0 0 12 12" class="light-icon opacity-0">
|
||||
<path d="M3.5 3.5h5v5h-5z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:语言 / 主题 / 设置 / 最小化 / 关闭 -->
|
||||
<!-- 右侧:语言 / 主题 / 检查更新 / GitHub / 设置(最小化与关闭由左侧红绿灯负责) -->
|
||||
<TitleBarActions @settings="emit('settings')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -46,7 +45,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n"
|
||||
import TitleBarActions from "./TitleBarActions.vue"
|
||||
import { closeWindow, minimizeWindow, toggleMaximizeWindow } from "@/utils/window"
|
||||
import { closeWindow, minimizeWindow } from "@/utils/window"
|
||||
|
||||
const emit = defineEmits<{ settings: [] }>()
|
||||
|
||||
@@ -79,6 +78,14 @@ const focused = useWindowFocus()
|
||||
background-color: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
/* macOS 不需要最大化:绿灯禁用置灰 */
|
||||
.traffic-light--disabled,
|
||||
.traffic-light:disabled {
|
||||
cursor: not-allowed;
|
||||
filter: saturate(0.35) brightness(1.05);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.light-icon {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="markdown-body" v-html="html" @click="onActivate" @auxclick="onActivate" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { openExternal } from "@/api/updater"
|
||||
import { renderMarkdown } from "@/utils/markdown"
|
||||
|
||||
const props = defineProps<{
|
||||
/** Markdown 源文本 */
|
||||
source?: string | null
|
||||
}>()
|
||||
|
||||
const html = computed(() => renderMarkdown(props.source))
|
||||
|
||||
/** 拦截链接点击,统一用系统默认浏览器打开,避免 webview 自身跳转 */
|
||||
function onActivate(e: MouseEvent) {
|
||||
const target = e.target
|
||||
const anchor = target instanceof Element ? target.closest("a") : null
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return
|
||||
|
||||
const href = anchor.getAttribute("href")
|
||||
if (!href || href.startsWith("#")) return
|
||||
|
||||
e.preventDefault()
|
||||
void openExternal(href)
|
||||
}
|
||||
</script>
|
||||
@@ -68,8 +68,9 @@
|
||||
<template #icon><SettingOutlined /></template>
|
||||
</a-button>
|
||||
|
||||
<!-- 最小化 -->
|
||||
<!-- 最小化(macOS 由左侧红绿灯负责,此处隐藏) -->
|
||||
<a-button
|
||||
v-if="!isMac"
|
||||
type="text"
|
||||
class="tb-btn"
|
||||
:title="t('titlebar.minimize')"
|
||||
@@ -78,8 +79,9 @@
|
||||
<template #icon><MinusOutlined /></template>
|
||||
</a-button>
|
||||
|
||||
<!-- 关闭 -->
|
||||
<!-- 关闭(macOS 由左侧红绿灯负责,此处隐藏) -->
|
||||
<a-button
|
||||
v-if="!isMac"
|
||||
type="text"
|
||||
class="tb-btn tb-btn-close"
|
||||
:title="t('titlebar.close')"
|
||||
@@ -108,12 +110,14 @@ import { useUpdateStore } from "@/stores/update"
|
||||
import { languageOptions, type Language } from "@/i18n"
|
||||
import { closeWindow, minimizeWindow } from "@/utils/window"
|
||||
import { openProjectRepo } from "@/api/updater"
|
||||
import { usePlatform } from "@/composables/usePlatform"
|
||||
|
||||
const emit = defineEmits<{ settings: [] }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const updateStore = useUpdateStore()
|
||||
const { isMac } = usePlatform()
|
||||
|
||||
const onLanguageClick: MenuProps["onClick"] = ({ key }) => {
|
||||
appStore.setLanguage(key as Language)
|
||||
|
||||
@@ -31,7 +31,10 @@
|
||||
<BulbOutlined />
|
||||
<span>{{ t("update.log") }}</span>
|
||||
</div>
|
||||
<div class="update-log">{{ logText }}</div>
|
||||
<div class="update-log">
|
||||
<MarkdownBlock v-if="hasLog" :source="info?.body" />
|
||||
<p v-else class="m-0 opacity-80">{{ t("update.emptyLog") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 下载 / 安装进度 -->
|
||||
<div v-if="store.installing" class="update-progress">
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="text-sm font-semibold tracking-wide">Frpilot</span>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:语言 / 主题 / 设置 / 最小化 / 关闭 -->
|
||||
<!-- 右侧:语言 / 主题 / 设置 / 最小化 / 关闭(macOS 下隐藏最小化与关闭) -->
|
||||
<TitleBarActions @settings="emit('settings')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"settings": "Settings",
|
||||
"minimize": "Minimize",
|
||||
"maximize": "Maximize",
|
||||
"maximizeDisabled": "Maximizing is not needed on macOS",
|
||||
"restore": "Restore",
|
||||
"close": "Close"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"settings": "设置",
|
||||
"minimize": "最小化",
|
||||
"maximize": "最大化",
|
||||
"maximizeDisabled": "macOS 下无需最大化窗口",
|
||||
"restore": "还原",
|
||||
"close": "关闭"
|
||||
},
|
||||
|
||||
Vendored
+1
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user