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>
|