Files
joke/web/src/App.vue
T
bwstudio ceed63fcb0 fix: resolve 10 code review issues
High priority:
- Fix concurrent race condition for view_count/like_count (atomic update)
- Add route request ID tracking to prevent race conditions
- Filter get_joke by status=approved (no pending content leak)
- Add error feedback for like button

Performance:
- Optimize random joke query (avoid full table sort)
- Limit page_size max to 100 (DoS prevention)

Medium:
- Add localStorage quota error handling
- Handle empty AI response gracefully
- Fix generate content title extraction

Low:
- Add rejected_jokes to stats API
- Update dashboard to show rejected count
2026-06-02 20:35:08 +08:00

272 lines
7.7 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"
@toggleTheme="themeStore.toggleTheme()"
@random="handleRandom"
/>
<!-- 内容区域: 左副导航 + 主内容 + 右信息栏 -->
<div class="content-wrapper">
<!-- 左侧副导航内容区内 -->
<AppSubNav
: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
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()
// 初始化主题
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) => {
const ids = filterTypeIds.value
const idx = ids.indexOf(typeId)
if (idx >= 0) ids.splice(idx, 1)
else ids.push(typeId)
filterCrowdIds.value = []
const query = ids.length ? { type_ids: ids.join(',') } : {}
router.push({ path: '/', query })
}
const handleSelectCrowd = (crowdId) => {
const ids = filterCrowdIds.value
const idx = ids.indexOf(crowdId)
if (idx >= 0) ids.splice(idx, 1)
else ids.push(crowdId)
filterTypeIds.value = []
const query = ids.length ? { crowd_ids: ids.join(',') } : {}
router.push({ path: '/', query })
}
// 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;
}
/* ============================================================
全局基础样式
============================================================ */
* { 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: 1440px;
margin: 0 auto;
width: 100%;
padding-top: 64px;
min-height: 100vh;
}
.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; }
/* ============================================================
深色主题 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>