From e4757446cf2aa735e413895c62cf13b61f622fa2 Mon Sep 17 00:00:00 2001 From: Dai0814 <3490199389@qq.com> Date: Fri, 6 Nov 2026 13:39:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(portal):=20=E8=A1=A5=E5=85=A5=E6=95=B4?= =?UTF-8?q?=E7=AB=99=E9=A1=B5=E8=84=9A=EF=BC=8C=E5=B9=B6=E8=AE=A9=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=88=97=E8=A1=A8=E9=A1=B5=E4=B8=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=A1=B5=E8=84=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈 /projects/(项目列表)页底部的品牌/联系/版权区应该去掉。 背景:这几个文件此前一直未提交——远端仓库其实没有页脚, PortalFooter.vue 只存在于本地,HEAD 版 index.ts 也没引用它。 所以本次把页脚整套补齐,再叠加「按页面决定是否展示」的逻辑。 新增/改动 - .vitepress/theme/components/PortalFooter.vue(新增,274 行) 品牌区 + 产品体系/文档资源/关于我们/联系我们四栏 + 二维码浮层 + 版权条 - .vitepress/theme/styles/portal.css:补入全部 .pf-foot-* 样式(508 行) 只搬运页脚相关规则,不夹带工作区里其它未提交的样式改动 - .vitepress/theme/index.ts:挂载页脚 + 页面级显隐判定 - pages/projects/index.md:frontmatter 加 footer: false 页脚显隐判定(isDocPage) 「文档型页面不挂页脚」——这类页面有左侧目录树、右侧本页目录、 底部上一页/下一页翻页,读者正在阅读流里,450px 的品牌收尾区是纯噪音。 命中任一条件即隐藏: 1) 路径 /projects//manual/... 2) frontmatter 有 sidebar / outline / aside 3) frontmatter 显式 footer: false frontmatter 的 footer 可双向覆盖(这解决了 /projects/ 的困境): 该页有 outline: deep,按上面的规则本会被判成文档页而隐藏页脚, 但它本质是「浏览页」,需要显式 footer: false 才符合预期。 同理 footer: true 可强制显示。 验证(干净 clone 构建后实测) - /projects/ 无页脚(footer=false,无版权条) - / 有页脚(453px,渐变背景正常) - 文档页(详情/对比/快速开始/手册)均无页脚 - 自定义整页(关于/联系/演示/官方文档)均有页脚 --- .vitepress/theme/components/PortalFooter.vue | 274 ++++++++++ .vitepress/theme/index.ts | 40 +- .vitepress/theme/styles/portal.css | 508 +++++++++++++++++++ pages/projects/index.md | 3 + 4 files changed, 823 insertions(+), 2 deletions(-) create mode 100644 .vitepress/theme/components/PortalFooter.vue diff --git a/.vitepress/theme/components/PortalFooter.vue b/.vitepress/theme/components/PortalFooter.vue new file mode 100644 index 0000000..05d8272 --- /dev/null +++ b/.vitepress/theme/components/PortalFooter.vue @@ -0,0 +1,274 @@ + + + diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index b22cccc..71f5f8c 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,6 +1,6 @@ import DefaultTheme from 'vitepress/theme' import type { Theme } from 'vitepress' -import { h, watch, nextTick } from 'vue' +import { h, watch, nextTick, computed } from 'vue' import { useData, useRoute } from 'vitepress' import ProjectList from './components/ProjectList.vue' @@ -8,6 +8,7 @@ import HomePage from './components/HomePage.vue' import CapabilityTabs from './components/CapabilityTabs.vue' import FeatureGrid from './components/FeatureGrid.vue' import ScreenshotPreview from './components/ScreenshotPreview.vue' +import PortalFooter from './components/PortalFooter.vue' import { setupNavAnchorHighlight, applyNavAnchorHighlight } from './nav-anchor' @@ -24,6 +25,37 @@ const PortalLayout = () => { const isPortal = frontmatter.value.layout === 'page' + /** + * 是否隐藏整站页脚。 + * + * 判定原则:**文档型页面不挂页脚**。 + * 这类页面有左侧目录树、右侧本页目录、底部上一页/下一页翻页, + * 读者正处在阅读流里,页脚那 450px 的品牌/联系/版权是纯噪音, + * 还会和翻页按钮挤在一起。 + * + * 覆盖三种情况: + * 1) 路径命中项目手册 `/projects//manual/...` + * 2) frontmatter 写了 `sidebar` / `outline` / `aside`(文档页标记) + * 3) frontmatter 显式 `footer: false` + * + * frontmatter 的 `footer` 可双向覆盖: + * footer: false → 强制隐藏(如清单页 /projects/:有 outline 但本质是浏览页) + * footer: true → 强制显示 + * + * 自定义整页(layout: page,如首页/关于/演示/官方文档)不在此列, + * 它们有完整的品牌收尾节奏,页脚保留。 + */ + const isManualPath = computed(() => /^\/projects\/[^/]+\/manual(\/|$)/.test(route.path)) + const isDocPage = computed(() => { + const fm = frontmatter.value as Record + if (fm.layout === 'page' || fm.layout === 'home') return false + if (fm.footer === false) return true + if (fm.footer === true) return false + const hasDocMarks = + fm.sidebar !== undefined || fm.outline !== undefined || fm.aside !== undefined + return isManualPath.value || hasDocMarks + }) + /** * Layout 是函数式组件,onMounted 在这里不生效(Vue 会告警), * 因此直接在渲染期间调度:等 DOM 落地后跑一次,再靠 MutationObserver 兜住后续变化。 @@ -38,7 +70,11 @@ const PortalLayout = () => { () => nextTick(() => applyNavAnchorHighlight()) ) - return h('div', { class: isPortal ? 'portal-page' : '' }, h(DefaultTheme.Layout)) + return h('div', { class: isPortal ? 'portal-page' : '' }, h(DefaultTheme.Layout, null, { + // 自定义整站页脚(品牌 + 多栏链接 + 联系方式 + 版权条) + // 文档型页面一律不挂(见 isDocPage 判定) + 'layout-bottom': isDocPage.value ? undefined : () => h(PortalFooter), + })) } export default { diff --git a/.vitepress/theme/styles/portal.css b/.vitepress/theme/styles/portal.css index 0e2733e..be224c0 100644 --- a/.vitepress/theme/styles/portal.css +++ b/.vitepress/theme/styles/portal.css @@ -1122,3 +1122,511 @@ html.portal-nav-home .VPNavBarMenuLink.active:not([data-anchor-active]) { display: none; } } + +/* ============================================================ + 整站页脚(PortalFooter) + 结构对齐商业站范式:品牌 + 多栏链接 + 联系方式 + 版权条。 + 配色沿用蓝调浅色体系(不铺深色),靠浅蓝底与细分隔线分区。 + ============================================================ */ +.pf-foot { + position: relative; + /* 页脚整宽仍是通栏(保证有侧栏的页面能盖住 fixed 侧栏), + 但浅蓝底纹只铺在内容宽度内:用 background-size + center 让底纹 + 与 .p-wrap 的 1180px 对齐,两侧自然留白。 + 这样视觉上不再是一条横贯整屏的浅蓝色带(1440 视口下比内容宽 300px)。 */ + background-image: linear-gradient(180deg, #f7faff 0%, #eef4fe 100%); + background-repeat: no-repeat; + background-position: top center; + background-size: min(100%, var(--p-max)) 100%; + color: var(--p-ink-2); +} + +/* 底纹与内容之间加一道极淡的描边,收窄后不至于是「悬空的一块色」 */ +.pf-foot::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: min(100%, var(--p-max)); + height: 1px; + background: var(--p-line); + pointer-events: none; +} + +/** + * 有侧边栏的页面(/projects/、/guide/ 等文档页): + * + * 早先的做法是给页脚加 padding-left: var(--vp-sidebar-width) 去「避让」侧栏。 + * 但侧栏是 fixed + 不透明底色(#f6f6f7),一直铺到视口底部,于是页脚那条 + * 浅蓝底纹被侧栏盖掉左边一大块,只剩内容被推到右边 —— 宽屏下左边空白可达 + * 视口宽度的 1/4(1440→272px、1920→512px),看上去像「页脚缺了一块」。 + * + * 现在的做法:页脚保持整宽、内容用 .p-wrap 居中(与首页/演示页完全一致), + * 只把页脚的层级抬到侧栏之上(桌面端侧栏 z-index = --vp-z-index-sidebar = 25)。 + * 侧栏由此在页脚上边缘「结束」,页脚恢复整幅底色;侧栏导航项目都排在最顶部, + * 被页脚覆盖的只是它下方那段空白。 + * 移动端(<960px)侧栏是抽屉,层级必须保持在上层,故不抬。 + */ +@media (min-width: 960px) { + .pf-foot { + position: relative; + z-index: calc(var(--vp-z-index-sidebar) + 1); + } + + /** + * ⚠️ 空白断层的真凶:VitePress 原生给 `.aside-content`(右侧「本页目录」容器) + * 设了 `min-height: calc(100vh - nav - 48px)`。 + * 短页面时它会先把 `.VPDoc .container` 撑到接近一屏, + * 再叠上页脚 → 总高 = 一屏 + 页脚,滚到底就只剩「一大片空白 + 页脚」。 + * 正文其实只有 466px,却被撑到 749px,多出 283px 纯空白。 + * + * 修法:取消目录容器的强制高度,让它按自身内容高走。 + * 目录条目少就短,条目多自然变长,不再拉高整页。 + */ + .VPDoc .aside-content { + min-height: 0; + } + + /** + * 短页面时让页脚自然跟随内容,不要把正文区强行撑满一屏。 + * (撑满一屏的旧写法会把页脚推到"一屏之外",制造空白断层) + */ + .VPDoc { + min-height: 0; + } + + /** + * 页脚压住侧栏下缘时,把那道「切边」做成 26px 渐隐 + * (--pf-side-encroach 由 theme/sidebar-fade.ts 按滚动位置写入), + * 否则侧栏最后一个导航项会只露半行,像渲染坏了。 + */ + .VPSidebar.pf-side-fade { + --pf-side-cut: calc(100% - var(--pf-side-encroach, 0px)); + -webkit-mask-image: linear-gradient( + to bottom, + #000 calc(var(--pf-side-cut) - 26px), + transparent var(--pf-side-cut) + ); + mask-image: linear-gradient( + to bottom, + #000 calc(var(--pf-side-cut) - 26px), + transparent var(--pf-side-cut) + ); + } +} + +html.dark .pf-foot { + /* 必须用 background-image 而不是简写 background: + 简写会把 background-size 重置回 auto,暗色下底纹又铺满整屏。 */ + background-image: linear-gradient(180deg, rgba(37, 99, 235, 0.10) 0%, transparent 100%); + background-repeat: no-repeat; + background-position: top center; + background-size: min(100%, var(--p-max)) 100%; +} + +.pf-foot-main { + display: grid; + /* 品牌区略宽(描述文案不碎行),中段三列够放「秀杰 DTU 管理平台」, + 右栏够放长邮箱且「邮箱」标签不被压成两行。 + 二维码改为点击浮出后,右栏不再需要为 246px 预留宽度。 */ + grid-template-columns: minmax(0, 1.25fr) minmax(0, 1.8fr) minmax(0, 0.92fr); + gap: 38px; + padding: 64px 0 48px; +} + +/* ---------- 品牌区 ---------- */ +.pf-foot-logo { + display: inline-flex; + align-items: center; + gap: 11px; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--p-ink) !important; + text-decoration: none !important; +} + +.pf-foot-logo img { + width: 42px; + height: 42px; + object-fit: contain; +} + +.pf-foot-slogan { + margin: 18px 0 0; + font-size: 16.5px; + font-weight: 650; + letter-spacing: 0.01em; + color: var(--p-accent); +} + +.pf-foot-desc { + margin: 10px 0 0; + max-width: 400px; + font-size: 15.5px; + line-height: 1.78; + color: var(--p-ink-2); + /* 避免中文末行只剩一两个字(如「署。」独占一行) */ + text-wrap: pretty; +} + +.pf-foot-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 20px 0 0; + padding: 0; + list-style: none; +} + +.pf-foot-chips li { + padding: 6px 13px; + font-size: 13px; + color: var(--p-ink-2); + background: var(--p-surface); + border: 1px solid var(--p-line); + border-radius: 999px; +} + +/* ---------- 链接栏 ---------- */ +.pf-foot-cols { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 30px; +} + +.pf-foot-col h4, +.pf-foot-contact h4 { + margin: 0 0 18px; + font-size: 15px; + font-weight: 650; + letter-spacing: 0.02em; + color: var(--p-ink); +} + +.pf-foot-col ul { + margin: 0; + padding: 0; + list-style: none; +} + +.pf-foot-col li + li { + margin-top: 11px; +} + +.pf-foot-col a { + display: inline-block; + font-size: 16px; + line-height: 1.45; + color: var(--p-ink-2) !important; + text-decoration: none !important; + white-space: nowrap; + transition: color 0.16s; +} + +.pf-foot-col a:hover { + color: var(--p-accent) !important; +} + +/* ---------- 联系方式 ---------- */ +.pf-foot-contact { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.pf-foot-contact-row { + display: flex; + /* 图标与首行文字对齐,而不是与整行(可能多行)垂直居中。 + 否则长邮箱折行时,图标会落在两行中间,看起来「歪了」。 */ + align-items: flex-start; + gap: 8px; + width: 100%; + padding: 0; + text-decoration: none !important; +} + +/* 行距与左侧链接栏同节奏:23px 行高 + 11px 间隔 */ +.pf-foot-contact-row + .pf-foot-contact-row { + margin-top: 11px; +} + +/* 前置图标:与首行文字同高,浅蓝底上取中灰蓝,hover 变蓝 */ +.pf-foot-contact-icon { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + /* 18px 图标放进 23px 行高里,补 2.5px 让它与首行文字视觉居中; + 行高 23px、字号 14/16px 时文字基线偏下,这个值实测最贴。 */ + margin-top: 2.5px; + color: #94a3b8; + transition: color 0.16s; +} + +.pf-foot-contact-icon svg { + width: 100%; + height: 100%; +} + +.pf-foot-contact-row:hover .pf-foot-contact-icon { + color: var(--p-accent); +} + +.pf-foot-contact-text { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + min-width: 0; +} + +/* 标签不换行:栏宽一度被长邮箱挤压,「邮箱」曾被压成两行 */ +.pf-foot-contact-label { + flex: none; + white-space: nowrap; +} + +/* 复制按钮:外观与普通文本行一致,抹掉浏览器默认按钮样式 */ +.pf-foot-contact-copy { + flex: 0 1 auto; + padding: 0; + font-family: inherit; + text-align: left; + background: transparent; + border: none; + cursor: pointer; +} + +/* 轻提示:常态不抢眼,hover / 已复制时变蓝 */ +.pf-foot-copy-hint { + margin-left: 7px; + font-style: normal; + font-size: 12px; + font-weight: 500; + color: var(--p-ink-3); + opacity: 0; + transition: opacity 0.16s, color 0.16s; +} + +.pf-foot-contact-row:hover .pf-foot-copy-hint { + opacity: 1; + color: var(--p-accent); +} + +/* 已复制:常显并高亮 */ +.pf-foot-contact-row.is-copied .pf-foot-copy-hint { + opacity: 1; + color: var(--p-accent); + font-weight: 600; +} + +.pf-foot-contact-row.is-copied .pf-foot-contact-value { + color: var(--p-accent); +} + +.pf-foot-contact-row--btn .pf-foot-contact-value { + display: inline-flex; + align-items: baseline; +} + +.pf-foot-contact-label { + font-size: 14px; + line-height: 23px; + color: var(--p-ink-3); +} + +.pf-foot-contact-value { + font-size: 16px; + font-weight: 400; + line-height: 23px; + color: var(--p-ink-2); + transition: color 0.16s; + /* 不折行:右栏仅 243px,contact@xueyangai.com 在 16px 下会断成两行, + 一断行图标就会「浮」在两行之间。 */ + white-space: nowrap; +} + +/* 邮箱偏长,单独降一档字号,保证在 243px 栏里单行放下 */ +.pf-foot-contact-row[data-key='mail'] .pf-foot-contact-value { + font-size: 15px; + letter-spacing: -0.01em; +} + +.pf-foot-contact-row:hover .pf-foot-contact-value { + color: var(--p-accent); +} + +/* ---------- 企业微信二维码(点击浮出) ---------- + 二维码必须按 246px 1:1 显示(实测小于此尺寸解码失败,浏览器缩放也会扫不出), + 这比页脚右栏还宽,直接平铺会把整栏拉宽。所以改成一行触发器 + 向上浮出的浮层: + 平时与「电话/邮箱」同构,点击后浮层盖在上方,不影响页脚栅格。 */ +.pf-foot-qr { + position: relative; + width: 100%; + margin-top: 11px; +} + +.pf-foot-qr-trigger { + display: flex; + /* 与电话/邮箱行一致:图标对齐首行,不随多行垂直居中 */ + align-items: flex-start; + gap: 8px; + width: 100%; + padding: 0; + font-family: inherit; + text-align: left; + background: none; + border: 0; + cursor: pointer; +} + +.pf-foot-qr-trigger .pf-foot-contact-icon { + width: 18px; + height: 18px; +} + +.pf-foot-qr-trigger .pf-foot-contact-icon svg { + width: 100%; + height: 100%; +} + +.pf-foot-qr-trigger:hover .pf-foot-contact-icon, +.pf-foot-qr-trigger.is-open .pf-foot-contact-icon { + color: var(--p-accent); +} + +.pf-foot-qr-value { + display: inline-flex; + align-items: baseline; + gap: 6px; +} + +.pf-foot-qr-trigger:hover .pf-foot-contact-value, +.pf-foot-qr-trigger.is-open .pf-foot-contact-value { + color: var(--p-accent); +} + +/* 浮层:向上展开,右对齐,避免超出视口右缘 */ +.pf-foot-qr-pop { + position: absolute; + bottom: calc(100% + 12px); + right: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 10px 10px 12px; + background: #ffffff; + border: 1px solid var(--p-line-strong); + border-radius: 14px; + box-shadow: 0 24px 56px -20px rgba(15, 23, 42, 0.32); + z-index: 10; +} + +.pf-foot-qr-pop img { + display: block; + /* 1:1,禁止浏览器重采样 —— 缩放会让码点边缘发灰导致扫不出 */ + width: 246px; + height: 246px; + image-rendering: pixelated; + border-radius: 6px; +} + +.pf-foot-qr-pop-hint { + font-size: 12.5px; + line-height: 1.4; + color: var(--p-ink-3); +} + +/* 小三角,指向触发器 */ +.pf-foot-qr-pop::after { + content: ''; + position: absolute; + bottom: -6px; + right: 26px; + width: 12px; + height: 12px; + background: #ffffff; + border-right: 1px solid var(--p-line-strong); + border-bottom: 1px solid var(--p-line-strong); + transform: rotate(45deg); +} + +/* 暗色主题下二维码保持白底黑码,不能被反色 */ +html.dark .pf-foot-qr-pop, +html.dark .pf-foot-qr-pop::after { + background: #ffffff; +} + +.pf-foot-btn { + margin-top: 22px; + height: 44px; + padding: 0 24px; + font-size: 15.5px; +} + +/* ---------- 版权条 ---------- */ +.pf-foot-bottom { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 20px 0 28px; + border-top: 1px solid var(--p-line); + font-size: 13.5px; + color: var(--p-ink-3); +} + +.pf-foot-bottom-right { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.pf-foot-bottom-right a { + color: var(--p-ink-3) !important; + text-decoration: none !important; +} + +.pf-foot-bottom-right a:hover { + color: var(--p-accent) !important; +} + +.pf-foot-bottom-right i { + font-style: normal; + opacity: 0.6; +} + +/* ---------- 响应式 ---------- */ +@media (max-width: 1024px) { + .pf-foot-main { + grid-template-columns: 1fr 1fr; + gap: 40px; + } + .pf-foot-contact { + grid-column: 1 / -1; + border-top: 1px solid var(--p-line); + padding-top: 28px; + } +} + +@media (max-width: 720px) { + .pf-foot-main { + grid-template-columns: 1fr; + gap: 32px; + padding: 44px 0 32px; + } + .pf-foot-cols { + grid-template-columns: repeat(2, 1fr); + gap: 24px; + } + .pf-foot-bottom { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/pages/projects/index.md b/pages/projects/index.md index 0aa5678..d875329 100644 --- a/pages/projects/index.md +++ b/pages/projects/index.md @@ -2,6 +2,9 @@ title: 全部项目 description: 雪羊科技开源项目清单 outline: deep +# 本页不挂整站页脚:清单页以「浏览项目」为主,底部紧接项目卡片收尾即可, +# 再叠一层品牌/联系/版权会把重点冲淡。 +footer: false --- # 全部项目