404 lines
12 KiB
Vue
404 lines
12 KiB
Vue
|
|
<!-- layouts/article.vue -->
|
|||
|
|
<template>
|
|||
|
|
<div ref="rootRef" class="article-root">
|
|||
|
|
<main ref="gridRef" class="a-grid" :style="{ '--rightw': rightwStyle }">
|
|||
|
|
<!-- 左列:自带滚动条(包含:固定导航 + 文章内容?-->
|
|||
|
|
<section class="a-left">
|
|||
|
|
<!-- 固定(sticky)白色毛玻璃导航:跟着左列滚动 -->
|
|||
|
|
<header class="glass-topbar">
|
|||
|
|
<div class="shell">
|
|||
|
|
<div class="glass-bar">
|
|||
|
|
<NuxtLink to="/" class="brand" aria-label="Aivise">
|
|||
|
|
<span class="dot" />
|
|||
|
|
<span class="brand-text">Aivise</span>
|
|||
|
|
</NuxtLink>
|
|||
|
|
|
|||
|
|
<nav class="menu" aria-label="主菜单">
|
|||
|
|
<NuxtLink
|
|||
|
|
v-for="m in menus"
|
|||
|
|
:key="m.to"
|
|||
|
|
:to="m.to"
|
|||
|
|
class="link"
|
|||
|
|
:class="{ active: isActive(m.to) }"
|
|||
|
|
>{{ m.label }}</NuxtLink>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<div class="actions">
|
|||
|
|
<a class="pill" href="#" @click.prevent>领取福利</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<!-- 文章内容(随左列滚动?-->
|
|||
|
|
<div class="a-article">
|
|||
|
|
<slot />
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<!-- 右列:内嵌评论(使用项目里的 RightDrawer.vue?-->
|
|||
|
|
<aside ref="asideRef" :class="['a-right', { collapsed: isCollapsedRef }]" >
|
|||
|
|
<template v-if="drawerReady">
|
|||
|
|
<RightDrawer
|
|||
|
|
class="rd-embed"
|
|||
|
|
:teleport="false"
|
|||
|
|
:append-to-body="false"
|
|||
|
|
:to="null"
|
|||
|
|
:mask="false"
|
|||
|
|
:fixed="false"
|
|||
|
|
:open="true"
|
|||
|
|
:visible="true"
|
|||
|
|
:model-value="true"
|
|||
|
|
width="100%"
|
|||
|
|
height="100%"
|
|||
|
|
/>
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
<div class="rd-placeholder" aria-hidden="true" />
|
|||
|
|
</template>
|
|||
|
|
</aside>
|
|||
|
|
</main>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
|
|||
|
|
import { useRoute, useRouter } from 'vue-router'
|
|||
|
|
import RightDrawer from '../components/RightDrawer.vue'
|
|||
|
|
import { useRightDrawer } from '@/composables/useRightDrawer'
|
|||
|
|
|
|||
|
|
const route = useRoute()
|
|||
|
|
const router = useRouter()
|
|||
|
|
const drawerState = useRightDrawer()
|
|||
|
|
|
|||
|
|
const menus = [
|
|||
|
|
{ label: '首页', to: '/' },
|
|||
|
|
{ label: '资讯广场', to: '/market' },
|
|||
|
|
{ label: '社区', to: '/community' },
|
|||
|
|
{ label: '使用教程', to: '/docs' },
|
|||
|
|
|
|||
|
|
]
|
|||
|
|
const isActive = (to) => (to === '/' ? route.path === '/' : route.path.startsWith(to))
|
|||
|
|
|
|||
|
|
/* ====== 同步右栏真实宽度?CSS 变量 --rightw(保持你的自适应列宽?====== */
|
|||
|
|
const rootRef = ref(null)
|
|||
|
|
const gridRef = ref(null)
|
|||
|
|
const asideRef = ref(null)
|
|||
|
|
let ro
|
|||
|
|
const drawerReady = ref(false)
|
|||
|
|
const lastExpandedWidth = ref(420)
|
|||
|
|
const isCollapsedRef = drawerState.isCollapsed || ref(false)
|
|||
|
|
const COLLAPSED_W = 60
|
|||
|
|
const rightwStyle = computed(() => {
|
|||
|
|
if (isCollapsedRef.value) return `${COLLAPSED_W}px`
|
|||
|
|
const w = Math.max(120, Math.round(lastExpandedWidth.value || 420))
|
|||
|
|
return `${w}px`
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function setRightWidth(px) {
|
|||
|
|
const w = Number.isFinite(px) && px > 80 ? Math.round(px) : lastExpandedWidth.value
|
|||
|
|
if (w) lastExpandedWidth.value = w
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ====== ?出场动画 ====== */
|
|||
|
|
const ENTER_MS = 160
|
|||
|
|
const LEAVE_MS = 260 // 整页缩写动画时间
|
|||
|
|
|
|||
|
|
let removeGuard
|
|||
|
|
|
|||
|
|
function playEnterAnimation () {
|
|||
|
|
// 维持右栏的轻弹入(可要可不要,保持你现状?
|
|||
|
|
const aside = asideRef.value
|
|||
|
|
if (!aside) return
|
|||
|
|
aside.classList.add('pop-seed')
|
|||
|
|
requestAnimationFrame(() => {
|
|||
|
|
aside.classList.add('pop-in')
|
|||
|
|
setTimeout(() => {
|
|||
|
|
aside.classList.remove('pop-seed')
|
|||
|
|
aside.classList.remove('pop-in')
|
|||
|
|
}, ENTER_MS + 20)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function playPageShrinkLeave () {
|
|||
|
|
// 整页缩写 ?消失
|
|||
|
|
return new Promise(resolve => {
|
|||
|
|
const root = rootRef.value
|
|||
|
|
const grid = gridRef.value
|
|||
|
|
if (!root) return resolve()
|
|||
|
|
|
|||
|
|
// 防止过程中布局抖动:锁一下右列当前宽?
|
|||
|
|
if (grid) {
|
|||
|
|
const cur = getComputedStyle(grid).getPropertyValue('--rightw')
|
|||
|
|
if (cur) grid.style.setProperty('--rightw', cur.trim())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
root.classList.add('page-leave')
|
|||
|
|
setTimeout(() => {
|
|||
|
|
root.classList.remove('page-leave')
|
|||
|
|
resolve()
|
|||
|
|
}, LEAVE_MS)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
drawerReady.value = true
|
|||
|
|
// 防止遗留的离场状态导致页面不可见
|
|||
|
|
if (rootRef.value?.classList.contains('page-leave')) {
|
|||
|
|
rootRef.value.classList.remove('page-leave')
|
|||
|
|
}
|
|||
|
|
// 初始宽度兜底
|
|||
|
|
const existed = parseFloat(getComputedStyle(gridRef.value).getPropertyValue('--rightw'))
|
|||
|
|
const init = Number.isFinite(existed) && existed > 80 ? existed : 420
|
|||
|
|
lastExpandedWidth.value = init
|
|||
|
|
|
|||
|
|
// 观察右栏内容宽度变化(折?展开?
|
|||
|
|
const target = asideRef.value?.firstElementChild || asideRef.value
|
|||
|
|
if (target && 'ResizeObserver' in window) {
|
|||
|
|
ro = new ResizeObserver(entries => {
|
|||
|
|
for (const e of entries) {
|
|||
|
|
const w = e.contentRect?.width ?? target.offsetWidth
|
|||
|
|
// 离开动画期间不再同步,避免冲突
|
|||
|
|
if (rootRef.value?.classList.contains('page-leave')) continue
|
|||
|
|
if (!isCollapsedRef.value && w > 80) {
|
|||
|
|
lastExpandedWidth.value = w
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
ro.observe(target)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await nextTick()
|
|||
|
|
playEnterAnimation()
|
|||
|
|
|
|||
|
|
// 禁用路由守卫中的离场动画,使用共享元素转场代替
|
|||
|
|
// removeGuard = router.beforeEach(async (to, from) => {
|
|||
|
|
// const toArticle = String(to?.path || '').startsWith('/articles/')
|
|||
|
|
// if (from.fullPath === route.fullPath && !toArticle) {
|
|||
|
|
// await playPageShrinkLeave()
|
|||
|
|
// if (removeGuard) { removeGuard(); removeGuard = undefined }
|
|||
|
|
// }
|
|||
|
|
// return true
|
|||
|
|
// })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
if (ro) ro.disconnect()
|
|||
|
|
if (removeGuard) { removeGuard(); removeGuard = undefined }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 抽屉收起/展开时动态调整右栏宽度:通过 rightwStyle 绑定
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
/* ===== 页面级:整页不滚,把滚动交给左列 ===== */
|
|||
|
|
.article-root{
|
|||
|
|
background: var(--soft-page-bg);
|
|||
|
|
height: 100vh;
|
|||
|
|
/* 将滚动限制在左侧文章区,右侧抽屉保持固定 */
|
|||
|
|
overflow: hidden;
|
|||
|
|
/* 为缩写动画准?*/
|
|||
|
|
will-change: transform, opacity, border-radius, box-shadow;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 右侧留“约 5%”呼吸间距;右列宽度?--rightw 决定(默?clamp?*/
|
|||
|
|
.a-grid{
|
|||
|
|
--rightw: clamp(380px, 35vw, 480px);
|
|||
|
|
height: 100%;
|
|||
|
|
width: 103%;
|
|||
|
|
margin: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
padding: 0px clamp(24px, 5vw, 50px) 14px 8px; /* ?| ?| ?| ?*/
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) var(--rightw);
|
|||
|
|
gap: 0px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ===== 左列(滚动容器) ===== */
|
|||
|
|
.a-left{
|
|||
|
|
min-width: 0;
|
|||
|
|
height: 100%;
|
|||
|
|
overflow: auto;
|
|||
|
|
-webkit-overflow-scrolling: touch;
|
|||
|
|
overscroll-behavior: contain;
|
|||
|
|
scroll-behavior: smooth;
|
|||
|
|
scroll-padding-top: 64px;
|
|||
|
|
padding-right: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 顶部白色毛玻璃胶囊条 */
|
|||
|
|
.glass-topbar{
|
|||
|
|
position: sticky; top: 0; z-index: 30;
|
|||
|
|
padding-top: 6px; margin-bottom: 12px;
|
|||
|
|
background: linear-gradient(to bottom, rgba(255,255,255,.85), rgba(255,255,255,0));
|
|||
|
|
}
|
|||
|
|
.shell{ max-width: 100%; margin: 0; padding: 0; }
|
|||
|
|
.glass-bar{
|
|||
|
|
height: 56px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto 1fr auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
padding: 0 16px;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background:
|
|||
|
|
radial-gradient(120% 160% at 50% 0%, rgba(255,255,255,.92), rgba(255,255,255,.72) 70%),
|
|||
|
|
rgba(255,255,255,.75);
|
|||
|
|
border: 1px solid rgba(17,24,39,.08);
|
|||
|
|
backdrop-filter: blur(12px);
|
|||
|
|
-webkit-backdrop-filter: blur(12px);
|
|||
|
|
box-shadow: 0 6px 24px rgba(17,24,39,.06);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 品牌 */
|
|||
|
|
.brand{ display:inline-flex; align-items:center; gap:10px; text-decoration:none; }
|
|||
|
|
.dot{ width:30px; height:30px; border-radius:12px; background: linear-gradient(135deg,#6da2ff 0%, #7733ff 100%); }
|
|||
|
|
.brand-text{ color:#111827; font-weight:800; letter-spacing:.4px; font-size:18px; }
|
|||
|
|
|
|||
|
|
/* 菜单 */
|
|||
|
|
.menu{ min-width: 0; display:flex; gap: 26px; justify-content:center; align-items:center; }
|
|||
|
|
.link{
|
|||
|
|
position: relative; color: rgba(17,24,39,.82);
|
|||
|
|
text-decoration: none; font-weight: 600; letter-spacing:.2px;
|
|||
|
|
padding: 8px 2px; transition: color .15s ease; white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.link:hover{ color:#111827; }
|
|||
|
|
.link.active{ color:#0f172a; }
|
|||
|
|
.link.active::after{
|
|||
|
|
content:""; position:absolute; left: 6px; right: 6px; bottom: 2px; height: 6px; border-radius: 6px;
|
|||
|
|
background:
|
|||
|
|
radial-gradient(16px 8px at 20% 50%, rgba(99,102,241,.35), transparent 70%),
|
|||
|
|
radial-gradient(16px 8px at 80% 50%, rgba(99,102,241,.35), transparent 70%),
|
|||
|
|
linear-gradient(90deg, rgba(99,102,241,.45), rgba(99,102,241,.15));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 右侧操作按钮(导航右侧) */
|
|||
|
|
.actions{ display:flex; gap: 12px; }
|
|||
|
|
.pill{
|
|||
|
|
display:inline-flex; align-items:center; justify-content:center;
|
|||
|
|
height: 40px; padding: 0 18px;
|
|||
|
|
text-decoration:none; color:#0f172a; font-weight:700;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
border: 1px solid rgba(17,24,39,.12);
|
|||
|
|
background: rgba(255,255,255,.7);
|
|||
|
|
backdrop-filter: blur(8px);
|
|||
|
|
transition: background .15s ease, border-color .15s ease, transform .12s ease;
|
|||
|
|
}
|
|||
|
|
.pill:hover{
|
|||
|
|
background: rgba(255,255,255,.95);
|
|||
|
|
border-color: rgba(17,24,39,.22);
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
.pill:active{ transform: translateY(0); }
|
|||
|
|
|
|||
|
|
/* ===== 右列:内嵌评论容?===== */
|
|||
|
|
.a-right{
|
|||
|
|
min-width: 0;
|
|||
|
|
height: 100%;
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
align-self: start;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-left: 1px solid #eef0f3;
|
|||
|
|
background: var(--soft-page-bg);
|
|||
|
|
display:flex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ?RightDrawer 在右栏中 100% 填充 */
|
|||
|
|
.rd-embed{
|
|||
|
|
height: 100%;
|
|||
|
|
display:flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
.rd-placeholder{
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 文章与导航间?*/
|
|||
|
|
.a-article{ padding-top: 2px; }
|
|||
|
|
|
|||
|
|
/* ===== 自定义滚动条(左列) ===== */
|
|||
|
|
:root{
|
|||
|
|
--sb-track: transparent;
|
|||
|
|
--sb-thumb: #d1d5db;
|
|||
|
|
--sb-thumb-hover: #9ca3af;
|
|||
|
|
--sb-corner: transparent;
|
|||
|
|
}
|
|||
|
|
.a-left{
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
scrollbar-color: var(--sb-thumb) var(--sb-track);
|
|||
|
|
}
|
|||
|
|
.a-left::-webkit-scrollbar{ width:10px; height:10px; }
|
|||
|
|
.a-left::-webkit-scrollbar-track{ background: var(--sb-track); }
|
|||
|
|
.a-left::-webkit-scrollbar-thumb{
|
|||
|
|
background-color: var(--sb-thumb);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
border: 2px solid var(--sb-track);
|
|||
|
|
}
|
|||
|
|
.a-left:hover::-webkit-scrollbar-thumb{ background-color: var(--sb-thumb-hover); }
|
|||
|
|
.a-left::-webkit-scrollbar-corner{ background: var(--sb-corner); }
|
|||
|
|
|
|||
|
|
/* 小屏:隐藏右列,仅左列(可滚?*/
|
|||
|
|
@media (max-width: 960px){
|
|||
|
|
.a-grid{
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
padding: 8px;
|
|||
|
|
}
|
|||
|
|
.a-right{ display: none; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ====== 右栏轻弹入(保留原来的灵动) ====== */
|
|||
|
|
.a-right.pop-seed .rd-embed{
|
|||
|
|
transform: translateX(14px) scale(0.985);
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
.a-right.pop-in .rd-embed{
|
|||
|
|
transition: transform .16s cubic-bezier(.2,.8,.2,1),
|
|||
|
|
opacity .16s cubic-bezier(.2,.8,.2,1);
|
|||
|
|
transform: translateX(0) scale(1);
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ====== 整页“缩写消失”离场动?====== */
|
|||
|
|
.article-root.page-leave{
|
|||
|
|
/* 向右上角缩写:基点靠近右上,略微位移更“像回收?*/
|
|||
|
|
transform-origin: 96% 6%;
|
|||
|
|
animation: pageZoomOut .26s cubic-bezier(.22,1,.36,1) forwards;
|
|||
|
|
/* 可选淡出背景的投影?*/
|
|||
|
|
box-shadow: 0 20px 60px rgba(17,24,39,.10);
|
|||
|
|
border-radius: 0; /* ?0 过渡?16px */
|
|||
|
|
}
|
|||
|
|
@keyframes pageZoomOut{
|
|||
|
|
0%{
|
|||
|
|
transform: translate(0,0) scale(1);
|
|||
|
|
opacity: 1;
|
|||
|
|
border-radius: 0px;
|
|||
|
|
}
|
|||
|
|
100%{
|
|||
|
|
transform: translate(14px, -10px) scale(.86);
|
|||
|
|
opacity: 0;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<!-- 兜底样式(非 scoped):确保 RightDrawer 内嵌 -->
|
|||
|
|
<style>
|
|||
|
|
.right-drawer, .rd-root, .drawer-root,
|
|||
|
|
.right-drawer__panel, .rd-panel, .drawer-panel{
|
|||
|
|
position: static !important;
|
|||
|
|
inset: auto !important;
|
|||
|
|
width: 100% !important;
|
|||
|
|
height: 100% !important;
|
|||
|
|
max-width: 100% !important;
|
|||
|
|
box-shadow: none !important;
|
|||
|
|
border: none !important;
|
|||
|
|
}
|
|||
|
|
.right-drawer__mask, .rd-mask, .drawer-mask,
|
|||
|
|
.right-drawer--fixed, .rd-fixed, .drawer-fixed{
|
|||
|
|
display: none !important;
|
|||
|
|
}
|
|||
|
|
.right-drawer, .rd-root, .drawer-root{ z-index: 1 !important; }
|
|||
|
|
</style>
|