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
---
# 全部项目