Browse Source

feat: enhance favorite functionality and improve error handling in file upload

as
npmrun 1 week ago
parent
commit
7cf0b88b85
  1. 8
      app/components/index/CardDetailModal.vue
  2. 5
      app/components/index/CardFormModal.vue
  3. 2
      app/components/index/LeftSidebar.vue
  4. 7
      app/composables/useFavorite.ts
  5. 16
      app/pages/index/index.vue
  6. 9
      server/api/file/upload.post.ts

8
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)"
>
<Icon :name="isFavorited(card.id) ? 'ph:star-fill' : 'ph:star'" />
</button>

5
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 = '图片上传失败'
}
}

2
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' },

7
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<Set<number>>(new Set())
const loadingIds = ref<Set<number>>(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<Set<number>>(new Set())
const loadingIds = ref<Set<number>>(new Set())
/**
* Check if a card is favorited.
*/

16
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"
/>
<!-- Card Form Modal -->

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

Loading…
Cancel
Save