用户反馈 /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/<id>/manual/... 2) frontmatter 有 sidebar / outline / aside 3) frontmatter 显式 footer: false frontmatter 的 footer 可双向覆盖(这解决了 /projects/ 的困境): 该页有 outline: deep,按上面的规则本会被判成文档页而隐藏页脚, 但它本质是「浏览页」,需要显式 footer: false 才符合预期。 同理 footer: true 可强制显示。 验证(干净 clone 构建后实测) - /projects/ 无页脚(footer=false,无版权条) - / 有页脚(453px,渐变背景正常) - 文档页(详情/对比/快速开始/手册)均无页脚 - 自定义整页(关于/联系/演示/官方文档)均有页脚
This commit is contained in:
@@ -0,0 +1,274 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { withBase, useData } from 'vitepress'
|
||||||
|
import { projects } from '../../../data/projects'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站点页脚
|
||||||
|
*
|
||||||
|
* 结构对齐商业站页脚范式:左侧品牌(Logo + 定位 + 描述 + 资质标签)、
|
||||||
|
* 中部多栏链接、右侧联系方式与行动按钮、底部版权条。
|
||||||
|
* 链接全部取自注册表与内容数据,避免硬编码产生死链。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有侧边栏的页面会带上 .has-sidebar 标记(目前仅作语义钩子,不参与布局)。
|
||||||
|
*
|
||||||
|
* 页脚不再靠 padding 避让侧栏:侧栏是 fixed 且不透明,避让反而会在宽屏下留出
|
||||||
|
* 一大片空白(1440→272px、1920→512px)。现在页脚整宽居中,靠 z-index 抬到
|
||||||
|
* 侧栏之上,见 portal.css 中 .pf-foot 的说明。
|
||||||
|
*
|
||||||
|
* 判定与官方 useSidebar().hasSidebar 同源:frontmatter.sidebar !== false、
|
||||||
|
* 该路径确实命中主题 sidebar 配置、且不是 home 布局。
|
||||||
|
*/
|
||||||
|
const { frontmatter, page, theme } = useData()
|
||||||
|
|
||||||
|
const hasSidebar = computed(() => {
|
||||||
|
if (frontmatter.value.sidebar === false) return false
|
||||||
|
if (frontmatter.value.layout === 'home') return false
|
||||||
|
const cfg = theme.value.sidebar
|
||||||
|
if (!cfg) return false
|
||||||
|
// 与官方 getSidebar 一致的匹配顺序:精确 → 目录前缀 → 最长前缀
|
||||||
|
const path = '/' + (page.value.relativePath || '').replace(/\.md$/, '').replace(/\/index$/, '/')
|
||||||
|
if (Array.isArray(cfg)) return cfg.length > 0
|
||||||
|
let matched = ''
|
||||||
|
for (const key of Object.keys(cfg)) {
|
||||||
|
if (path.startsWith(key) && key.length > matched.length) matched = key
|
||||||
|
}
|
||||||
|
return matched ? cfg[matched].length > 0 : false
|
||||||
|
})
|
||||||
|
|
||||||
|
const ORG_URL = 'https://gitee.com/snowgoat'
|
||||||
|
|
||||||
|
/** 中部链接栏 */
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '产品体系',
|
||||||
|
items: projects.map((p) => ({ text: p.name, link: withBase(p.page) })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文档资源',
|
||||||
|
items: [
|
||||||
|
{ text: '官方文档', link: withBase('/docs/') },
|
||||||
|
{ text: '开源地址', link: ORG_URL, external: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '关于我们',
|
||||||
|
items: [
|
||||||
|
{ text: '关于雪羊', link: withBase('/about') },
|
||||||
|
{ text: '联系我们', link: withBase('/contact') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 右侧联系方式:电话/邮箱为点击复制;微信点击展开二维码浮层 */
|
||||||
|
const contacts = [
|
||||||
|
{ key: 'phone', label: '电话', value: '0531-85950527', icon: 'phone' },
|
||||||
|
{ key: 'mail', label: '邮箱', value: 'contact@xueyangai.com', icon: 'mail' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码浮层
|
||||||
|
*
|
||||||
|
* 为什么不直接把二维码摆在页脚里:这张码 33×33 模块,实测码区小于 160px
|
||||||
|
* 就会被解码器判定失败,浏览器再缩放一次更是直接扫不出。也就是说它必须
|
||||||
|
* 按 246px(198 码区 + 24 白边)1:1 显示,而这比页脚右栏(约 280px)还宽,
|
||||||
|
* 硬塞会把整栏拉得很宽、压掉其它列。
|
||||||
|
*
|
||||||
|
* 所以改成「点击展开、分层浮出」:平时只占一行文字的位置,
|
||||||
|
* 点击后在按钮上方浮出完整尺寸的二维码,扫码体验和尺寸都不打折。
|
||||||
|
*/
|
||||||
|
const qrOpen = ref(false)
|
||||||
|
const qrWrap = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
function toggleQr() {
|
||||||
|
qrOpen.value = !qrOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeQrOnOutside(e: MouseEvent) {
|
||||||
|
if (!qrOpen.value) return
|
||||||
|
if (qrWrap.value && !qrWrap.value.contains(e.target as Node)) qrOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeQrOnEsc(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') qrOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', closeQrOnOutside)
|
||||||
|
document.addEventListener('keydown', closeQrOnEsc)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', closeQrOnOutside)
|
||||||
|
document.removeEventListener('keydown', closeQrOnEsc)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 当前已复制的项(key),用于各自独立显示「已复制」 */
|
||||||
|
const copiedKey = ref('')
|
||||||
|
let copyTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
async function copyText(text: string, key: string) {
|
||||||
|
if (!text) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} catch {
|
||||||
|
// 非 https / 旧浏览器:退回 textarea + execCommand
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.position = 'fixed'
|
||||||
|
ta.style.opacity = '0'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
}
|
||||||
|
copiedKey.value = key
|
||||||
|
clearTimeout(copyTimer)
|
||||||
|
copyTimer = setTimeout(() => (copiedKey.value = ''), 1600)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => clearTimeout(copyTimer))
|
||||||
|
|
||||||
|
const year = new Date().getFullYear()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="pf-foot" :class="{ 'has-sidebar': hasSidebar }">
|
||||||
|
<div class="p-wrap">
|
||||||
|
<div class="pf-foot-main">
|
||||||
|
<!-- 品牌 -->
|
||||||
|
<div class="pf-foot-brand">
|
||||||
|
<a class="pf-foot-logo" :href="withBase('/')">
|
||||||
|
<img :src="withBase('/snowgoat-logo.png')" alt="雪羊科技" width="34" height="34" />
|
||||||
|
<span>雪羊科技</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="pf-foot-slogan">开源物联网卡与设备管理平台</p>
|
||||||
|
<p class="pf-foot-desc">
|
||||||
|
物联网卡管理、DTU 设备管理、中后台开发底座,三个项目同一套工程规范。
|
||||||
|
全部以 Apache-2.0 发布,可自由使用、修改与商业部署。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="pf-foot-chips">
|
||||||
|
<li>Apache-2.0 开源许可</li>
|
||||||
|
<li>支持私有化部署</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 链接栏 -->
|
||||||
|
<div class="pf-foot-cols">
|
||||||
|
<div v-for="col in columns" :key="col.title" class="pf-foot-col">
|
||||||
|
<h4>{{ col.title }}</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in col.items" :key="item.text">
|
||||||
|
<a
|
||||||
|
:href="item.link"
|
||||||
|
:target="item.external ? '_blank' : undefined"
|
||||||
|
:rel="item.external ? 'noopener' : undefined"
|
||||||
|
>{{ item.text }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联系 -->
|
||||||
|
<div class="pf-foot-contact">
|
||||||
|
<h4>联系我们</h4>
|
||||||
|
<!-- 各项统一:整行点击复制 -->
|
||||||
|
<div
|
||||||
|
v-for="c in contacts"
|
||||||
|
:key="c.key"
|
||||||
|
class="pf-foot-contact-row"
|
||||||
|
:data-key="c.key"
|
||||||
|
:class="{ 'is-copied': copiedKey === c.key }"
|
||||||
|
>
|
||||||
|
<span class="pf-foot-contact-icon" aria-hidden="true">
|
||||||
|
<svg v-if="c.icon === 'phone'" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M6.6 10.8c1.2 2.4 3.2 4.4 5.6 5.6l1.9-1.9c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.5.6.6 0 1 .4 1 1V19c0 .6-.4 1-1 1C10.4 20 4 13.6 4 5.5c0-.6.5-1 1.1-1h2.9c.6 0 1 .4 1 1 0 1.2.2 2.4.6 3.5.1.3 0 .7-.2 1l-1.8 1.8z"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="c.icon === 'mail'" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M4 6h16c.6 0 1 .4 1 1v10c0 .6-.4 1-1 1H4c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1zm8 7.2 7-4.7V7l-7 4.6L5 7v1.5l7 4.7z"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M8.7 4C4.9 4 1.8 6.6 1.8 9.8c0 1.8 1 3.4 2.7 4.5l-.6 2 2.2-1.1c.7.2 1.4.3 2.2.3h.5a5.6 5.6 0 0 1-.2-1.5c0-3.1 3-5.6 6.6-5.6h.5C15.2 5.6 12.3 4 8.7 4zm-2.3 3.1a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zm4.6 0a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8z"/>
|
||||||
|
<path d="M22.2 13.9c0-2.7-2.6-4.9-5.9-4.9s-5.9 2.2-5.9 4.9 2.6 4.9 5.9 4.9c.7 0 1.4-.1 2-.3l1.9.9-.5-1.7c1.5-.9 2.5-2.3 2.5-3.8zm-7.9-.9a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6zm4 0a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pf-foot-contact-text pf-foot-contact-copy"
|
||||||
|
:title="`复制${c.label} ${c.value}`"
|
||||||
|
@click="copyText(c.value, c.key)"
|
||||||
|
>
|
||||||
|
<span class="pf-foot-contact-label">{{ c.label }}</span>
|
||||||
|
<span class="pf-foot-contact-value">
|
||||||
|
{{ c.value }}
|
||||||
|
<em class="pf-foot-copy-hint">{{ copiedKey === c.key ? '已复制' : '复制' }}</em>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
企业微信:平时只占一行,点击向上浮出完整二维码。
|
||||||
|
原因见 script 里 qrOpen 上方的注释:二维码必须 246px 1:1 显示,
|
||||||
|
比右栏还宽,直接摆进去会把整栏拉宽、挤压其它列。
|
||||||
|
-->
|
||||||
|
<div ref="qrWrap" class="pf-foot-qr">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pf-foot-contact-text pf-foot-qr-trigger"
|
||||||
|
:class="{ 'is-open': qrOpen }"
|
||||||
|
:aria-expanded="qrOpen"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
@click="toggleQr"
|
||||||
|
>
|
||||||
|
<span class="pf-foot-contact-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M8.7 4C4.9 4 1.8 6.6 1.8 9.8c0 1.8 1 3.4 2.7 4.5l-.6 2 2.2-1.1c.7.2 1.4.3 2.2.3h.5a5.6 5.6 0 0 1-.2-1.5c0-3.1 3-5.6 6.6-5.6h.5C15.2 5.6 12.3 4 8.7 4zm-2.3 3.1a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zm4.6 0a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8z"/>
|
||||||
|
<path d="M22.2 13.9c0-2.7-2.6-4.9-5.9-4.9s-5.9 2.2-5.9 4.9 2.6 4.9 5.9 4.9c.7 0 1.4-.1 2-.3l1.9.9-.5-1.7c1.5-.9 2.5-2.3 2.5-3.8zm-7.9-.9a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6zm4 0a.8.8 0 1 1 0-1.6.8.8 0 0 1 0 1.6z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="pf-foot-contact-label">企业微信</span>
|
||||||
|
<span class="pf-foot-contact-value pf-foot-qr-value">
|
||||||
|
扫码添加
|
||||||
|
<em class="pf-foot-copy-hint">{{ qrOpen ? '收起' : '二维码' }}</em>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-show="qrOpen" class="pf-foot-qr-pop" role="dialog" aria-label="企业微信二维码">
|
||||||
|
<img
|
||||||
|
:src="withBase('/wechat-qr.png')"
|
||||||
|
alt="雪羊科技企业微信二维码,扫码添加好友"
|
||||||
|
width="246"
|
||||||
|
height="246"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
<span class="pf-foot-qr-pop-hint">微信扫一扫,添加好友</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="p-btn p-btn--primary pf-foot-btn" :href="withBase('/demo')">
|
||||||
|
进入系统演示
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 版权条 -->
|
||||||
|
<div class="pf-foot-bottom">
|
||||||
|
<span>Copyright © {{ year }} 雪羊科技 · All Rights Reserved.</span>
|
||||||
|
<span class="pf-foot-bottom-right">
|
||||||
|
<a :href="ORG_URL" target="_blank" rel="noopener">开源项目</a>
|
||||||
|
<i aria-hidden="true">·</i>
|
||||||
|
<span>Apache-2.0</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import DefaultTheme from 'vitepress/theme'
|
import DefaultTheme from 'vitepress/theme'
|
||||||
import type { Theme } from 'vitepress'
|
import type { Theme } from 'vitepress'
|
||||||
import { h, watch, nextTick } from 'vue'
|
import { h, watch, nextTick, computed } from 'vue'
|
||||||
import { useData, useRoute } from 'vitepress'
|
import { useData, useRoute } from 'vitepress'
|
||||||
|
|
||||||
import ProjectList from './components/ProjectList.vue'
|
import ProjectList from './components/ProjectList.vue'
|
||||||
@@ -8,6 +8,7 @@ import HomePage from './components/HomePage.vue'
|
|||||||
import CapabilityTabs from './components/CapabilityTabs.vue'
|
import CapabilityTabs from './components/CapabilityTabs.vue'
|
||||||
import FeatureGrid from './components/FeatureGrid.vue'
|
import FeatureGrid from './components/FeatureGrid.vue'
|
||||||
import ScreenshotPreview from './components/ScreenshotPreview.vue'
|
import ScreenshotPreview from './components/ScreenshotPreview.vue'
|
||||||
|
import PortalFooter from './components/PortalFooter.vue'
|
||||||
|
|
||||||
import { setupNavAnchorHighlight, applyNavAnchorHighlight } from './nav-anchor'
|
import { setupNavAnchorHighlight, applyNavAnchorHighlight } from './nav-anchor'
|
||||||
|
|
||||||
@@ -24,6 +25,37 @@ const PortalLayout = () => {
|
|||||||
|
|
||||||
const isPortal = frontmatter.value.layout === 'page'
|
const isPortal = frontmatter.value.layout === 'page'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否隐藏整站页脚。
|
||||||
|
*
|
||||||
|
* 判定原则:**文档型页面不挂页脚**。
|
||||||
|
* 这类页面有左侧目录树、右侧本页目录、底部上一页/下一页翻页,
|
||||||
|
* 读者正处在阅读流里,页脚那 450px 的品牌/联系/版权是纯噪音,
|
||||||
|
* 还会和翻页按钮挤在一起。
|
||||||
|
*
|
||||||
|
* 覆盖三种情况:
|
||||||
|
* 1) 路径命中项目手册 `/projects/<id>/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<string, unknown>
|
||||||
|
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 会告警),
|
* Layout 是函数式组件,onMounted 在这里不生效(Vue 会告警),
|
||||||
* 因此直接在渲染期间调度:等 DOM 落地后跑一次,再靠 MutationObserver 兜住后续变化。
|
* 因此直接在渲染期间调度:等 DOM 落地后跑一次,再靠 MutationObserver 兜住后续变化。
|
||||||
@@ -38,7 +70,11 @@ const PortalLayout = () => {
|
|||||||
() => nextTick(() => applyNavAnchorHighlight())
|
() => 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 {
|
export default {
|
||||||
|
|||||||
@@ -1122,3 +1122,511 @@ html.portal-nav-home .VPNavBarMenuLink.active:not([data-anchor-active]) {
|
|||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
title: 全部项目
|
title: 全部项目
|
||||||
description: 雪羊科技开源项目清单
|
description: 雪羊科技开源项目清单
|
||||||
outline: deep
|
outline: deep
|
||||||
|
# 本页不挂整站页脚:清单页以「浏览项目」为主,底部紧接项目卡片收尾即可,
|
||||||
|
# 再叠一层品牌/联系/版权会把重点冲淡。
|
||||||
|
footer: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# 全部项目
|
# 全部项目
|
||||||
|
|||||||
Reference in New Issue
Block a user