438 lines
12 KiB
Vue
438 lines
12 KiB
Vue
<template>
|
||
<div id="app" class="app">
|
||
<!-- 顶部主导航 -->
|
||
<AppHeader
|
||
:theme="themeStore.theme"
|
||
:menu-open="menuOpen"
|
||
@toggleTheme="themeStore.toggleTheme()"
|
||
@random="handleRandom"
|
||
@toggleMenu="menuOpen = !menuOpen"
|
||
/>
|
||
|
||
<!-- 移动端抽屉菜单 -->
|
||
<div class="mobile-drawer" :class="{ open: menuOpen }" @click.self="menuOpen = false">
|
||
<div class="drawer-content">
|
||
<AppSidebar
|
||
:types="allTypes"
|
||
:current-type="null"
|
||
@selectType="handleDrawerSelectType"
|
||
@random="handleRandom"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域: 左副导航 + 主内容 + 右信息栏 -->
|
||
<div class="content-wrapper">
|
||
<!-- 左侧边栏(仅大屏幕显示) -->
|
||
<div class="left-sidebar">
|
||
<AppSidebar
|
||
:types="allTypes"
|
||
:current-type="null"
|
||
@selectType="() => {}"
|
||
@random="handleRandom"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 左侧副导航(平板及以上显示) -->
|
||
<AppSubNav
|
||
class="left-subnav"
|
||
:types="allTypes"
|
||
:crowds="allCrowds"
|
||
:current-type-ids="filterTypeIds"
|
||
:current-crowd-ids="filterCrowdIds"
|
||
:show="showSubNav"
|
||
@select-type="handleSelectType"
|
||
@select-crowd="handleSelectCrowd"
|
||
@random="handleRandom"
|
||
/>
|
||
|
||
<!-- 主内容路由区 -->
|
||
<main class="main-content">
|
||
<router-view />
|
||
</main>
|
||
|
||
<!-- 右侧信息栏(仅大屏幕) -->
|
||
<AppRightbar
|
||
class="rightbar"
|
||
v-if="showRightBar"
|
||
:hot-jokes="hotJokes"
|
||
:types="allTypes"
|
||
:crowds="allCrowds"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 底部 -->
|
||
<Footer />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, provide, onMounted, computed, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import AppHeader from '@/components/AppHeader.vue'
|
||
import AppSubNav from '@/components/AppSubNav.vue'
|
||
import AppRightbar from '@/components/AppRightbar.vue'
|
||
import Footer from '@/components/Footer.vue'
|
||
import { getTypes, getCrowds } from '@/api/category'
|
||
import { getJokes } from '@/api/joke'
|
||
import { useThemeStore } from '@/stores/theme'
|
||
|
||
const themeStore = useThemeStore()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// 移动端菜单状态
|
||
const menuOpen = ref(false)
|
||
|
||
// 初始化主题
|
||
themeStore.initTheme()
|
||
|
||
// 共享分类数据
|
||
const allTypes = ref([])
|
||
const allCrowds = ref([])
|
||
|
||
// 当前筛选状态(支持多选)
|
||
const filterTypeIds = ref([])
|
||
const filterCrowdIds = ref([])
|
||
|
||
// 热门笑话(右侧栏)
|
||
const hotJokes = ref([])
|
||
|
||
// 右侧栏:仅首页显示
|
||
const showRightBar = computed(() => route.path === '/')
|
||
// 副导航:首页/分类/搜索显示,详情页显示推荐列表
|
||
const showSubNav = computed(() => !route.path.startsWith('/detail'))
|
||
|
||
// 函数必须先定义,才能在 provide 中使用
|
||
const handleRandom = async () => {
|
||
try {
|
||
const res = await getJokes({ page: 1, page_size: 50 })
|
||
if (res.items && res.items.length > 0) {
|
||
const rnd = res.items[Math.floor(Math.random() * res.items.length)]
|
||
router.push(`/detail/${rnd.id}`)
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
const handleSelectType = (typeId) => {
|
||
// 单选:点同一个取消,点别的切换
|
||
if (filterTypeIds.value.length === 1 && filterTypeIds.value[0] === typeId) {
|
||
filterTypeIds.value = []
|
||
} else {
|
||
filterTypeIds.value = [typeId]
|
||
}
|
||
filterCrowdIds.value = []
|
||
const query = filterTypeIds.value.length ? { type_ids: filterTypeIds.value.join(',') } : {}
|
||
router.push({ path: '/', query })
|
||
}
|
||
|
||
const handleSelectCrowd = (crowdId) => {
|
||
// 单选:点同一个取消,点别的切换
|
||
if (filterCrowdIds.value.length === 1 && filterCrowdIds.value[0] === crowdId) {
|
||
filterCrowdIds.value = []
|
||
} else {
|
||
filterCrowdIds.value = [crowdId]
|
||
}
|
||
filterTypeIds.value = []
|
||
const query = filterCrowdIds.value.length ? { crowd_ids: filterCrowdIds.value.join(',') } : {}
|
||
router.push({ path: '/', query })
|
||
// 移动端关闭菜单
|
||
menuOpen.value = false
|
||
}
|
||
|
||
// 抽屉菜单选择分类(单选)
|
||
const handleDrawerSelectType = (typeId) => {
|
||
if (filterTypeIds.value.length === 1 && filterTypeIds.value[0] === typeId) {
|
||
filterTypeIds.value = []
|
||
} else {
|
||
filterTypeIds.value = [typeId]
|
||
}
|
||
filterCrowdIds.value = []
|
||
const query = filterTypeIds.value.length ? { type_ids: filterTypeIds.value.join(',') } : {}
|
||
router.push({ path: '/', query })
|
||
// 移动端关闭菜单
|
||
menuOpen.value = false
|
||
}
|
||
|
||
// provide 必须在函数定义之后
|
||
provide('filterTypeIds', filterTypeIds)
|
||
provide('filterCrowdIds', filterCrowdIds)
|
||
provide('openRandom', handleRandom)
|
||
provide('allTypes', allTypes)
|
||
provide('allCrowds', allCrowds)
|
||
|
||
// 同步 URL query 到 filter state
|
||
watch(() => route.query, (q) => {
|
||
filterTypeIds.value = q.type_ids ? String(q.type_ids).split(',').map(Number) : []
|
||
filterCrowdIds.value = q.crowd_ids ? String(q.crowd_ids).split(',').map(Number) : []
|
||
}, { immediate: true })
|
||
|
||
onMounted(async () => {
|
||
console.log('[APP] mounting, route:', route.path)
|
||
try {
|
||
allTypes.value = await getTypes()
|
||
allCrowds.value = await getCrowds()
|
||
console.log('[APP] categories loaded:', allTypes.value.length, allCrowds.value.length)
|
||
} catch (e) {
|
||
console.warn('[APP] Failed to load categories:', e)
|
||
}
|
||
|
||
try {
|
||
const res = await getJokes({ page: 1, page_size: 30 })
|
||
console.log('[APP] jokes loaded:', res.items?.length, 'total:', res.total)
|
||
const sorted = ((res.items || []).sort((a, b) => (b.view_count || 0) - (a.view_count || 0)))
|
||
hotJokes.value = sorted.slice(0, 5)
|
||
} catch (e) {
|
||
console.warn('[APP] Failed to load hot jokes:', e)
|
||
hotJokes.value = []
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
/* ============================================================
|
||
双主题 CSS 变量架构
|
||
============================================================ */
|
||
|
||
/* 浅色主题(默认) */
|
||
:root,
|
||
:root[data-theme="light"] {
|
||
--primary: #FF6B00;
|
||
--primary-light: #FF8C30;
|
||
--primary-dark: #E55A00;
|
||
--bg-page: #f0f2f5;
|
||
--bg-card: #ffffff;
|
||
--bg-subnav: #ffffff;
|
||
--bg-header: #ffffff;
|
||
--bg-sidebar: #1a1a2e;
|
||
--text-primary: #1f1f1f;
|
||
--text-secondary: #555555;
|
||
--text-muted: #999999;
|
||
--text-on-dark: #ffffff;
|
||
--border: #eeeeee;
|
||
--border-dark: #2a2a3e;
|
||
--shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||
--shadow-hover: 0 8px 28px rgba(0,0,0,0.13);
|
||
--radius: 10px;
|
||
--radius-lg: 14px;
|
||
--tag-type-bg: #FFF0E6;
|
||
--tag-crowd-bg: #E3F2FD;
|
||
}
|
||
|
||
/* 深色主题 */
|
||
:root[data-theme="dark"] {
|
||
--primary: #FF8C30;
|
||
--primary-light: #FFA050;
|
||
--primary-dark: #FF6B00;
|
||
--bg-page: #0f0f14;
|
||
--bg-card: #1a1a24;
|
||
--bg-subnav: #1a1a24;
|
||
--bg-header: #141420;
|
||
--bg-sidebar: #0a0a14;
|
||
--text-primary: #e8e8ec;
|
||
--text-secondary: #a0a0a8;
|
||
--text-muted: #606068;
|
||
--text-on-dark: #ffffff;
|
||
--border: #2a2a3a;
|
||
--border-dark: #2a2a3e;
|
||
--shadow: 0 2px 12px rgba(0,0,0,0.25);
|
||
--shadow-hover: 0 8px 28px rgba(0,0,0,0.4);
|
||
--tag-type-bg: #3a2010;
|
||
--tag-crowd-bg: #0a2040;
|
||
}
|
||
|
||
/* ============================================================
|
||
响应式断点变量
|
||
============================================================ */
|
||
:root {
|
||
/* 左侧边栏 */
|
||
--sidebar-width: 0px;
|
||
/* 副导航栏 */
|
||
--subnav-width: 220px;
|
||
/* 右侧边栏 */
|
||
--rightbar-width: 260px;
|
||
/* 内容区域最大宽度 */
|
||
--content-max-width: 1440px;
|
||
/* 顶部导航高度 */
|
||
--header-height: 64px;
|
||
}
|
||
|
||
/* 平板(768px+)显示副导航 */
|
||
@media (min-width: 768px) {
|
||
:root {
|
||
--subnav-width: 220px;
|
||
}
|
||
}
|
||
|
||
/* 大屏幕(1200px+)显示完整布局 */
|
||
@media (min-width: 1200px) {
|
||
:root {
|
||
--sidebar-width: 220px;
|
||
--rightbar-width: 260px;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
全局基础样式
|
||
============================================================ */
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
|
||
background: var(--bg-page);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
transition: background 0.3s, color 0.3s;
|
||
}
|
||
|
||
#app { min-height: 100vh; }
|
||
|
||
/* ============================================================
|
||
整体布局
|
||
============================================================ */
|
||
.app {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 内容区域:副导航 + 主内容 + 右侧栏 */
|
||
.content-wrapper {
|
||
display: flex;
|
||
flex: 1;
|
||
max-width: var(--content-max-width);
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
padding-top: var(--header-height);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 默认隐藏侧边栏,小屏幕适配 */
|
||
.left-sidebar { display: none; }
|
||
.left-subnav { display: none; } /* 默认隐藏平板导航 */
|
||
|
||
/* 平板及以上显示副导航 */
|
||
@media (min-width: 768px) {
|
||
.left-subnav {
|
||
display: flex;
|
||
width: var(--subnav-width);
|
||
min-width: var(--subnav-width);
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
/* 大屏幕显示所有侧边栏 */
|
||
@media (min-width: 1200px) {
|
||
.left-sidebar {
|
||
display: block;
|
||
width: var(--sidebar-width);
|
||
min-width: var(--sidebar-width);
|
||
flex-shrink: 0;
|
||
}
|
||
.rightbar {
|
||
width: var(--rightbar-width);
|
||
min-width: var(--rightbar-width);
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 24px 20px 40px;
|
||
}
|
||
|
||
/* ============================================================
|
||
辅助类
|
||
============================================================ */
|
||
.tag {
|
||
display: inline-block;
|
||
padding: 3px 10px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
transition: background 0.3s, color 0.3s;
|
||
}
|
||
.tag-type { background: var(--tag-type-bg, #FFF0E6); color: var(--primary); }
|
||
.tag-crowd { background: var(--tag-crowd-bg, #E8F4FD); color: #2196F3; }
|
||
|
||
/* ============================================================
|
||
Element Plus 覆盖
|
||
============================================================ */
|
||
.el-input__wrapper { border-radius: 8px !important; }
|
||
.el-button { border-radius: 8px !important; }
|
||
.el-pagination.is-background .el-pager li.is-active { background: var(--primary) !important; }
|
||
|
||
/* ============================================================
|
||
移动端抽屉菜单
|
||
============================================================ */
|
||
.mobile-drawer {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.mobile-drawer.open {
|
||
display: block;
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.drawer-content {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 280px;
|
||
max-width: 80vw;
|
||
transform: translateX(-100%);
|
||
transition: transform 0.3s;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mobile-drawer.open .drawer-content {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
/* 确保抽屉内的 AppSidebar 有正确样式 */
|
||
.drawer-content .sidebar {
|
||
width: 100%;
|
||
position: static;
|
||
height: 100%;
|
||
}
|
||
|
||
/* 移动端:小屏幕隐藏副导航,显示汉堡菜单按钮 */
|
||
@media (max-width: 767px) {
|
||
.content-wrapper {
|
||
padding-top: var(--header-height);
|
||
}
|
||
|
||
.main-content {
|
||
padding: 16px 12px 40px;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
深色主题 Element Plus 额外覆盖
|
||
============================================================ */
|
||
[data-theme="dark"] .el-input__wrapper {
|
||
background: #252535 !important;
|
||
box-shadow: none !important;
|
||
border: 1px solid var(--border) !important;
|
||
}
|
||
[data-theme="dark"] .el-input__inner {
|
||
color: var(--text-primary) !important;
|
||
}
|
||
[data-theme="dark"] .el-pagination {
|
||
--el-pagination-bg-color: var(--bg-card);
|
||
--el-pagination-button-bg-color: var(--bg-card);
|
||
}
|
||
</style> |