Files
joke/web/src/App.vue
T

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