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<{ const emit = defineEmits<{
'update:visible': [v: boolean] 'update:visible': [v: boolean]
'favorite-toggled': [cardId: number, favorited: boolean]
}>() }>()
const { isFavorited, toggle, loadingIds } = useFavorite() const { isFavorited, toggle, loadingIds } = useFavorite()
async function handleToggle(cardId: number) {
const result = await toggle(cardId)
emit('favorite-toggled', cardId, result)
}
const categoryName = computed(() => { const categoryName = computed(() => {
if (!props.card?.categoryId) return null if (!props.card?.categoryId) return null
function find(nodes: CategoryNode[]): string | null { function find(nodes: CategoryNode[]): string | null {
@ -126,7 +132,7 @@ function formatDate(d?: string) {
class="detail-fav-btn" class="detail-fav-btn"
:class="{ favorited: isFavorited(card.id), loading: loadingIds.has(card.id) }" :class="{ favorited: isFavorited(card.id), loading: loadingIds.has(card.id) }"
:disabled="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'" /> <Icon :name="isFavorited(card.id) ? 'ph:star-fill' : 'ph:star'" />
</button> </button>

5
app/components/index/CardFormModal.vue

@ -140,8 +140,9 @@ async function onFileUpload(e: Event) {
method: 'POST', method: 'POST',
body: form, body: form,
}) })
imageUrl.value = raw.data.url imageUrl.value = raw.data[0].url
} catch { } catch (e) {
console.log(e)
error.value = '图片上传失败' error.value = '图片上传失败'
} }
} }

2
app/components/index/LeftSidebar.vue

@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
width: '300px', width: '300px',
side: 'left', side: 'left',
tools: () => [ tools: () => [
{ key: 'home', label: '主页', icon: 'lucide:home' }, { key: 'home', label: '全部卡片', icon: 'lucide:home' },
{ key: 'collect', label: '收藏', icon: 'lucide:star' }, { key: 'collect', label: '收藏', icon: 'lucide:star' },
// { key: 'search', label: '', icon: 'lucide:search' }, // { key: 'search', label: '', icon: 'lucide:search' },
// { key: 'tags', label: '', icon: 'lucide:tag' }, // { key: 'tags', label: '', icon: 'lucide:tag' },

7
app/composables/useFavorite.ts

@ -1,5 +1,9 @@
import { useAuthSession } from "./useAuthSession"; 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. * Favorite management composable.
* Provides a reactive set of favorited card IDs and toggle function. * Provides a reactive set of favorited card IDs and toggle function.
@ -8,9 +12,6 @@ export function useFavorite() {
const { loggedIn } = useAuthSession() const { loggedIn } = useAuthSession()
const { $toast } = useNuxtApp() const { $toast } = useNuxtApp()
const favoritedCardIds = ref<Set<number>>(new Set())
const loadingIds = ref<Set<number>>(new Set())
/** /**
* Check if a card is favorited. * Check if a card is favorited.
*/ */

16
app/pages/index/index.vue

@ -521,13 +521,24 @@ function onContainerResize() {
}, 120) }, 120)
} }
function removeCardFromList(cardId: number) {
allItems.value = allItems.value.filter((item) => item.id !== cardId)
distributeAll()
}
async function handleToggleFavorite(cardId: number) { async function handleToggleFavorite(cardId: number) {
const wasFavorited = isFavorited(cardId) const wasFavorited = isFavorited(cardId)
const result = await toggleFavorite(cardId) const result = await toggleFavorite(cardId)
// When unfavoriting on the collect page, remove the card from the list immediately // When unfavoriting on the collect page, remove the card from the list immediately
if (wasFavorited && !result && activeToolKey.value === 'collect') { if (wasFavorited && !result && activeToolKey.value === 'collect') {
allItems.value = allItems.value.filter((item) => item.id !== cardId) removeCardFromList(cardId)
distributeAll() }
}
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" :card="detailCard"
:categories="categories" :categories="categories"
@update:visible="(v) => showDetailModal = v" @update:visible="(v) => showDetailModal = v"
@favorite-toggled="onDetailFavoriteToggled"
/> />
<!-- Card Form Modal --> <!-- Card Form Modal -->

9
server/api/file/upload.post.ts

@ -2,6 +2,7 @@ import multer from 'multer';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { callNodeListener } from 'h3'; import { callNodeListener } from 'h3';
import { RELATIVE_ASSETS_DIR, POST_MEDIA_PUBLIC_PREFIX } from '../../constants/upload';
// 类型定义 // 类型定义
interface IFile { interface IFile {
@ -14,8 +15,8 @@ interface IFile {
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
try { try {
// 存储目录 // 存储目录(使用统一定义的常量,默认 static/upload)
const uploadDir = path.join(process.cwd(), 'public/assets'); const uploadDir = RELATIVE_ASSETS_DIR;
// 自动创建目录 // 自动创建目录
if (!fs.existsSync(uploadDir)) { if (!fs.existsSync(uploadDir)) {
@ -69,12 +70,12 @@ export default defineWrappedResponseHandler(async (event) => {
// 格式化返回数据 // 格式化返回数据
const result: IFile[] = uploadedFiles.map((file: any) => ({ const result: IFile[] = uploadedFiles.map((file: any) => ({
name: file.originalname, name: file.originalname,
url: `/public/assets/${file.filename}`, // ✅ 前端可直接访问 url: `${POST_MEDIA_PUBLIC_PREFIX}${file.filename}`, // ✅ 与 static 插件对齐
mimeType: file.mimetype, mimeType: file.mimetype,
size: file.size, size: file.size,
})); }));
return result; return R.success(result);
} catch (err: any) { } catch (err: any) {
console.error('上传失败:', err); console.error('上传失败:', err);

Loading…
Cancel
Save