From 37bd82714d57ee66b36d2fdfa8e8cac16e316dbf Mon Sep 17 00:00:00 2001 From: aid Date: Thu, 9 Apr 2026 19:29:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=9C=E5=93=81=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E5=BC=95=E5=85=A5=20PIP=20=E7=94=BB=E4=B8=AD=E7=94=BB=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=8E=9F=E5=9B=BE=EF=BC=8C=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=8A=A0=E5=85=A8=E5=B1=8F=E6=94=BE=E5=A4=A7=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 大图为 AI 生成的绘本封面(coverUrl),右下角小图为用户上传的原图(originalImageUrl),形成"创作素材→AI 成果"的视觉对比 - 草稿等无 AI 封面的作品:大图为占位图,PIP 仍展示原图 涉及文件: - works/Index.vue 作品库列表卡片加 PIP(右下角 34% 正方形 + 白边阴影) - Gallery.vue 发现页卡片加同款 PIP - mine/Favorites.vue 收藏列表加 PIP,type 加 originalImageUrl 字段 - components/WorkSelector.vue 作品选择器加更小尺寸 PIP(32%) - works/Detail.vue 详情页新增「画作原图」独立卡片(左 84px 缩略图 + 右文字描述) · 点击缩略图全屏 overlay 放大查看,背景毛玻璃 + 紫黑半透明 · hover 缩略图时显示放大镜图标 - _dev-mock.ts 5 条 mock 作品都加 originalImageUrl(不同 hue 区分),id=102 (draft) 的 coverUrl 设为 null 测试占位边界 兼容性: - v-if 检查 originalImageUrl 不为空且与 coverUrl 不同,防止字段未拆分时显示重复 - 后端 originalImageUrl 字段为 null 时 PIP 不显示,老数据自动兼容 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/views/public/Gallery.vue | 35 ++++++ .../views/public/components/WorkSelector.vue | 45 ++++++- frontend/src/views/public/mine/Favorites.vue | 35 ++++++ frontend/src/views/public/works/Detail.vue | 111 ++++++++++++++++++ frontend/src/views/public/works/Index.vue | 35 ++++++ frontend/src/views/public/works/_dev-mock.ts | 12 +- 6 files changed, 263 insertions(+), 10 deletions(-) diff --git a/frontend/src/views/public/Gallery.vue b/frontend/src/views/public/Gallery.vue index f8f1f2b..7fff163 100644 --- a/frontend/src/views/public/Gallery.vue +++ b/frontend/src/views/public/Gallery.vue @@ -77,10 +77,19 @@ @click="$router.push(`/p/works/${work.id}`)" >
+
+ +
+ 原图 +

{{ work.title }}

@@ -351,10 +360,36 @@ $primary: #6366f1; &:hover { box-shadow: 0 4px 20px rgba($primary, 0.1); transform: translateY(-2px); } .card-cover { + position: relative; aspect-ratio: 3/4; background: #f5f3ff; img { width: 100%; height: 100%; object-fit: cover; } .cover-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 28px; color: #d1d5db; } + + /* 右下角 PIP:用户原图 */ + .cover-pip { + position: absolute; + right: 6px; + bottom: 6px; + width: 34%; + aspect-ratio: 1 / 1; + border-radius: 7px; + overflow: hidden; + border: 2px solid #fff; + background: #fff; + box-shadow: 0 3px 10px rgba(15, 12, 41, 0.25); + transition: transform 0.2s; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + + &:hover .cover-pip { + transform: scale(1.05); } .card-body { diff --git a/frontend/src/views/public/components/WorkSelector.vue b/frontend/src/views/public/components/WorkSelector.vue index 8674953..dbc125d 100644 --- a/frontend/src/views/public/components/WorkSelector.vue +++ b/frontend/src/views/public/components/WorkSelector.vue @@ -25,8 +25,18 @@ :class="['selector-card', { selected: selectedWork?.id === work.id }]" @click="selectedWork = work" > - -
+
+ +
+ +
+ 原图 +
+

{{ work.title }}

