feat: 新增 Markdown 渲染组件并适配 macOS 标题栏

- 新增 MarkdownBlock 组件实现更新日志的 Markdown 渲染
- 新增 markdown 工具函数和样式
- 适配 macOS 标题栏,隐藏最小化/关闭按钮并禁用最大化按钮
- 新增 1.0.0 版本更新日志文档
This commit is contained in:
Yuhang Wu
2026-10-14 10:40:34 +08:00
parent 746a4be2bc
commit bc4da1c7a4
13 changed files with 344 additions and 16 deletions
+162
View File
@@ -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;
+14 -7
View File
@@ -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;
+29
View File
@@ -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>
+6 -2
View File
@@ -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)
+7 -5
View File
@@ -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;
}
+1 -1
View File
@@ -10,7 +10,7 @@
<span class="text-sm font-semibold tracking-wide">Frpilot</span>
</div>
<!-- 右侧:语言 / 主题 / 设置 / 最小化 / 关闭 -->
<!-- 右侧:语言 / 主题 / 设置 / 最小化 / 关闭(macOS 下隐藏最小化与关闭) -->
<TitleBarActions @settings="emit('settings')" />
</div>
</template>
+1
View File
@@ -13,6 +13,7 @@
"settings": "Settings",
"minimize": "Minimize",
"maximize": "Maximize",
"maximizeDisabled": "Maximizing is not needed on macOS",
"restore": "Restore",
"close": "Close"
},
+1
View File
@@ -13,6 +13,7 @@
"settings": "设置",
"minimize": "最小化",
"maximize": "最大化",
"maximizeDisabled": "macOS 下无需最大化窗口",
"restore": "还原",
"close": "关闭"
},
+1
View File
@@ -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']
+31
View File
@@ -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)
}