feat: 人生事件和剧本收藏从 localStorage 切换为真实 API

- 后端:新增 t_event_favorite 表(UNIQUE KEY + MyBatis-Plus Entity)
- 后端:LifeEventController 新增 /favorite/toggle、/favorite/page、/favorite/check 三个真实端点
- 后端:EventFavoriteService 完整 CRUD,含 toggle/分页/批量查询/状态检查
- 后端:删除旧的 favorite-placeholder mock 端点
- 前端:life-event/detail.vue 从 localStorage 假收藏切换为真实 API + localStorage 缓存防闪烁
- 前端:ScriptLibraryView.vue 从 localFavorites 切换为 checkFavoriteScript 真实 API
- 前端:stores/app.js 新增 toggleEventFavorite / checkEventFavorite 方法
- 修复:审核发现的 UNIQUE 约束缺失、未使用 watch 导入、Controller 参数注解遗漏、Controller 委托 Service 构造 Response

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:23:40 +08:00
parent a3bfad099a
commit dd11cfae73
12 changed files with 451 additions and 41 deletions
@@ -147,9 +147,9 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { toggleFavoriteScript } from '../../services/epicScript.js'
import { toggleFavoriteScript, checkFavoriteScript } from '../../services/epicScript.js'
const store = useAppStore()
const activeType = ref('long')
@@ -157,7 +157,7 @@ const activeStatus = ref('all')
const keyword = ref('')
const sortMode = ref('updated')
const viewMode = ref('list')
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
const favoriteStatus = ref({})
const activeMenuId = ref('')
const deleteTarget = ref(null)
const deletingScript = ref(false)
@@ -198,6 +198,26 @@ const visibleScripts = computed(() => {
})
})
// 列表加载后批量检查收藏状态
const loadFavoriteStatus = async () => {
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
if (!ids.length) return
const status = {}
await Promise.all(ids.map(async (id) => {
try {
status[id] = await checkFavoriteScript(id)
} catch {
status[id] = false
}
}))
favoriteStatus.value = status
}
// 监听 scripts 变化时重新加载收藏状态
watch(scripts, () => {
loadFavoriteStatus()
}, { immediate: true })
const sortLabel = computed(() => {
const map = { updated: '最近更新⌄', words: '字数最多⌄', progress: '进度最高⌄' }
return map[sortMode.value] || '最近更新⌄'
@@ -243,7 +263,7 @@ const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress
const getInitial = (script) => (script.title || '剧').slice(0, 1)
const isFavorite = (script) => {
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
return Boolean(favoriteStatus.value[String(script.id)])
}
const openScriptChat = (script) => {
@@ -319,15 +339,14 @@ const toggleScriptMenu = (script) => {
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改本地缓存
const next = { ...localFavorites.value }
// 乐观更新:先改内存状态
const next = { ...favoriteStatus.value }
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
favoriteStatus.value = next
closeScriptMenu()
try {
@@ -335,14 +354,13 @@ const toggleFavorite = async (script) => {
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
} catch (error) {
// 回滚乐观更新
const restored = { ...localFavorites.value }
const restored = { ...favoriteStatus.value }
if (wasFav) {
restored[String(script.id)] = true
} else {
delete restored[String(script.id)]
}
localFavorites.value = restored
uni.setStorageSync('script_favorites', restored)
favoriteStatus.value = restored
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
}
}
@@ -383,10 +401,9 @@ const confirmDeleteScript = async () => {
return
}
const next = { ...localFavorites.value }
const next = { ...favoriteStatus.value }
delete next[id]
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
favoriteStatus.value = next
const conversationId = deleteTarget.value.conversationId || deleteTarget.value.plotJson?.conversationId
if (conversationId) uni.removeStorageSync(`script_conversation_history_${conversationId}`)
uni.removeStorageSync(`script_chat_history_${id}`)