diff --git a/app/components/index/CardDetailModal.vue b/app/components/index/CardDetailModal.vue index 0d8191d..3452031 100644 --- a/app/components/index/CardDetailModal.vue +++ b/app/components/index/CardDetailModal.vue @@ -28,7 +28,7 @@ const props = withDefaults(defineProps<{ const dialogStyle = computed(() => { if (props.size === 'expanded') { - return { width: '80vw' } + return { width: '85vw' } } return { width: '720px' } }) @@ -472,7 +472,7 @@ function formatDate(d?: string) { /* ── Expanded mode ── */ .detail--expanded { - height: 80vh; + height: 85vh; max-height: none; } diff --git a/app/components/index/CardFormModal.vue b/app/components/index/CardFormModal.vue index 4924b96..d26f506 100644 --- a/app/components/index/CardFormModal.vue +++ b/app/components/index/CardFormModal.vue @@ -21,7 +21,7 @@ const props = defineProps<{ const emit = defineEmits<{ 'update:visible': [v: boolean] - saved: [] + saved: [cardData: any] }>() // ── Tag fetching ── @@ -256,12 +256,13 @@ async function onSubmit() { body.articleIds = selectedArticleIds.value.length > 0 ? [...selectedArticleIds.value] : null try { + let res: any if (isEdit.value && props.card) { - await request(`/api/cards/${props.card.id}`, { method: 'PUT', body }) + res = await request(`/api/cards/${props.card.id}`, { method: 'PUT', body }) } else { - await request('/api/cards', { method: 'POST', body }) + res = await request('/api/cards', { method: 'POST', body }) } - emit('saved') + emit('saved', res.data) emit('update:visible', false) } catch (err: any) { error.value = err?.data?.message ?? err?.message ?? '保存失败' @@ -275,7 +276,7 @@ async function onSubmit() {
@@ -294,288 +295,285 @@ async function onSubmit() {
- +
{{ error }}
- -
-
卡片类型
-
- -
-
- - -
-
基本信息
- - -
-
- - {{ title.length }}/60 +
+ +
+ +
+
卡片类型
+
+ +
- -
- - -
- - -
-
- - -
-
图片 *
- -
- - -
-
- 预览 - -
-
- - -
-
图片列表 *
- -
-
- {{ idx + 1 }} + +
+
标题 *
+
+ {{ title.length }}/60 +
-
-
- - -
-
- + +
+
分类
+
-
-
- - -
-
- - {{ description.length }}/200 -
- -
- -
-
宽高比
-
- -
-
- - -
-
标签
- - -
- - {{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} - + +
+
宽高比
+
+ +
+
- - 暂无可用标签,请先在标签管理中创建 - -
+ +
+ +
+
图片 *
+ +
+ + +
+ +
+ 预览 + +
+
- -
-
关联文章
- - -
- - {{ allArticles.find(a => a.id === aid)?.title ?? `文章 #${aid}` }} - +
+
+ + - -
- -
-
- - +
+
+ +
+
-
- + + +
+
标签
+ +
+ + {{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} + + +
+ +
+ +
+ + + 暂无可用标签,请先在标签管理中创建 +
-
- 未找到匹配文章 + + +
+
关联文章
+ +
+ + {{ allArticles.find(a => a.id === aid)?.title ?? `文章 #${aid}` }} + + +
+ +
+
+ + +
+
+ +
+
+ 未找到匹配文章 +
+
@@ -659,7 +657,25 @@ async function onSubmit() { padding: 20px 28px; display: flex; flex-direction: column; - gap: 20px; + gap: 16px; + max-height: calc(85vh - 130px); + overflow-y: auto; +} + +/* ── Two-column grid ────────────────────────────────────────────── */ + +.form-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} + +.form-left, +.form-right { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; } /* ── Error ──────────────────────────────────────────────────────── */ @@ -862,7 +878,7 @@ async function onSubmit() { } .image-preview-img { width: 100%; - max-height: 200px; + max-height: 320px; object-fit: cover; display: block; } @@ -955,8 +971,8 @@ async function onSubmit() { .image-previews-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); - gap: 6px; + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: 8px; } .image-preview-thumb { @@ -1198,4 +1214,33 @@ async function onSubmit() { @keyframes spin { to { transform: rotate(360deg); } } + +/* ── Responsive: single column on mobile ────────────────────────── */ + +@media (max-width: 767.98px) { + .form-columns { + grid-template-columns: 1fr; + gap: 16px; + } + + .form-body { + padding: 16px; + max-height: calc(85vh - 120px); + } + + .form-header { + padding: 16px 16px 0; + } + + .form-footer { + padding: 12px 16px; + flex-direction: column; + gap: 8px; + } + + .form-footer .btn { + width: 100%; + justify-content: center; + } +} diff --git a/app/pages/collect/index.vue b/app/pages/collect/index.vue index b7a6d55..6721139 100644 --- a/app/pages/collect/index.vue +++ b/app/pages/collect/index.vue @@ -404,10 +404,17 @@ function onNewCard() { showCardFormModal.value = true } -function onCardSaved() { +function onCardSaved(cardData: ServerCard) { $toast.success(cardFormMode.value === 'create' ? '卡片已创建' : '卡片已更新') + + // 就地更新 / 插入卡片,不触发全量重载 + upsertCardInList(cardData) + + // 如果当前视图按分类筛选且卡片不再匹配,移除 + removeCardIfFiltered(cardData.id, cardData.categoryId) + + // 更新侧栏分类计数 fetchCategories() - resetAndReload() } // ── Drag & Drop to category ── @@ -458,14 +465,23 @@ function onCardDragEnd(e: DragEvent) { async function onCardDropToCategory(categoryId: string | null, categoryName: string) { if (dragCardId === null) return + const cardId = dragCardId try { - await request(`/api/cards/${dragCardId}`, { + await request(`/api/cards/${cardId}`, { method: 'PUT', body: { categoryId }, }) $toast.success(`已移至「${categoryName}」`) + + // 就地更新卡片的分类归属,不需要全量重载布局 + const card = allItems.value.find((item) => item.id === cardId) + if (card) { + card.categoryId = categoryId + } + // 如果当前视图按分类筛选且卡片不再匹配,移除 + removeCardIfFiltered(cardId, categoryId) + // 更新侧栏分类计数 fetchCategories() - resetAndReload() } catch { $toast.error('移动失败') } finally { @@ -550,6 +566,39 @@ function removeCardFromList(cardId: number) { distributeAll() } +/** + * 就地更新 allItems 中的卡片(编辑)或前置插入(创建),不触发服务端重新加载。 + * 用于拖拽修改分类 / 表单编辑保存后免刷新更新布局。 + */ +function upsertCardInList(cardData: ServerCard) { + const mapped = mapCard(cardData) + const idx = allItems.value.findIndex((item) => item.id === mapped.id) + if (idx >= 0) { + allItems.value[idx] = mapped + } else { + // 新创建的卡片 —— 前置插入 + allItems.value.unshift(mapped) + } + distributeAll() +} + +/** + * 如果当前视图按分类筛选,且卡片的新分类不匹配当前筛选条件, + * 则从列表中移除该卡片并重新布局。 + */ +function removeCardIfFiltered(cardId: number, newCategoryId: string | null) { + // 全部 / 收藏 / 搜索视图 → 不移除 + if (activeCategoryId.value === 'all') return + if (activeToolKey.value === 'collect') return + if (activeToolKey.value === 'search') return + + // 当前分类视图,检查卡片的新分类是否匹配 + const currentFilter = activeCategoryId.value === '__uncategorized__' ? null : activeCategoryId.value + if (newCategoryId !== currentFilter) { + removeCardFromList(cardId) + } +} + async function handleToggleFavorite(cardId: number) { const wasFavorited = isFavorited(cardId) const result = await toggleFavorite(cardId) diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index ebc9cb4..79b17c8 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