feat: 添加噜噜支付SDK和前端懒加载指令
This commit is contained in:
112
demo/frontend/package-lock.json
generated
112
demo/frontend/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.44.1",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
},
|
||||
@@ -531,12 +532,55 @@
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
@@ -1036,6 +1080,19 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -1097,6 +1154,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1176,6 +1240,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -1965,6 +2036,16 @@
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1974,6 +2055,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -2000,6 +2092,26 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -10,24 +10,26 @@
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"pinia": "^2.1.6",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"qrcode": "^1.5.3"
|
||||
"pinia": "^2.1.6",
|
||||
"qrcode": "^1.5.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"vite": "^4.4.9",
|
||||
"sass": "^1.66.1"
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.44.1",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"keywords": ["vue", "frontend", "aigc"],
|
||||
"keywords": [
|
||||
"vue",
|
||||
"frontend",
|
||||
"aigc"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
120
demo/frontend/src/directives/lazyLoad.js
Normal file
120
demo/frontend/src/directives/lazyLoad.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 图片懒加载指令
|
||||
* 使用 Intersection Observer API 实现
|
||||
*/
|
||||
|
||||
// 默认占位图(1x1透明像素)
|
||||
const defaultPlaceholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||
|
||||
// 加载中占位图(可选,灰色背景)
|
||||
const loadingPlaceholder = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23333" width="100" height="100"/%3E%3C/svg%3E'
|
||||
|
||||
// 创建 Intersection Observer
|
||||
let observer = null
|
||||
|
||||
const getObserver = () => {
|
||||
if (observer) return observer
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const el = entry.target
|
||||
const src = el.dataset.src
|
||||
|
||||
if (src) {
|
||||
// 创建新图片预加载
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
el.src = src
|
||||
el.classList.add('lazy-loaded')
|
||||
el.classList.remove('lazy-loading')
|
||||
}
|
||||
img.onerror = () => {
|
||||
el.classList.add('lazy-error')
|
||||
el.classList.remove('lazy-loading')
|
||||
}
|
||||
img.src = src
|
||||
}
|
||||
|
||||
// 停止观察
|
||||
observer.unobserve(el)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
rootMargin: '100px', // 提前100px开始加载
|
||||
threshold: 0.1
|
||||
})
|
||||
|
||||
return observer
|
||||
}
|
||||
|
||||
export const lazyLoad = {
|
||||
mounted(el, binding) {
|
||||
const src = binding.value
|
||||
|
||||
if (!src) return
|
||||
|
||||
// 保存真实src
|
||||
el.dataset.src = src
|
||||
|
||||
// 设置占位图
|
||||
el.src = binding.arg === 'loading' ? loadingPlaceholder : defaultPlaceholder
|
||||
el.classList.add('lazy-loading')
|
||||
|
||||
// 开始观察
|
||||
getObserver().observe(el)
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
// 如果src变化,重新加载
|
||||
if (binding.value !== binding.oldValue && binding.value) {
|
||||
el.dataset.src = binding.value
|
||||
el.classList.remove('lazy-loaded', 'lazy-error')
|
||||
el.classList.add('lazy-loading')
|
||||
getObserver().observe(el)
|
||||
}
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
if (observer) {
|
||||
observer.unobserve(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视频懒加载指令
|
||||
export const lazyVideo = {
|
||||
mounted(el, binding) {
|
||||
const src = binding.value
|
||||
|
||||
if (!src) return
|
||||
|
||||
el.dataset.src = src
|
||||
el.preload = 'none' // 不预加载
|
||||
el.classList.add('lazy-loading')
|
||||
|
||||
const videoObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
el.src = el.dataset.src
|
||||
el.preload = 'metadata' // 只加载元数据
|
||||
el.classList.add('lazy-loaded')
|
||||
el.classList.remove('lazy-loading')
|
||||
videoObserver.unobserve(el)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1
|
||||
})
|
||||
|
||||
videoObserver.observe(el)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
app.directive('lazy', lazyLoad)
|
||||
app.directive('lazy-video', lazyVideo)
|
||||
}
|
||||
}
|
||||
@@ -481,7 +481,9 @@ export default {
|
||||
videoFileNotExist: 'Video file may not exist or has been deleted',
|
||||
retry: 'Retry',
|
||||
deleteFailedWork: 'Delete This Work',
|
||||
deleteFailedWorkConfirm: 'This work\'s video failed to load. Are you sure you want to delete it? This action cannot be undone.'
|
||||
deleteFailedWorkConfirm: 'This work\'s video failed to load. Are you sure you want to delete it? This action cannot be undone.',
|
||||
readyToGenerateVideo: 'Storyboard image loaded, ready to generate video',
|
||||
noDownloadUrl: 'No downloadable file available'
|
||||
},
|
||||
|
||||
subscription: {
|
||||
|
||||
@@ -495,7 +495,9 @@ export default {
|
||||
videoFileNotExist: '视频文件可能不存在或已被删除',
|
||||
retry: '重试',
|
||||
deleteFailedWork: '删除此作品',
|
||||
deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。'
|
||||
deleteFailedWorkConfirm: '此作品视频加载失败,确定要删除吗?删除后无法恢复。',
|
||||
readyToGenerateVideo: '已填充分镜图,可以开始生成视频',
|
||||
noDownloadUrl: '没有可下载的文件'
|
||||
},
|
||||
|
||||
subscription: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
import { useUserStore } from './stores/user'
|
||||
import lazyLoadDirective from './directives/lazyLoad'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -15,6 +16,7 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
app.use(lazyLoadDirective)
|
||||
|
||||
console.log('[main.js] i18n 当前语言:', i18n.global.locale.value)
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<!-- 优先使用首帧作为封面,如果没有则使用视频 -->
|
||||
<img
|
||||
v-if="work.firstFrameUrl"
|
||||
:src="work.firstFrameUrl"
|
||||
v-lazy:loading="work.firstFrameUrl"
|
||||
:alt="work.title || work.prompt"
|
||||
class="work-image-thumbnail"
|
||||
/>
|
||||
@@ -287,6 +287,31 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.image-to-video-page {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
></video>
|
||||
</div>
|
||||
<div v-else-if="task.firstFrameUrl" class="history-image-thumbnail">
|
||||
<img :src="task.firstFrameUrl" :alt="t('video.imageToVideo.firstFrameImage')" />
|
||||
<img v-lazy:loading="task.firstFrameUrl" :alt="t('video.imageToVideo.firstFrameImage')" />
|
||||
</div>
|
||||
<div v-else class="history-placeholder">
|
||||
<div class="no-result-text">{{ t('video.imageToVideo.noResult') }}</div>
|
||||
@@ -1546,6 +1546,31 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.image-to-video-create-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
|
||||
@@ -170,10 +170,10 @@
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError"
|
||||
></video>
|
||||
<!-- 如果有封面图,使用图片 -->
|
||||
<!-- 如果有封面图,使用图片(懒加载) -->
|
||||
<img
|
||||
v-else-if="item.cover"
|
||||
:src="item.cover"
|
||||
v-lazy:loading="item.cover"
|
||||
:alt="item.title"
|
||||
@error="onImageError"
|
||||
/>
|
||||
@@ -208,21 +208,28 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- 鼠标悬停时显示的做同款按钮 -->
|
||||
<div class="hover-create-btn" @click.stop="createSimilar(item)">
|
||||
<el-button type="primary" size="small" round>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
{{ t('profile.createSimilar') }}
|
||||
</el-button>
|
||||
<!-- 鼠标悬停时显示的按钮区域 -->
|
||||
<div class="hover-buttons-container">
|
||||
<!-- 做同款按钮 - 左下角 -->
|
||||
<div class="hover-create-btn left" @click.stop="createSimilar(item)">
|
||||
<el-button type="primary" size="small" round>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
{{ t('profile.createSimilar') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 生视频按钮 - 右下角(仅分镜图显示) -->
|
||||
<div v-if="item.category === '分镜图' || item.workType === 'STORYBOARD_IMAGE'" class="hover-create-btn right" @click.stop="goToGenerateVideo(item)">
|
||||
<el-button type="success" size="small" round>
|
||||
<el-icon><Film /></el-icon>
|
||||
{{ t('video.storyboard.generateVideo') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title" :title="item.title">{{ item.title }}</div>
|
||||
<div class="sub">
|
||||
{{ item.date || t('profile.unknown') }} · {{ item.id }}
|
||||
<span v-if="item.quality && item.type !== 'image'" class="quality-badge" :class="`quality-${(item.quality || '').toLowerCase()}`">
|
||||
{{ formatQuality(item.quality) }}
|
||||
</span>
|
||||
<span v-if="item.sizeText && item.sizeText !== '未知大小'"> · {{ item.sizeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,13 +365,6 @@
|
||||
<span class="value">{{ selectedItem.aspectRatio || '16:9' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="action-section">
|
||||
<button class="create-similar-btn full-width" @click="createSimilar(selectedItem)">
|
||||
{{ t('works.createSimilar') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -566,8 +566,10 @@ const transformWorkData = (work) => {
|
||||
quality: work.quality || work.resolution || '',
|
||||
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||
status: work.status || 'COMPLETED',
|
||||
uploadedImages: work.uploadedImages || null, // 用户上传的参考图,用于做同款
|
||||
uploadedImages: work.uploadedImages || null, // 分镜图阶段用户上传的参考图
|
||||
videoReferenceImages: work.videoReferenceImages || null, // 视频阶段用户上传的参考图(分镜视频做同款需要)
|
||||
imagePrompt: work.imagePrompt || null, // 分镜图优化后的提示词
|
||||
videoPrompt: work.videoPrompt || null, // 视频优化后的提示词
|
||||
workType: work.workType || '', // 原始作品类型
|
||||
// overlayText 已移除,前端详情不再显示浮动文本
|
||||
}
|
||||
@@ -1113,12 +1115,6 @@ const createSimilar = (item) => {
|
||||
query.referenceImage = item.cover
|
||||
}
|
||||
|
||||
// 用户上传的参考图(分镜图/分镜视频做同款需要)
|
||||
// uploadedImages 现在存储的是 COS URL,可以直接通过 URL 传递
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
|
||||
console.log('[做同款] 跳转参数:', query, 'category:', item.category, 'workType:', item.workType)
|
||||
|
||||
if (item.category === '文生视频') {
|
||||
@@ -1131,14 +1127,22 @@ const createSimilar = (item) => {
|
||||
if (item.imagePrompt) {
|
||||
query.imagePrompt = item.imagePrompt
|
||||
}
|
||||
// 传递分镜图阶段的参考图
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else if (item.category === '分镜视频') {
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图和视频参考图
|
||||
query.step = 'video'
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图参考
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.storyboardImage = item.cover
|
||||
}
|
||||
// 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图
|
||||
if (item.videoReferenceImages) {
|
||||
query.videoReferenceImages = item.videoReferenceImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else {
|
||||
// 默认跳转到文生视频
|
||||
@@ -1148,6 +1152,38 @@ const createSimilar = (item) => {
|
||||
ElMessage.success(t('works.createSimilarInfo', { title: item.title }))
|
||||
}
|
||||
|
||||
// 生视频 - 使用分镜图直接生成视频
|
||||
const goToGenerateVideo = (item) => {
|
||||
if (!item) return
|
||||
|
||||
// 跳转到图生视频创作页面,传递分镜图作为参考图
|
||||
const query = {
|
||||
aspectRatio: item.aspectRatio || '',
|
||||
duration: item.duration || '',
|
||||
hdMode: item.quality === 'HD' ? 'true' : 'false'
|
||||
}
|
||||
|
||||
// 使用分镜图作为参考图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.referenceImage = item.cover
|
||||
} else if (item.resultUrl) {
|
||||
query.referenceImage = item.resultUrl
|
||||
}
|
||||
|
||||
// 传递视频提示词:优先使用 videoPrompt,其次 imagePrompt,最后 prompt
|
||||
if (item.videoPrompt) {
|
||||
query.prompt = item.videoPrompt
|
||||
} else if (item.imagePrompt) {
|
||||
query.prompt = item.imagePrompt
|
||||
} else if (item.prompt) {
|
||||
query.prompt = item.prompt
|
||||
}
|
||||
|
||||
console.log('[生视频] 跳转到图生视频页面,参数:', query)
|
||||
router.push({ path: '/image-to-video/create', query })
|
||||
ElMessage.success(t('works.readyToGenerateVideo'))
|
||||
}
|
||||
|
||||
const download = async (item) => {
|
||||
try {
|
||||
// 检查是否有结果URL
|
||||
@@ -1648,6 +1684,31 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.works-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
@@ -2174,17 +2235,39 @@ onActivated(() => {
|
||||
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
|
||||
.thumb:hover .actions { opacity: 1; }
|
||||
|
||||
/* 鼠标悬停时显示的做同款按钮 */
|
||||
.hover-create-btn {
|
||||
/* 鼠标悬停时显示的按钮容器 */
|
||||
.hover-buttons-container {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.thumb:hover .hover-buttons-container {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 鼠标悬停时显示的做同款按钮 */
|
||||
.hover-create-btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-create-btn.left {
|
||||
/* 左下角 */
|
||||
}
|
||||
|
||||
.hover-create-btn.right {
|
||||
/* 右下角 */
|
||||
}
|
||||
|
||||
.thumb:hover .hover-create-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -2201,6 +2284,16 @@ onActivated(() => {
|
||||
background: rgba(64, 158, 255, 1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 生视频按钮样式 */
|
||||
.hover-create-btn.right .el-button {
|
||||
background: rgba(103, 194, 58, 0.9);
|
||||
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
.hover-create-btn.right .el-button:hover {
|
||||
background: rgba(103, 194, 58, 1);
|
||||
}
|
||||
.work-card.selected .thumb::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -110,10 +110,10 @@
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError($event, video)"
|
||||
></video>
|
||||
<!-- 如果有封面图,使用图片 -->
|
||||
<!-- 如果有封面图,使用图片(懒加载) -->
|
||||
<img
|
||||
v-else-if="video.cover"
|
||||
:src="video.cover"
|
||||
v-lazy:loading="video.cover"
|
||||
:alt="video.title"
|
||||
class="video-cover-img"
|
||||
@error="onImageError"
|
||||
@@ -125,10 +125,17 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-action">
|
||||
<el-button v-if="video.status === 'COMPLETED'" type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
|
||||
<span v-else-if="video.status === 'FAILED'" class="status-text failed">生成失败</span>
|
||||
<span v-else class="status-text processing">{{ video.status === 'PENDING' ? '排队中...' : '生成中...' }}</span>
|
||||
<div class="video-action" v-if="video.status === 'COMPLETED'">
|
||||
<!-- 做同款按钮 - 左侧 -->
|
||||
<el-button type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
|
||||
<!-- 生视频按钮 - 右侧(仅分镜图显示) -->
|
||||
<el-button v-if="video.category === '分镜图' || video.workType === 'STORYBOARD_IMAGE'" type="success" size="small" @click.stop="goToGenerateVideo(video)">{{ t('video.storyboard.generateVideo') }}</el-button>
|
||||
</div>
|
||||
<div class="video-action" v-else-if="video.status === 'FAILED'">
|
||||
<span class="status-text failed">生成失败</span>
|
||||
</div>
|
||||
<div class="video-action" v-else>
|
||||
<span class="status-text processing">{{ video.status === 'PENDING' ? '排队中...' : '生成中...' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,12 +229,6 @@
|
||||
<span class="value">{{ selectedItem.aspectRatio || t('profile.unknown') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-section">
|
||||
<button class="create-similar-btn" @click="createSimilar(selectedItem)">
|
||||
{{ t('profile.createSimilar') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -541,12 +542,6 @@ const createSimilar = (item) => {
|
||||
query.referenceImage = item.cover
|
||||
}
|
||||
|
||||
// 用户上传的参考图(分镜图/分镜视频做同款需要)
|
||||
// uploadedImages 现在存储的是 COS URL,可以直接通过 URL 传递
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
|
||||
console.log('[做同款] 跳转参数:', query, 'category:', item.category, 'workType:', item.workType)
|
||||
|
||||
// 根据作品类型跳转
|
||||
@@ -560,14 +555,22 @@ const createSimilar = (item) => {
|
||||
if (item.imagePrompt) {
|
||||
query.imagePrompt = item.imagePrompt
|
||||
}
|
||||
// 传递分镜图阶段的参考图
|
||||
if (item.uploadedImages) {
|
||||
query.uploadedImages = item.uploadedImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else if (item.category === '分镜视频') {
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图
|
||||
// 分镜视频做同款:进入 Step 2(生成视频),携带已生成的分镜图和视频参考图
|
||||
query.step = 'video'
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图参考
|
||||
// cover 是分镜图(thumbnailUrl),传递给 Step 2 作为分镜图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.storyboardImage = item.cover
|
||||
}
|
||||
// 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图
|
||||
if (item.videoReferenceImages) {
|
||||
query.videoReferenceImages = item.videoReferenceImages
|
||||
}
|
||||
router.push({ path: '/storyboard-video/create', query })
|
||||
} else {
|
||||
// 默认根据类型跳转
|
||||
@@ -579,6 +582,38 @@ const createSimilar = (item) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生视频 - 使用分镜图直接生成视频(跳转到图生视频页面)
|
||||
const goToGenerateVideo = (item) => {
|
||||
if (!item) return
|
||||
|
||||
// 跳转到图生视频创作页面,传递分镜图作为参考图
|
||||
const query = {
|
||||
aspectRatio: item.aspectRatio || '',
|
||||
duration: item.duration || '',
|
||||
hdMode: item.quality === 'HD' ? 'true' : 'false'
|
||||
}
|
||||
|
||||
// 使用分镜图作为参考图
|
||||
if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') {
|
||||
query.referenceImage = item.cover
|
||||
} else if (item.resultUrl) {
|
||||
query.referenceImage = item.resultUrl
|
||||
}
|
||||
|
||||
// 传递视频提示词:优先使用 videoPrompt,其次 imagePrompt,最后 prompt
|
||||
if (item.videoPrompt) {
|
||||
query.prompt = item.videoPrompt
|
||||
} else if (item.imagePrompt) {
|
||||
query.prompt = item.imagePrompt
|
||||
} else if (item.prompt) {
|
||||
query.prompt = item.prompt
|
||||
}
|
||||
|
||||
console.log('[生视频] 跳转到图生视频页面,参数:', query)
|
||||
router.push({ path: '/image-to-video/create', query })
|
||||
ElMessage.success(t('works.readyToGenerateVideo'))
|
||||
}
|
||||
|
||||
// 处理URL,确保相对路径正确
|
||||
const processUrl = (url) => {
|
||||
if (!url) return null
|
||||
@@ -620,7 +655,8 @@ const transformWorkData = (work) => {
|
||||
quality: work.quality || work.resolution || '',
|
||||
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||
status: work.status || 'COMPLETED',
|
||||
uploadedImages: work.uploadedImages || null, // 用户上传的参考图,用于做同款
|
||||
uploadedImages: work.uploadedImages || null, // 分镜图阶段用户上传的参考图
|
||||
videoReferenceImages: work.videoReferenceImages || null, // 视频阶段用户上传的参考图(分镜视频做同款需要)
|
||||
imagePrompt: work.imagePrompt || null, // 分镜图优化后的提示词
|
||||
workType: work.workType || '', // 原始作品类型
|
||||
}
|
||||
@@ -1374,12 +1410,26 @@ onUnmounted(() => {
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.video-item:hover .video-action {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 生视频按钮样式 */
|
||||
.video-action .el-button--success {
|
||||
background: rgba(103, 194, 58, 0.9);
|
||||
border-color: rgba(103, 194, 58, 0.9);
|
||||
}
|
||||
|
||||
.video-action .el-button--success:hover {
|
||||
background: rgba(103, 194, 58, 1);
|
||||
border-color: rgba(103, 194, 58, 1);
|
||||
}
|
||||
|
||||
.director-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="works-grid">
|
||||
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
|
||||
<div class="work-thumbnail">
|
||||
<img :src="work.cover" :alt="work.title" />
|
||||
<img v-lazy:loading="work.cover" :alt="work.title" />
|
||||
<!-- 鼠标悬停时显示的做同款按钮 -->
|
||||
<div class="hover-create-btn" @click.stop="goToCreate(work)">
|
||||
<el-button type="primary" size="small" round>
|
||||
@@ -249,6 +249,31 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.storyboard-video-page {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
@@ -573,7 +573,7 @@
|
||||
></video>
|
||||
</div>
|
||||
<div v-else-if="task.resultUrl && isImageUrl(task.resultUrl)" class="history-image-thumbnail">
|
||||
<img :src="processHistoryUrl(task.resultUrl)" :alt="t('video.storyboard.storyboardImage')" @error="handleImageError" />
|
||||
<img v-lazy:loading="processHistoryUrl(task.resultUrl)" :alt="t('video.storyboard.storyboardImage')" @error="handleImageError" />
|
||||
</div>
|
||||
<div v-else class="history-placeholder">
|
||||
<div class="no-result-text">{{ t('video.storyboard.noResult') }}</div>
|
||||
@@ -711,6 +711,7 @@ const videoPollIntervalId = ref(null) // 视频任务轮询定时器ID
|
||||
const isCreatingTask = ref(false) // 标记是否正在创建任务,避免重复恢复
|
||||
const hasRestoredTask = ref(false) // 标记是否已经恢复过任务
|
||||
let isFromCreateSimilar = false // 标记是否从"做同款"进入(非响应式,仅用于onMounted判断)
|
||||
const isFromGenerateVideo = ref(false) // 标记是否从"生视频"按钮进入(有分镜图但无taskId)
|
||||
const videoTaskStatus = ref('') // 视频任务状态:PROCESSING, COMPLETED, FAILED
|
||||
const videoResultUrl = ref('') // 视频结果URL
|
||||
const videoProgress = ref(0) // 视频生成进度
|
||||
@@ -1993,7 +1994,8 @@ const startVideoGenerate = async () => {
|
||||
duration: parseInt(duration.value),
|
||||
aspectRatio: aspectRatio.value,
|
||||
hdMode: hdMode.value,
|
||||
referenceImages: referenceImages // 只传递视频阶段的参考图
|
||||
referenceImages: referenceImages, // 只传递视频阶段的参考图
|
||||
storyboardImage: generatedImageUrl.value // 传递分镜图URL,用于恢复被覆盖的分镜图
|
||||
})
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
@@ -2019,8 +2021,13 @@ const startVideoGenerate = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果从"生视频"按钮进入(有分镜图但无taskId),使用分镜图直接生成视频
|
||||
if (isFromGenerateVideo.value && generatedImageUrl.value) {
|
||||
console.log('[startVideoGenerate] 从"生视频"按钮进入,使用分镜图直接生成视频')
|
||||
}
|
||||
|
||||
// 如果没有分镜图任务ID,使用上传的分镜图直接生成视频
|
||||
let imageUrl = mainReferenceImage.value || uploadedImage.value
|
||||
let imageUrl = mainReferenceImage.value || generatedImageUrl.value || uploadedImage.value
|
||||
|
||||
if (!imageUrl) {
|
||||
ElMessage.warning(t('video.storyboard.uploadOrGenerateFirst'))
|
||||
@@ -2114,7 +2121,8 @@ const startVideoGenerate = async () => {
|
||||
const videoResponse = await startVideoGeneration(newTaskId, {
|
||||
duration: parseInt(duration.value),
|
||||
aspectRatio: aspectRatio.value,
|
||||
hdMode: hdMode.value
|
||||
hdMode: hdMode.value,
|
||||
storyboardImage: imageUrl
|
||||
})
|
||||
|
||||
if (videoResponse.data && videoResponse.data.success) {
|
||||
@@ -3133,11 +3141,18 @@ onMounted(async () => {
|
||||
console.log('[做同款-视频] 设置 videoPrompt:', route.query.prompt.substring(0, 100))
|
||||
}
|
||||
|
||||
// 复用分镜图的taskId
|
||||
if (route.query.taskId) {
|
||||
taskId.value = route.query.taskId
|
||||
console.log('[生视频] 复用分镜图taskId:', route.query.taskId)
|
||||
}
|
||||
|
||||
// 设置已生成的分镜图
|
||||
if (route.query.storyboardImage) {
|
||||
generatedImageUrl.value = route.query.storyboardImage
|
||||
mainReferenceImage.value = route.query.storyboardImage
|
||||
isAIGeneratedImage.value = true
|
||||
isFromGenerateVideo.value = true // 标记从"生视频"按钮进入
|
||||
console.log('[做同款-视频] 设置分镜图:', route.query.storyboardImage)
|
||||
}
|
||||
} else {
|
||||
@@ -3163,7 +3178,7 @@ onMounted(async () => {
|
||||
hdMode.value = route.query.hdMode === 'true'
|
||||
}
|
||||
|
||||
// 处理用户上传的参考图(uploadedImages 现在是 COS URL 的 JSON 数组)
|
||||
// 处理分镜图阶段用户上传的参考图(uploadedImages)
|
||||
if (route.query.uploadedImages) {
|
||||
try {
|
||||
const parsedImages = typeof route.query.uploadedImages === 'string'
|
||||
@@ -3178,20 +3193,10 @@ onMounted(async () => {
|
||||
name: `参考图片${idx + 1}`
|
||||
}))
|
||||
|
||||
// 根据当前步骤决定填充到哪个参考图数组
|
||||
if (currentStep.value === 'video') {
|
||||
// Step 2:填充到视频参考图
|
||||
videoReferenceImages.value = [null, null, null]
|
||||
imageObjects.forEach((img, idx) => {
|
||||
if (idx < 3) {
|
||||
videoReferenceImages.value[idx] = img
|
||||
}
|
||||
})
|
||||
console.log('[做同款-视频] 恢复用户上传图片:', imageObjects.length, '张')
|
||||
} else {
|
||||
// Step 1:填充到分镜图参考图
|
||||
// 分镜图阶段的参考图只填充到 Step 1
|
||||
if (currentStep.value !== 'video') {
|
||||
uploadedImages.value = imageObjects
|
||||
console.log('[做同款-图] 恢复用户上传图片:', imageObjects.length, '张')
|
||||
console.log('[做同款-图] 恢复分镜图阶段参考图:', imageObjects.length, '张')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -3207,6 +3212,35 @@ onMounted(async () => {
|
||||
console.log('[做同款] 设置参考图:', route.query.referenceImage)
|
||||
}
|
||||
|
||||
// 处理视频阶段用户上传的参考图(videoReferenceImages)- 分镜视频做同款专用
|
||||
if (route.query.videoReferenceImages) {
|
||||
try {
|
||||
const parsedImages = typeof route.query.videoReferenceImages === 'string'
|
||||
? JSON.parse(route.query.videoReferenceImages)
|
||||
: route.query.videoReferenceImages
|
||||
if (Array.isArray(parsedImages) && parsedImages.length > 0) {
|
||||
const imageObjects = parsedImages
|
||||
.filter(img => img && img !== 'null')
|
||||
.map((url, idx) => ({
|
||||
url: url,
|
||||
file: null,
|
||||
name: `视频参考图${idx + 1}`
|
||||
}))
|
||||
|
||||
// 视频阶段的参考图填充到 videoReferenceImages
|
||||
videoReferenceImages.value = [null, null, null]
|
||||
imageObjects.forEach((img, idx) => {
|
||||
if (idx < 3) {
|
||||
videoReferenceImages.value[idx] = img
|
||||
}
|
||||
})
|
||||
console.log('[做同款-视频] 恢复视频阶段参考图:', imageObjects.length, '张')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[做同款] 解析 videoReferenceImages 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 静默填充,不显示弹窗提示(减少干扰)
|
||||
// 清除URL中的query参数,避免刷新页面重复填充
|
||||
router.replace({ path: route.path })
|
||||
@@ -3245,6 +3279,31 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-loading {
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: lazy-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.lazy-loaded {
|
||||
animation: lazy-fade-in 0.3s ease-in;
|
||||
}
|
||||
|
||||
.lazy-error {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@keyframes lazy-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@keyframes lazy-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.storyboard-video-create-page {
|
||||
height: 100vh;
|
||||
background: #0a0a0a;
|
||||
|
||||
Reference in New Issue
Block a user