{{ work._count?.pages || 0 }}页 @@ -107,15 +117,22 @@ $primary: #6366f1; &:hover { border-color: rgba($primary, 0.3); } &.selected { border-color: $primary; box-shadow: 0 0 0 2px rgba($primary, 0.2); } + .cover-wrap { + position: relative; + width: 100%; + aspect-ratio: 3 / 4; + } + .selector-cover { width: 100%; - aspect-ratio: 3/4; + height: 100%; object-fit: cover; + display: block; } .selector-cover-empty { width: 100%; - aspect-ratio: 3/4; + height: 100%; background: #f5f3ff; display: flex; align-items: center; @@ -124,6 +141,26 @@ $primary: #6366f1; color: #d1d5db; } + /* 右下角 PIP:用户原图(更小尺寸) */ + .cover-pip { + position: absolute; + right: 5px; + bottom: 5px; + width: 32%; + aspect-ratio: 1 / 1; + border-radius: 6px; + overflow: hidden; + border: 1.5px solid #fff; + background: #fff; + box-shadow: 0 2px 8px rgba(15, 12, 41, 0.22); + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + .selector-info { padding: 8px 10px; h4 { font-size: 12px; font-weight: 600; color: #1e1b4b; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/frontend/src/views/public/mine/Favorites.vue b/frontend/src/views/public/mine/Favorites.vue index 1ef44c1..1536f09 100644 --- a/frontend/src/views/public/mine/Favorites.vue +++ b/frontend/src/views/public/mine/Favorites.vue @@ -26,6 +26,14 @@
+ +
+ 原图 +

{{ item.title }}

@@ -65,6 +73,7 @@ interface FavoriteListItem { workId: number title: string coverUrl?: string | null + originalImageUrl?: string | null likeCount?: number viewCount?: number } @@ -129,10 +138,36 @@ $primary: #6366f1; &:hover { box-shadow: 0 4px 20px rgba($primary, 0.1); transform: translateY(-2px); } .card-cover { + position: relative; aspect-ratio: 3/4; background: #f5f3ff; img { width: 100%; height: 100%; object-fit: cover; } .cover-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 28px; color: #d1d5db; } + + /* 右下角 PIP:用户原图 */ + .cover-pip { + position: absolute; + right: 6px; + bottom: 6px; + width: 34%; + aspect-ratio: 1 / 1; + border-radius: 7px; + overflow: hidden; + border: 2px solid #fff; + background: #fff; + box-shadow: 0 3px 10px rgba(15, 12, 41, 0.25); + transition: transform 0.2s; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + + &:hover .cover-pip { + transform: scale(1.05); } .card-body { diff --git a/frontend/src/views/public/works/Detail.vue b/frontend/src/views/public/works/Detail.vue index 1df6a74..8e755c6 100644 --- a/frontend/src/views/public/works/Detail.vue +++ b/frontend/src/views/public/works/Detail.vue @@ -63,6 +63,18 @@
+ +
+
+ 画作原图 +
+
+
+
画作原图
+
AI 根据这张画作生成的绘本
+
+
+
@@ -187,6 +199,13 @@ >

{{ confirmContent }}

