From 7cf0b88b85709e8207b361e4ee897ea248863226 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 5 Jun 2026 14:24:19 +0800 Subject: [PATCH] feat: enhance favorite functionality and improve error handling in file upload --- app/components/index/CardDetailModal.vue | 8 +++++++- app/components/index/CardFormModal.vue | 5 +++-- app/components/index/LeftSidebar.vue | 2 +- app/composables/useFavorite.ts | 7 ++++--- app/pages/index/index.vue | 16 ++++++++++++++-- server/api/file/upload.post.ts | 9 +++++---- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/components/index/CardDetailModal.vue b/app/components/index/CardDetailModal.vue index 8b00f50..ce2f376 100644 --- a/app/components/index/CardDetailModal.vue +++ b/app/components/index/CardDetailModal.vue @@ -22,10 +22,16 @@ const props = defineProps<{ const emit = defineEmits<{ 'update:visible': [v: boolean] + 'favorite-toggled': [cardId: number, favorited: boolean] }>() const { isFavorited, toggle, loadingIds } = useFavorite() +async function handleToggle(cardId: number) { + const result = await toggle(cardId) + emit('favorite-toggled', cardId, result) +} + const categoryName = computed(() => { if (!props.card?.categoryId) return null function find(nodes: CategoryNode[]): string | null { @@ -126,7 +132,7 @@ function formatDate(d?: string) { class="detail-fav-btn" :class="{ favorited: isFavorited(card.id), loading: loadingIds.has(card.id) }" :disabled="loadingIds.has(card.id)" - @click="toggle(card.id)" + @click="handleToggle(card.id)" > diff --git a/app/components/index/CardFormModal.vue b/app/components/index/CardFormModal.vue index 46412cd..5232d01 100644 --- a/app/components/index/CardFormModal.vue +++ b/app/components/index/CardFormModal.vue @@ -140,8 +140,9 @@ async function onFileUpload(e: Event) { method: 'POST', body: form, }) - imageUrl.value = raw.data.url - } catch { + imageUrl.value = raw.data[0].url + } catch (e) { + console.log(e) error.value = '图片上传失败' } } diff --git a/app/components/index/LeftSidebar.vue b/app/components/index/LeftSidebar.vue index 439f1e5..d94605a 100644 --- a/app/components/index/LeftSidebar.vue +++ b/app/components/index/LeftSidebar.vue @@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{ width: '300px', side: 'left', tools: () => [ - { key: 'home', label: '主页', icon: 'lucide:home' }, + { key: 'home', label: '全部卡片', icon: 'lucide:home' }, { key: 'collect', label: '收藏', icon: 'lucide:star' }, // { key: 'search', label: '搜索', icon: 'lucide:search' }, // { key: 'tags', label: '标签', icon: 'lucide:tag' }, diff --git a/app/composables/useFavorite.ts b/app/composables/useFavorite.ts index 4c80dd0..e2ef560 100644 --- a/app/composables/useFavorite.ts +++ b/app/composables/useFavorite.ts @@ -1,5 +1,9 @@ import { useAuthSession } from "./useAuthSession"; +// Module-level singleton state — shared across all components that call useFavorite(). +const favoritedCardIds = ref>(new Set()) +const loadingIds = ref>(new Set()) + /** * Favorite management composable. * Provides a reactive set of favorited card IDs and toggle function. @@ -8,9 +12,6 @@ export function useFavorite() { const { loggedIn } = useAuthSession() const { $toast } = useNuxtApp() - const favoritedCardIds = ref>(new Set()) - const loadingIds = ref>(new Set()) - /** * Check if a card is favorited. */ diff --git a/app/pages/index/index.vue b/app/pages/index/index.vue index e6b3468..d8373f9 100644 --- a/app/pages/index/index.vue +++ b/app/pages/index/index.vue @@ -521,13 +521,24 @@ function onContainerResize() { }, 120) } +function removeCardFromList(cardId: number) { + allItems.value = allItems.value.filter((item) => item.id !== cardId) + distributeAll() +} + async function handleToggleFavorite(cardId: number) { const wasFavorited = isFavorited(cardId) const result = await toggleFavorite(cardId) // When unfavoriting on the collect page, remove the card from the list immediately if (wasFavorited && !result && activeToolKey.value === 'collect') { - allItems.value = allItems.value.filter((item) => item.id !== cardId) - distributeAll() + removeCardFromList(cardId) + } +} + +function onDetailFavoriteToggled(cardId: number, favorited: boolean) { + // When unfavoriting from the detail modal on the collect page, remove the card + if (!favorited && activeToolKey.value === 'collect') { + removeCardFromList(cardId) } } @@ -864,6 +875,7 @@ onUnmounted(() => { :card="detailCard" :categories="categories" @update:visible="(v) => showDetailModal = v" + @favorite-toggled="onDetailFavoriteToggled" /> diff --git a/server/api/file/upload.post.ts b/server/api/file/upload.post.ts index 24bb12a..b2ae6ba 100644 --- a/server/api/file/upload.post.ts +++ b/server/api/file/upload.post.ts @@ -2,6 +2,7 @@ import multer from 'multer'; import fs from 'node:fs'; import path from 'node:path'; import { callNodeListener } from 'h3'; +import { RELATIVE_ASSETS_DIR, POST_MEDIA_PUBLIC_PREFIX } from '../../constants/upload'; // 类型定义 interface IFile { @@ -14,8 +15,8 @@ interface IFile { export default defineWrappedResponseHandler(async (event) => { try { - // 存储目录 - const uploadDir = path.join(process.cwd(), 'public/assets'); + // 存储目录(使用统一定义的常量,默认 static/upload) + const uploadDir = RELATIVE_ASSETS_DIR; // 自动创建目录 if (!fs.existsSync(uploadDir)) { @@ -69,12 +70,12 @@ export default defineWrappedResponseHandler(async (event) => { // 格式化返回数据 const result: IFile[] = uploadedFiles.map((file: any) => ({ name: file.originalname, - url: `/public/assets/${file.filename}`, // ✅ 前端可直接访问 + url: `${POST_MEDIA_PUBLIC_PREFIX}${file.filename}`, // ✅ 与 static 插件对齐 mimeType: file.mimetype, size: file.size, })); - return result; + return R.success(result); } catch (err: any) { console.error('上传失败:', err);