AI-News/frontend/app/layouts/article.vue

404 lines
12 KiB
Vue
Raw Normal View History

2025-12-04 10:04:21 +08:00
<!-- 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>