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);