diff --git a/app/components/index/CardDetailModal.vue b/app/components/index/CardDetailModal.vue index 61d5d08..0d8191d 100644 --- a/app/components/index/CardDetailModal.vue +++ b/app/components/index/CardDetailModal.vue @@ -17,11 +17,21 @@ export interface CardDetail { articles?: { id: number; title: string }[] } -const props = defineProps<{ +const props = withDefaults(defineProps<{ card: CardDetail | null visible: boolean categories: CategoryNode[] -}>() + size?: 'default' | 'expanded' +}>(), { + size: 'default', +}) + +const dialogStyle = computed(() => { + if (props.size === 'expanded') { + return { width: '80vw' } + } + return { width: '720px' } +}) const emit = defineEmits<{ 'update:visible': [v: boolean] @@ -108,10 +118,10 @@ function formatDate(d?: string) { -
+
diff --git a/app/components/index/CardFormModal.vue b/app/components/index/CardFormModal.vue index 109d85b..4924b96 100644 --- a/app/components/index/CardFormModal.vue +++ b/app/components/index/CardFormModal.vue @@ -9,11 +9,12 @@ const ASPECT_PRESETS = [ { label: '方形 1:1', value: 1.0 }, { label: '横图 4:3', value: 1.33 }, { label: '宽屏 16:9', value: 1.78 }, + { label: '超宽 2.35:1', value: 2.35 }, ] const props = defineProps<{ mode: 'create' | 'edit' - card: CardDetail | null // null for create, prefilled for edit + card: CardDetail | null categories: CategoryNode[] visible: boolean }>() @@ -43,7 +44,7 @@ async function loadTags() { const formType = ref('image-text') const title = ref('') const description = ref('') -const aspectRatio = ref(0.75) +const aspectRatio = ref(1.33) const categoryId = ref(null) const imageUrl = ref('') const imageUrls = ref(['']) @@ -98,7 +99,6 @@ const isEdit = computed(() => props.mode === 'edit') const titleText = computed(() => isEdit.value ? '编辑卡片' : '新增卡片') -// 从注册表读取表单字段需求(新增类型无需修改此文件) const formFields = computed(() => cardTypeRegistry[formType.value as CardType]?.formFields ?? {}) const needsImage = computed(() => formFields.value.image ?? false) const needsMultiImage = computed(() => formFields.value.multiImage ?? false) @@ -108,7 +108,7 @@ const needsDescription = computed(() => formFields.value.description ?? false) // Flatten category tree for select interface FlatCat { id: string; name: string; depth: number } const flatCategories = computed(() => { - const result: FlatCat[] = [{ id: '', name: '(无分类)', depth: 0 }] + const result: FlatCat[] = [{ id: '', name: '无分类', depth: 0 }] function walk(nodes: CategoryNode[], depth: number) { for (const n of nodes) { if (n.id === '__uncategorized__') continue @@ -143,13 +143,12 @@ watch(() => props.visible, (v) => { : [''] selectedTagIds.value = [] selectedArticleIds.value = [] - // Re-fetch card to get full relations including articles fetchCardArticles(props.card.id) } else { formType.value = 'image-text' title.value = '' description.value = '' - aspectRatio.value = 0.75 + aspectRatio.value = 1.33 categoryId.value = null imageUrl.value = '' imageUrls.value = [''] @@ -227,7 +226,7 @@ async function onSubmit() { error.value = '' if (!title.value.trim()) { error.value = '请输入标题'; return } - if (needsImage.value && !imageUrl.value.trim()) { error.value = '请输入图片 URL'; return } + if (needsImage.value && !imageUrl.value.trim()) { error.value = '请添加图片'; return } if (needsMultiImage.value) { const validUrls = imageUrls.value.filter(u => u.trim()) if (validUrls.length === 0) { error.value = '请至少添加一张图片'; return } @@ -243,7 +242,6 @@ async function onSubmit() { categoryId: categoryId.value || null, } - // Images if (needsMultiImage.value) { const validUrls = imageUrls.value.filter(u => u.trim()) body.images = validUrls.map((url, i) => ({ url: url.trim(), sortOrder: i })) @@ -251,12 +249,10 @@ async function onSubmit() { body.images = [{ url: imageUrl.value.trim(), sortOrder: 0 }] } - // Tags if (needsTags.value) { body.tagIds = [...selectedTagIds.value] } - // Articles body.articleIds = selectedArticleIds.value.length > 0 ? [...selectedArticleIds.value] : null try { @@ -279,82 +275,96 @@ async function onSubmit() {
-
-

{{ titleText }}

-
-
-
{{ error }}
+
+ +
+ + {{ error }} +
- -
- -
+ +
+
卡片类型
+
- -
- - -
- - -
- - -
- - -
- -
+ +
+
基本信息
+ + +
+
+ + {{ title.length }}/60 +
-
+ + +
+ + +
+
+ + +
+
图片 *
+ +
+ +
-
- 预览 + +
+ 预览 +
- -
- -
-
+ +
+
图片列表 *
+ +
+
+ {{ idx + 1 }}
-
-
- 预览 -
-
- + +
+
+ +
+
- -
- + +
+
+ + {{ description.length }}/200 +
- -
- -
+ +
+
宽高比
+
-
- -
- - -
+ +
+
标签
+ + +
- {{ allArticles.find(a => a.id === aid)?.title ?? `文章 #${aid}` }} - - -
- -
- -
+ {{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} -
-
- 无匹配文章 -
+
+ + +
+ +
+ + + 暂无可用标签,请先在标签管理中创建 +
- -
- -
+ +
+
关联文章
+ + +
- {{ allTags.find(t => t.id === tid)?.name ?? `#${tid}` }} -
-
- 可选标签 -
+ + +
+
+ + +
+
+
+ 未找到匹配文章 +
- 暂无可用标签,请先在标签管理中创建
- @@ -542,33 +606,37 @@ async function onSubmit() { diff --git a/app/components/index/WaterfallCard.vue b/app/components/index/WaterfallCard.vue index 99e395a..dae7790 100644 --- a/app/components/index/WaterfallCard.vue +++ b/app/components/index/WaterfallCard.vue @@ -125,7 +125,7 @@ function onImgError() { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 3; -webkit-box-orient: vertical; } @@ -138,12 +138,12 @@ function onImgError() { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; -webkit-box-orient: vertical; } .card-text { - padding: 0 16px 16px; + padding: 0 18px 20px; } .img-fallback { diff --git a/app/components/index/WaterfallImageCard.vue b/app/components/index/WaterfallImageCard.vue index 1db8684..cba7c4f 100644 --- a/app/components/index/WaterfallImageCard.vue +++ b/app/components/index/WaterfallImageCard.vue @@ -96,9 +96,9 @@ function onImgError() { max-width: calc(100% - 16px); background: var(--color-surface-dark); color: var(--color-on-dark); - font-size: 10px; + font-size: 12px; font-weight: 500; - padding: 4px 8px; + padding: 5px 10px; border-radius: 5px; letter-spacing: 0.03em; opacity: 0; diff --git a/app/components/index/WaterfallPortfolioCard.vue b/app/components/index/WaterfallPortfolioCard.vue index 812ab95..2cecfd1 100644 --- a/app/components/index/WaterfallPortfolioCard.vue +++ b/app/components/index/WaterfallPortfolioCard.vue @@ -140,7 +140,7 @@ const totalImages = computed(() => props.images.length) text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 1; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; } @@ -153,7 +153,7 @@ const totalImages = computed(() => props.images.length) text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 3; -webkit-box-orient: vertical; } diff --git a/app/components/index/WaterfallProjectCard.vue b/app/components/index/WaterfallProjectCard.vue index a2e6dd1..977dc41 100644 --- a/app/components/index/WaterfallProjectCard.vue +++ b/app/components/index/WaterfallProjectCard.vue @@ -143,7 +143,7 @@ function onImgError() { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 3; -webkit-box-orient: vertical; } @@ -156,12 +156,12 @@ function onImgError() { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; -webkit-box-orient: vertical; } .card-body { - padding: 0 16px 16px; + padding: 0 18px 20px; } .tags { diff --git a/app/components/index/WaterfallTextCard.vue b/app/components/index/WaterfallTextCard.vue index 69baf90..6a06ad7 100644 --- a/app/components/index/WaterfallTextCard.vue +++ b/app/components/index/WaterfallTextCard.vue @@ -66,7 +66,7 @@ defineProps<{ margin: 0; overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 4; + -webkit-line-clamp: 8; -webkit-box-orient: vertical; } diff --git a/app/pages/collect/index.vue b/app/pages/collect/index.vue index 65228b9..b7a6d55 100644 --- a/app/pages/collect/index.vue +++ b/app/pages/collect/index.vue @@ -347,11 +347,12 @@ async function onCatFormSubmit(data: { const showDetailModal = ref(false) const detailCard = ref(null) +const detailSize = ref<'default' | 'expanded'>('default') const showCardFormModal = ref(false) const cardFormMode = ref<'create' | 'edit'>('create') const cardFormEditData = ref(null) -function onCardClick(card: CardItem) { +function onCardExpand(card: CardItem) { detailCard.value = { id: card.id, type: card.type, @@ -364,6 +365,7 @@ function onCardClick(card: CardItem) { categoryId: card.categoryId, createdAt: card.createdAt, } + detailSize.value = 'expanded' showDetailModal.value = true } @@ -418,6 +420,40 @@ function onCardDragStart(e: DragEvent, item: CardItem) { e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('application/x-card-id', String(item.id)) } + const el = e.currentTarget as HTMLElement + + // build a blurred ghost clone for the drag image + const MAX_DRAG_WIDTH = 280 + const ghost = el.cloneNode(true) as HTMLElement + const rect = el.getBoundingClientRect() + const ratio = rect.width > MAX_DRAG_WIDTH ? MAX_DRAG_WIDTH / rect.width : 1 + + ghost.style.position = 'fixed' + ghost.style.top = '-9999px' + ghost.style.left = '-9999px' + ghost.style.width = (ratio < 1 ? MAX_DRAG_WIDTH : rect.width) + 'px' + ghost.style.opacity = '0.55' + ghost.style.filter = 'blur(4px)' + ghost.style.animation = 'none' + ghost.style.pointerEvents = 'none' + document.body.appendChild(ghost) + + const offsetX = (e.clientX - rect.left) * ratio + const offsetY = (e.clientY - rect.top) * ratio + e.dataTransfer!.setDragImage(ghost, offsetX, offsetY) + + // fade the original card after browser snaps the drag image + requestAnimationFrame(() => { + el.classList.add('dragging') + // ghost can be removed — browser holds a reference + ghost.remove() + }) +} + +function onCardDragEnd(e: DragEvent) { + dragCardId = null + const el = e.currentTarget as HTMLElement + el.classList.remove('dragging') } async function onCardDropToCategory(categoryId: string | null, categoryName: string) { @@ -456,9 +492,9 @@ function containerWidth(): number { } function getColumnWidth(): number { - const padding = 48 + const padding = 32 const w = containerWidth() - padding - const gap = 12 + const gap = 8 return (w - gap * (columnCount.value - 1)) / columnCount.value } @@ -493,9 +529,8 @@ function distributeAll() { function decideColumnCount(): number { if (isMobile.value) return 1 const w = containerWidth() - if (w < 900) return 2 - if (w < 1200) return 3 - return 4 + if (w < 1100) return 2 + return 3 } let resizeTimer: ReturnType | null = null @@ -770,8 +805,8 @@ onUnmounted(() => { class="card-reveal" :style="{ '--enter-delay': `${ci * 80 + ri * 60}ms` }" draggable="true" - @click="onCardClick(item)" @dragstart="onCardDragStart($event, item)" + @dragend="onCardDragEnd($event)" >
@@ -787,6 +822,14 @@ onUnmounted(() => { +