主题功能同步:vivid 页脚补社交按钮排(含源码入口,居中);classic 文章页补右栏目录(滚动高亮)与顶部阅读进度条——快发盒经核实两套 UI 均已存在,无需补
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { publicApi } from '../api'
|
import { publicApi } from '../api'
|
||||||
import { site } from '../site'
|
import { site } from '../site'
|
||||||
import { socialIcon } from '../socialIcons'
|
import { socialIcon } from '../socialIcons'
|
||||||
@@ -8,6 +8,35 @@ import { relativeDate, stripTags, thumbURL } from '../utils'
|
|||||||
// vivid 是单栏布局,右栏整体不渲染(省掉 latest/tags 两次请求)。
|
// vivid 是单栏布局,右栏整体不渲染(省掉 latest/tags 两次请求)。
|
||||||
const isVivid = computed(() => site.ui_id === 'vivid')
|
const isVivid = computed(() => site.ui_id === 'vivid')
|
||||||
|
|
||||||
|
// 文章页目录(PostView 传入 headings;空数组时不渲染这张卡)。
|
||||||
|
// 高亮跟随滚动:视口上方最近的那个标题算「当前」。
|
||||||
|
const props = defineProps({
|
||||||
|
toc: { type: Array, default: () => [] }
|
||||||
|
})
|
||||||
|
const activeId = ref('')
|
||||||
|
let tocObserver = null
|
||||||
|
|
||||||
|
function watchToc() {
|
||||||
|
if (tocObserver) tocObserver.disconnect()
|
||||||
|
if (!props.toc.length) return
|
||||||
|
tocObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.isIntersecting) activeId.value = e.target.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '0px 0px -65% 0px' }
|
||||||
|
)
|
||||||
|
nextTick(() => {
|
||||||
|
for (const t of props.toc) {
|
||||||
|
const el = document.getElementById(t.id)
|
||||||
|
if (el) tocObserver.observe(el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
watch(() => props.toc, watchToc, { deep: false })
|
||||||
|
onBeforeUnmount(() => tocObserver && tocObserver.disconnect())
|
||||||
|
|
||||||
// 项目源码仓库是固定入口(本站 git remote),不来自后台配置 —— 和版权一样
|
// 项目源码仓库是固定入口(本站 git remote),不来自后台配置 —— 和版权一样
|
||||||
// 固定渲染在自己的那排;后台填的社交账号单独一排,URL 撞车的去重。
|
// 固定渲染在自己的那排;后台填的社交账号单独一排,URL 撞车的去重。
|
||||||
const PROJECT_REPO = 'https://git.gopher.ink/mirrors2/ONE'
|
const PROJECT_REPO = 'https://git.gopher.ink/mirrors2/ONE'
|
||||||
@@ -62,6 +91,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside v-if="!isVivid" class="rail-right">
|
<aside v-if="!isVivid" class="rail-right">
|
||||||
|
<section v-if="toc.length" class="card toc-card">
|
||||||
|
<p class="eyebrow">目录</p>
|
||||||
|
<ul class="toc-list">
|
||||||
|
<li v-for="t in toc" :key="t.id" :class="{ h3: t.level >= 3, on: activeId === t.id }">
|
||||||
|
<a :href="'#' + t.id" :class="{ on: activeId === t.id }">{{ t.text }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="eyebrow">关于这里</p>
|
<p class="eyebrow">关于这里</p>
|
||||||
<p class="bio">{{ site.author_bio || site.site_desc }}</p>
|
<p class="bio">{{ site.author_bio || site.site_desc }}</p>
|
||||||
@@ -174,6 +212,24 @@ onMounted(async () => {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文章目录:当前小节高亮,层级缩进 */
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.toc-list li {margin: 2px 0;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
padding-left: 8px;}
|
||||||
|
.toc-list li.h3 {padding-left: 20px;
|
||||||
|
font-size: 12.5px;}
|
||||||
|
.toc-list li.on {border-left-color: var(--accent);}
|
||||||
|
.toc-list a {color: var(--muted);
|
||||||
|
text-decoration: none;}
|
||||||
|
.toc-list li.on a, .toc-list a:hover {color: var(--accent);}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
|
|||||||
@@ -1451,3 +1451,14 @@ a.cm-name-a:hover {color: var(--accent);}
|
|||||||
比例存进 markdown 的 alt)。默认半透明让访客知道它在那儿。 */
|
比例存进 markdown 的 alt)。默认半透明让访客知道它在那儿。 */
|
||||||
.admin-shell .image-resize-handle {opacity: 0.45;}
|
.admin-shell .image-resize-handle {opacity: 0.45;}
|
||||||
.admin-shell .image-wrapper:hover .image-resize-handle {opacity: 1;}
|
.admin-shell .image-wrapper:hover .image-resize-handle {opacity: 1;}
|
||||||
|
|
||||||
|
/* classic 文章页阅读进度条:视口顶部细线,vivid 的同款能力 */
|
||||||
|
.reading-bar {position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 0;
|
||||||
|
background: var(--accent);
|
||||||
|
z-index: 60;
|
||||||
|
transition: width .1s linear;
|
||||||
|
pointer-events: none;}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { site } from '../../site'
|
import { site } from '../../site'
|
||||||
|
import { socialIcon } from '../../socialIcons'
|
||||||
|
|
||||||
|
// 社交按钮与 classic 页脚同源:后台「社交链接」+ 固定源码仓库入口
|
||||||
|
const PROJECT_REPO = 'https://git.gopher.ink/mirrors2/ONE'
|
||||||
|
const social = (site.social_links || []).concat([{ label: '源码', url: PROJECT_REPO }])
|
||||||
|
.filter((s) => s && s.url)
|
||||||
|
|
||||||
// 余白页脚。对应设计稿的 <footer class="foot">。
|
// 余白页脚。对应设计稿的 <footer class="foot">。
|
||||||
//
|
//
|
||||||
@@ -21,6 +27,20 @@ const links = [
|
|||||||
<template>
|
<template>
|
||||||
<footer class="foot">
|
<footer class="foot">
|
||||||
<div class="quote">What’s left unsaid holds the most weight.</div>
|
<div class="quote">What’s left unsaid holds the most weight.</div>
|
||||||
|
<div v-if="social.length" class="foot-social">
|
||||||
|
<a
|
||||||
|
v-for="s in social"
|
||||||
|
:key="s.label + s.url"
|
||||||
|
:href="s.url"
|
||||||
|
:title="s.label"
|
||||||
|
:aria-label="s.label"
|
||||||
|
:target="s.url.startsWith('http') ? '_blank' : undefined"
|
||||||
|
:rel="s.url.startsWith('http') ? 'noopener' : undefined"
|
||||||
|
>
|
||||||
|
<svg v-if="socialIcon(s.url)" viewBox="0 0 24 24" aria-hidden="true"><path :d="socialIcon(s.url)" /></svg>
|
||||||
|
<span v-else>{{ s.label }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div>{{ site.footer_note }}<template v-if="site.icp"> · {{ site.icp }}</template></div>
|
<div>{{ site.footer_note }}<template v-if="site.icp"> · {{ site.icp }}</template></div>
|
||||||
<div class="foot-nav">
|
<div class="foot-nav">
|
||||||
<template v-for="(l, i) in links" :key="l.to">
|
<template v-for="(l, i) in links" :key="l.to">
|
||||||
|
|||||||
@@ -169,6 +169,15 @@ html[data-ui='vivid'] .foot {max-width:1100px;margin:40px auto 0;padding:28px 28
|
|||||||
border-top:1px solid var(--line);
|
border-top:1px solid var(--line);
|
||||||
text-align:center;color:var(--muted);font-size:12.5px;line-height:2;}
|
text-align:center;color:var(--muted);font-size:12.5px;line-height:2;}
|
||||||
html[data-ui='vivid'] .foot a {color:var(--accent);font-weight:600;transition:opacity .25s}
|
html[data-ui='vivid'] .foot a {color:var(--accent);font-weight:600;transition:opacity .25s}
|
||||||
|
/* 页脚社交排:图标居中一行,与 classic 页脚同步 */
|
||||||
|
html[data-ui='vivid'] .foot-social {display:flex;justify-content:center;align-items:center;
|
||||||
|
gap:18px;margin:14px 0 18px;}
|
||||||
|
html[data-ui='vivid'] .foot-social a {color:var(--soft);font-weight:400;
|
||||||
|
display:inline-flex;align-items:center;justify-content:center;
|
||||||
|
width:30px;height:30px;border-radius:8px;transition:color .25s,background .25s;}
|
||||||
|
html[data-ui='vivid'] .foot-social a:hover {color:var(--accent);background:var(--accent-soft);}
|
||||||
|
html[data-ui='vivid'] .foot-social svg {width:17px;height:17px;fill:currentColor;}
|
||||||
|
html[data-ui='vivid'] .foot-social span {font-size:12px;}
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
html[data-ui='vivid'] .foot a:hover {opacity:.75}
|
html[data-ui='vivid'] .foot a:hover {opacity:.75}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import LeftNav from '../components/LeftNav.vue'
|
import LeftNav from '../components/LeftNav.vue'
|
||||||
import RightRail from '../components/RightRail.vue'
|
import RightRail from '../components/RightRail.vue'
|
||||||
@@ -33,6 +33,14 @@ function withHeadingIds(html) {
|
|||||||
const contentHtml = ref('')
|
const contentHtml = ref('')
|
||||||
const headings = ref([])
|
const headings = ref([])
|
||||||
|
|
||||||
|
// 顶部阅读进度条(classic):滚动过正文才算数,短文太短不显示
|
||||||
|
const progress = ref(0)
|
||||||
|
function onScroll() {
|
||||||
|
const doc = document.documentElement
|
||||||
|
const total = doc.scrollHeight - window.innerHeight
|
||||||
|
progress.value = total > 300 ? Math.min(100, (window.scrollY / total) * 100) : 0
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -52,7 +60,11 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll))
|
||||||
watch(() => route.params.slug, load)
|
watch(() => route.params.slug, load)
|
||||||
|
|
||||||
const isShort = computed(() => post.value && post.value.kind === 'short')
|
const isShort = computed(() => post.value && post.value.kind === 'short')
|
||||||
@@ -71,6 +83,8 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-else class="shell">
|
<div v-else class="shell">
|
||||||
|
<!-- 阅读进度:贴在视口顶部的细线 -->
|
||||||
|
<div class="reading-bar" :style="{ width: progress + '%' }" aria-hidden="true"></div>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<!-- LeftNav / RightRail 之前 import 了却没渲染,导致整页掉进 236px 的左列。
|
<!-- LeftNav / RightRail 之前 import 了却没渲染,导致整页掉进 236px 的左列。
|
||||||
补上,和 HomeView 的三栏一致。 -->
|
补上,和 HomeView 的三栏一致。 -->
|
||||||
@@ -146,7 +160,7 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
|||||||
<!-- 评论区(和 vivid 共用同一个组件,见 components/comments/) -->
|
<!-- 评论区(和 vivid 共用同一个组件,见 components/comments/) -->
|
||||||
<CommentSection v-if="post" :post-id="post.id" />
|
<CommentSection v-if="post" :post-id="post.id" />
|
||||||
</main>
|
</main>
|
||||||
<RightRail />
|
<RightRail :toc="isShort ? [] : headings" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user