+ + + +
+ 画作原图 +
+
@@ -207,6 +226,7 @@ import { UndoOutlined, InboxOutlined, DeleteOutlined, + ZoomInOutlined, } from '@ant-design/icons-vue' import { publicUserWorksApi, @@ -228,6 +248,7 @@ const loading = ref(true) const currentPageIndex = ref(0) const interaction = ref({ liked: false, favorited: false }) const actionLoading = ref(false) +const previewOriginal = ref('') const currentPageData = computed(() => work.value?.pages?.[currentPageIndex.value] || null) @@ -592,6 +613,96 @@ $accent: #ec4899; .info-title { font-size: 13px; font-weight: 700; color: #1e1b4b; margin-bottom: 3px; } .info-desc { font-size: 12px; color: #6b7280; line-height: 1.6; } +/* ---------- 画作原图卡片 ---------- */ +.original-card { + display: flex; + align-items: center; + gap: 14px; + background: #fff; + border: 1px solid rgba($primary, 0.06); + border-radius: 16px; + padding: 14px; + margin-bottom: 14px; + box-shadow: 0 2px 12px rgba($primary, 0.05); +} +.original-thumb { + position: relative; + width: 84px; + aspect-ratio: 1 / 1; + border-radius: 12px; + overflow: hidden; + border: 2px solid rgba($primary, 0.18); + cursor: zoom-in; + flex-shrink: 0; + background: #f5f3ff; + transition: all 0.2s; + + &:hover { + border-color: $primary; + transform: scale(1.03); + .zoom-hint { opacity: 1; } + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + .zoom-hint { + position: absolute; + inset: 0; + background: rgba(15, 12, 41, 0.4); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + opacity: 0; + transition: opacity 0.2s; + } +} +.original-text { + flex: 1; + min-width: 0; +} +.original-title { + font-size: 14px; + font-weight: 700; + color: #1e1b4b; + margin-bottom: 4px; +} +.original-desc { + font-size: 12px; + color: #6b7280; + line-height: 1.5; +} + +/* ---------- 全屏原图预览 ---------- */ +.preview-overlay { + position: fixed; + inset: 0; + z-index: 999; + background: rgba(15, 12, 41, 0.88); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + cursor: zoom-out; +} +.preview-full-img { + max-width: 90%; + max-height: 80vh; + object-fit: contain; + border-radius: 16px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); +} +.fade-enter-active, +.fade-leave-active { transition: opacity 0.2s; } +.fade-enter-from, +.fade-leave-to { opacity: 0; } + /* ---------- 绘本阅读器 ---------- */ .book-reader { background: #fff; diff --git a/frontend/src/views/public/works/Index.vue b/frontend/src/views/public/works/Index.vue index af699a7..00612e4 100644 --- a/frontend/src/views/public/works/Index.vue +++ b/frontend/src/views/public/works/Index.vue @@ -30,10 +30,19 @@
+
+ +
+ 原图 +
{{ statusTextMap[work.status] || work.status }}
@@ -268,6 +277,32 @@ $primary: #6366f1; &.rejected { background: rgba(239,68,68,0.92); color: #fff; } &.taken_down { background: rgba(107,114,128,0.85); color: #fff; } } + + /* 右下角 PIP:用户原图 */ + .cover-pip { + position: absolute; + right: 8px; + bottom: 8px; + width: 34%; + aspect-ratio: 1 / 1; + border-radius: 8px; + overflow: hidden; + border: 2px solid #fff; + background: #fff; + box-shadow: 0 3px 10px rgba(15, 12, 41, 0.25); + transition: transform 0.2s; + + img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + } + } + + &:hover .cover-pip { + transform: scale(1.05); } .work-info { diff --git a/frontend/src/views/public/works/_dev-mock.ts b/frontend/src/views/public/works/_dev-mock.ts index 3a8cf4e..712eb67 100644 --- a/frontend/src/views/public/works/_dev-mock.ts +++ b/frontend/src/views/public/works/_dev-mock.ts @@ -48,7 +48,7 @@ export const MOCK_USER_WORKS: UserWork[] = [ visibility: 'private', status: 'unpublished', reviewNote: null, - originalImageUrl: null, + originalImageUrl: mockCover(40), voiceInputUrl: null, textInput: null, aiMeta: null, @@ -67,12 +67,12 @@ export const MOCK_USER_WORKS: UserWork[] = [ id: 102, userId: 1, title: '正在创作中…', - coverUrl: mockCover(180), + coverUrl: null, description: null, visibility: 'private', status: 'draft', reviewNote: null, - originalImageUrl: null, + originalImageUrl: mockCover(60), voiceInputUrl: null, textInput: null, aiMeta: null, @@ -96,7 +96,7 @@ export const MOCK_USER_WORKS: UserWork[] = [ visibility: 'public', status: 'pending_review', reviewNote: null, - originalImageUrl: null, + originalImageUrl: mockCover(80), voiceInputUrl: null, textInput: null, aiMeta: null, @@ -120,7 +120,7 @@ export const MOCK_USER_WORKS: UserWork[] = [ visibility: 'public', status: 'published', reviewNote: null, - originalImageUrl: null, + originalImageUrl: mockCover(120), voiceInputUrl: null, textInput: null, aiMeta: null, @@ -144,7 +144,7 @@ export const MOCK_USER_WORKS: UserWork[] = [ visibility: 'public', status: 'rejected', reviewNote: '内容包含疑似版权角色,请修改后重新提交', - originalImageUrl: null, + originalImageUrl: mockCover(160), voiceInputUrl: null, textInput: null, aiMeta: null,