AI-News/frontend/app/layouts/article.vue
2025-12-04 10:04:21 +08:00

404 lines
12 KiB
Vue
Raw 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.

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