Files
Dai0814 e4757446cf
mirror-to-gitee / mirror (push) Successful in 4s
feat(portal): 补入整站页脚,并让项目列表页不显示页脚
用户反馈 /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,渐变背景正常)
- 文档页(详情/对比/快速开始/手册)均无页脚
- 自定义整页(关于/联系/演示/官方文档)均有页脚
2026-11-06 13:39:41 +08:00

275 lines
11 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>