From 5b170b77616968ca323c2b463ef7832032a13002 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 6 Jun 2026 08:33:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=20CardDetailModal=20?= =?UTF-8?q?=E5=92=8C=20CardFormModal=20=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=93=8D=E5=BA=94=E5=BC=8F=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E5=92=8C=E5=8D=A1=E7=89=87=E4=BF=9D=E5=AD=98=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/index/CardDetailModal.vue | 4 +- app/components/index/CardFormModal.vue | 579 +++++++++++++++++-------------- app/pages/collect/index.vue | 57 ++- packages/drizzle-pkg/db.sqlite | Bin 282624 -> 282624 bytes 4 files changed, 367 insertions(+), 273 deletions(-) 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 ebc9cb4dde5bd2bffc0437407e6c94834dac5b15..79b17c855ea584b90adb7102ceb7bfae2fab86b9 100644 GIT binary patch delta 923 zcmZWnT}TvB6uxI?XPg<`IlHT?ETishT1eK0u9{M*=z~ehnlO;|qvjt$5Q2#8&V>2koVn*7zWMHV?)~ocK5_Iu zag3-iIGR`tgA27CpH+lMdEOm$FSr zJb*%F8;kZh0oMrr!d=|KZH(X=uHaJ0t1aT#(w>maa{yYFnRCs2+RQm-&JOMF*$d+Z zDMDiUm0S2iilCr`71;=O5D>%ad3X=DJ4x{}apB#(D4plS-%DZGVx24sT4{e7{HCkr zaKw2%8>SvCWz4|T}<|i;;?E)pAWDo>*dN zwg)@9nu0f4f_0q@1>4Ma_RzX%NR46CjMV{_GYgWb5P%YKvgmC!`&>iy0L;7b84*w> zxQu?hgR6K2Wj0tC9w>DA8syTe(~t%leajtetynUP@{y%H<%rLxTJ>NFI8iDnm is3Zfv=?QP5g|kCac-v#)HAouRDX_4?g?9?9PWxX9jR9}~ delta 1012 zcmY+CYiJW&5P;9@W|Q4*vy*4lyV^9gMY)n_8k<%`TTs+u+NyLxtJk_DtyB@KR`j1q zMZA}5(`wKYY>5?o^oRJMVpI_Imp>F6#K#X?1+h{=doNP(fv9H_tGKf}4ByP0d7O<6 z*kc3sUKLu7%v=gBmpAVkRy~9eNs8#ZqR8{5U>Gg!+x0E2yGxVrhNrnW3==$yVe88u z;qKwObq1wg$RMI41ptu9=`C)JVW?9 z;@DYN7?OTQ4TWD^v#|~)34X>&9Qh$x%no@=u3<9!&0Nfq%q|po0H?z2>5c-X9S)i4 zLWeXo`OQ{aBuFEqunX7VC20iB)=!FT2%7@LjJgu;f))Q+Xwd{AH@GkJUmf&trc~dn z2HF*dFErZ-vt8N1Sx8g0?kekioqI;P5l_1tpqM_bhch-vAK%B_&uc6t4RlEZsLQza z9BD8ALn3aw-9iMRvvpsmzFUvhjI$Qm!vgkB?#%1=O?7sOOmmW-&FFfB4%dT9&5f`^ zOnLr}|HUGx)(Gckuw(&)1cP`158_kYI3>n*6#8%zRJ%fLz6J9uqrU1uFysqXw9er* zps_}9(~Boz4&BfKeq+2c!8YY76YM$DpsHKO53I-y6W+l4IEEAW3_swYA*@IO{UYxM zW1`|IW0t~uA-_k1+u8ez&?h^<4?;%hfXi6y&2MX_%nOiTlbSc_*Z$WY`e;8?h}!@c z6>d}dGD-}N7aSRAE^Lxy%`=Y>QDpnSnTE1eZ&J6$dk4o(pNr_7zU@2bR|WP|1bl5B zdI-|TDj%ovI%<(Mr;})+BnVZN(NJ}jUg3+x| z(*sK&P8apS9aAb?bnj{}jPAF1*#G$Tc3tKba=Z;ZnQ;#%lz`{ZL=DG2J~L5ca@f8D D4T~o+