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
This commit is contained in:
bwstudio
2026-06-02 20:35:08 +08:00
parent 0b43973236
commit ceed63fcb0
144 changed files with 191660 additions and 270 deletions
+263 -12
View File
@@ -1,21 +1,272 @@
<template>
<div id="app">
<AppHeader />
<main class="main-content">
<router-view />
</main>
<AppFooter />
<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 AppHeader from '@/components/Header.vue'
import AppFooter from '@/components/Footer.vue'
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: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #fafafa; }
#app { min-height: 100vh; display: flex; flex-direction: column; }
.main-content { flex: 1; max-width: 1200px; margin: 0 auto; padding: 20px; width: 100%; }
</style>
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>