jvs-ppt-ui 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2229 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
+ import { View, Plus, VideoPlay } from '@element-plus/icons-vue'
4
+ import { useSlides } from '../../composables/useSlides'
5
+ import { useDamAuth } from './useDamAuth'
6
+ import type { MaterialItem } from '../TemplateGallery.vue'
7
+
8
+ export interface ImageModelMetaVO {
9
+ provider?: string
10
+ providerName?: string
11
+ vendor?: string
12
+ model: string
13
+ displayName?: string
14
+ description?: string
15
+ defaultSize?: string
16
+ size?: string | string[]
17
+ recommendedSizes?: string[]
18
+ supportsRefImage?: boolean
19
+ isDefault?: boolean
20
+ }
21
+
22
+ export interface ReferenceItem {
23
+ id: string
24
+ name: string
25
+ url: string
26
+ type: 'style' | 'character' | 'add'
27
+ assetId?: string
28
+ assetType?: string
29
+ sourceType?: string
30
+ }
31
+
32
+ const DEFAULT_IMAGE_MODELS: ImageModelMetaVO[] = [
33
+ {
34
+ provider: 'DASHSCOPE',
35
+ providerName: '阿里百炼',
36
+ vendor: '通义千问',
37
+ model: 'qwen-image-3.0',
38
+ displayName: '通义千问生图 3.0',
39
+ description: '新一代真实感高画质生图模型,擅长版面布局与极清文字渲染',
40
+ size: '1328*1328',
41
+ recommendedSizes: ['1328*1328', '1024*1024', '1280*720', '720*1280'],
42
+ supportsRefImage: true,
43
+ isDefault: true
44
+ },
45
+ {
46
+ provider: 'DASHSCOPE',
47
+ providerName: '阿里百炼',
48
+ vendor: '通义千问',
49
+ model: 'qwen-image-3.0-pro',
50
+ displayName: '通义千问生图 3.0 Pro',
51
+ description: '专业级画质生图大模型,提供摄影级光影与高保真质感',
52
+ size: '1328*1328',
53
+ recommendedSizes: ['1328*1328', '1024*1024', '1280*720', '720*1280'],
54
+ supportsRefImage: true,
55
+ isDefault: false
56
+ },
57
+ {
58
+ provider: 'DASHSCOPE',
59
+ providerName: '阿里百炼',
60
+ vendor: '通义千问',
61
+ model: 'qwen-image-max',
62
+ displayName: '通义千问生图 Max',
63
+ description: '旗舰级多模态文生图模型,色彩绚丽生动,适合商业海报与插画',
64
+ size: '1328*1328',
65
+ recommendedSizes: ['1328*1328', '1024*1024', '1280*720', '720*1280'],
66
+ supportsRefImage: true,
67
+ isDefault: false
68
+ },
69
+ {
70
+ provider: 'DASHSCOPE',
71
+ providerName: '阿里百炼',
72
+ vendor: '通义万相',
73
+ model: 'wan2.7-image',
74
+ displayName: '通义万相 2.7 图像生成',
75
+ description: '万相 2.7 旗舰文生图模型,构图与光影细节极佳',
76
+ size: '1024*1024',
77
+ recommendedSizes: ['1024*1024', '1280*720', '720*1280', '1024*768', '768*1024'],
78
+ supportsRefImage: false,
79
+ isDefault: false
80
+ },
81
+ {
82
+ provider: 'DASHSCOPE',
83
+ providerName: '阿里百炼',
84
+ vendor: '通义万相',
85
+ model: 'wanx2.1-t2i-turbo',
86
+ displayName: '通义万相 2.1 Turbo',
87
+ description: '极速出图模型,适合高频概念预览与灵感探索',
88
+ size: '1024*1024',
89
+ recommendedSizes: ['1024*1024', '1280*720', '720*1280'],
90
+ supportsRefImage: false,
91
+ isDefault: false
92
+ }
93
+ ]
94
+
95
+ const DEFAULT_VIDEO_MODELS = [
96
+ { model: 'wanx2.1-i2v-plus', displayName: '通义万相 2.1 视频增强版', vendor: '通义万相' },
97
+ { model: 'Wan 3.0', displayName: '通义万相 Wan 3.0 视频生成', vendor: '通义万相' },
98
+ { model: 'Seedance 2.5', displayName: 'Seedance 2.5 动态视频', vendor: 'ByteDance' },
99
+ { model: 'Kling 3.0', displayName: '可灵 Kling 3.0 高清视频', vendor: 'Kling' },
100
+ { model: 'MiniMax H3', displayName: '海螺 MiniMax H3 视频', vendor: 'MiniMax' }
101
+ ]
102
+
103
+ const PROMPT_TEMPLATES = {
104
+ image: [
105
+ { title: '科技发布会背景', prompt: '极简未来感科技展厅背景,深海蓝与流光青渐变,柔和体积光,3D微距玻璃几何线条,8k超高清PPT背景' },
106
+ { title: '3D 商务玻璃图标', prompt: 'C4D 渲染 3D 磨砂玻璃质感商务数据分析图标,柔和工作室布光,干净纯白浅灰背景,OC渲染器高细节' },
107
+ { title: '新中式国风插画', prompt: '现代新中式国潮山水云纹插画,金色与青绿设色,留白构图适合PPT封面排版,细腻纸张纹理' },
108
+ { title: '企业团队办公摄影', prompt: '明亮现代化的落地窗办公室,年轻专业的商务团队围绕全息屏幕讨论,自然晨光,电影级景深摄影' }
109
+ ],
110
+ video: [
111
+ { title: '流光粒子科技背景', prompt: '深蓝色数字科技空间,柔和发光粒子缓慢向上漂浮汇聚成波浪网格,镜头平滑向前推进,无缝循环感 4K' },
112
+ { title: '晨雾森林航拍推进', prompt: '无人机航拍视角平稳穿过清晨薄雾笼罩的松林,金色阳光透过树冠洒下丁达尔光束,电影级4K 60fps' },
113
+ { title: '现代城市延时天际线', prompt: '繁华现代都市CBD玻璃幕墙天际线,云层快速流动,光影在建筑表面缓慢推移,商业大片质感' }
114
+ ]
115
+ }
116
+
117
+ const QUICK_STYLE_TAGS = [
118
+ '电影级光影',
119
+ '3D 磨砂玻璃',
120
+ '极简留白构图',
121
+ '赛博朋克霓虹',
122
+ '柔和体积光',
123
+ '扁平矢量插画',
124
+ '8K 超清细节',
125
+ '等轴测微缩景观'
126
+ ]
127
+
128
+ const PRESET_STYLE_REFS = [
129
+ { id: 'style-3d', name: '3D 玻璃质感', url: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=400&auto=format&fit=crop&q=80' },
130
+ { id: 'style-cyber', name: '流光科技感', url: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=400&auto=format&fit=crop&q=80' },
131
+ { id: 'style-minimal', name: '极简建筑光影', url: 'https://images.unsplash.com/photo-1513694203232-719a280e022f?w=400&auto=format&fit=crop&q=80' },
132
+ { id: 'style-art', name: '暖调商业插画', url: 'https://images.unsplash.com/photo-1579783902614-a3fb3927b675?w=400&auto=format&fit=crop&q=80' }
133
+ ]
134
+
135
+ const emit = defineEmits<{
136
+ (e: 'select', item: MaterialItem): void
137
+ (e: 'preview', item: MaterialItem, list: MaterialItem[], event?: Event): void
138
+ (e: 'generated', item: MaterialItem): void
139
+ }>()
140
+
141
+ const {
142
+ notify,
143
+ activeSlide,
144
+ selectedElementId,
145
+ placeholderMaterialTargetId,
146
+ resolveActivePlaceholderTargetId
147
+ } = useSlides()
148
+ const selectedElement = computed(() => {
149
+ if (!selectedElementId?.value) return null
150
+ return activeSlide?.value?.elements?.find((el: any) => el.id === selectedElementId.value) || null
151
+ })
152
+ const hasActivePlaceholderTarget = computed(() => {
153
+ const resolved = resolveActivePlaceholderTargetId ? resolveActivePlaceholderTargetId() : null
154
+ if (resolved) return true
155
+ if (placeholderMaterialTargetId?.value) return true
156
+ if (typeof window !== 'undefined' && (window as any).__jvsPlaceholderTargetId) return true
157
+ return false
158
+ })
159
+ const { damFetchJson, isDamLoggedIn } = useDamAuth()
160
+
161
+ // 生成器模式:image (图片生成器 - 图二) | video (视频生成)
162
+ const createMode = ref<'image' | 'video'>('image')
163
+
164
+ // 模型列表与选择状态
165
+ const imageModels = ref<ImageModelMetaVO[]>([...DEFAULT_IMAGE_MODELS])
166
+ const selectedImageModel = ref<string>('qwen-image-3.0')
167
+ const selectedVideoModel = ref<string>('wanx2.1-i2v-plus')
168
+ const showModelDropdown = ref(false)
169
+ const showTemplatePopover = ref(false)
170
+
171
+ // 参考素材状态 (图片模式 0/8,视频模式首尾帧)
172
+ const referenceList = ref<ReferenceItem[]>([])
173
+ const videoStartImage = ref<ReferenceItem | null>(null)
174
+ const videoEndImage = ref<ReferenceItem | null>(null)
175
+ const refPickerVisible = ref(false)
176
+ const refPickerTargetMode = ref<'style' | 'character' | 'add' | 'video-start' | 'video-end'>('add')
177
+ const recentPickerAssets = ref<MaterialItem[]>([])
178
+ const loadingPickerAssets = ref(false)
179
+ const refFileInputRef = ref<HTMLInputElement | null>(null)
180
+ const uploadingRefFile = ref(false)
181
+
182
+ // 提示词与底部工具状态
183
+ const promptText = ref('')
184
+ const isAiPromptOn = ref(true)
185
+ const isFixedSeedOn = ref(false)
186
+ const showSeedPopover = ref(false)
187
+ const showTagPopover = ref(false)
188
+ const showExpandModal = ref(false)
189
+ const expandedPromptDraft = ref('')
190
+
191
+ // 参数胶囊状态 (图片模式:数量 / 宽高比 / 清晰度)
192
+ const batchCount = ref(1)
193
+ const aspectRatio = ref<'1:1' | '16:9' | '9:16' | '4:3' | '3:4'>('1:1')
194
+ const qualitySpec = ref<'1K · High' | '1K · Fast' | '2K · HD'>('1K · High')
195
+ const showRatioDropdown = ref(false)
196
+ const showQualityDropdown = ref(false)
197
+
198
+ // 视频模式专属参数状态
199
+ const videoDuration = ref<'5-6"' | '10"'>('5-6"')
200
+ const videoRatio = ref<'16:9' | '9:16' | '1:1'>('16:9')
201
+ const videoMotionOn = ref(true)
202
+ const showVideoDurationDropdown = ref(false)
203
+ const showVideoRatioDropdown = ref(false)
204
+
205
+ // 生成执行与结果列表
206
+ const isGenerating = ref(false)
207
+ const currentSessionId = ref<string | undefined>(undefined)
208
+ const generatedResults = ref<MaterialItem[]>([])
209
+
210
+ const currentImageModelMeta = computed(() => {
211
+ return imageModels.value.find(m => m.model === selectedImageModel.value) || imageModels.value[0] || DEFAULT_IMAGE_MODELS[0]
212
+ })
213
+
214
+ const isWanxBrand = computed(() => {
215
+ const m = selectedImageModel.value.toLowerCase()
216
+ return m.includes('wan')
217
+ })
218
+
219
+ // 检测当前 PPT 画布是否选中了图片元素(支持一键拾取为参考图)
220
+ const canvasSelectedImageUrl = computed(() => {
221
+ const el = selectedElement.value as any
222
+ if (!el) return ''
223
+ if (el.type === 'image' && (el.src || el.url)) {
224
+ return String(el.src || el.url)
225
+ }
226
+ return ''
227
+ })
228
+
229
+ function mapRatioToSize(ratio: string, quality: string): string {
230
+ const is2K = quality.startsWith('2K')
231
+ switch (ratio) {
232
+ case '1:1':
233
+ return is2K ? '1440*1440' : (selectedImageModel.value.startsWith('qwen') ? '1328*1328' : '1024*1024')
234
+ case '16:9':
235
+ return is2K ? '1920*1080' : '1280*720'
236
+ case '9:16':
237
+ return is2K ? '1080*1920' : '720*1280'
238
+ case '4:3':
239
+ return is2K ? '1440*1080' : '1024*768'
240
+ case '3:4':
241
+ return is2K ? '1080*1440' : '768*1024'
242
+ default:
243
+ return '1024*1024'
244
+ }
245
+ }
246
+
247
+ async function fetchSupportedImageModels() {
248
+ if (!isDamLoggedIn.value) return
249
+ try {
250
+ const res = await damFetchJson('/image/generate/models')
251
+ let list: ImageModelMetaVO[] = []
252
+ if (Array.isArray(res)) list = res
253
+ else if (Array.isArray(res?.data?.data)) list = res.data.data
254
+ else if (Array.isArray(res?.data)) list = res.data
255
+
256
+ if (list.length > 0) {
257
+ imageModels.value = list
258
+ if (!list.some(m => m.model === selectedImageModel.value)) {
259
+ const def = list.find(m => m.isDefault) || list[0]
260
+ if (def) selectedImageModel.value = def.model
261
+ }
262
+ }
263
+ } catch {
264
+ // 保持内置 DEFAULT_IMAGE_MODELS
265
+ }
266
+ }
267
+
268
+ function closeAllPopovers() {
269
+ showModelDropdown.value = false
270
+ showTemplatePopover.value = false
271
+ showSeedPopover.value = false
272
+ showTagPopover.value = false
273
+ showRatioDropdown.value = false
274
+ showQualityDropdown.value = false
275
+ showVideoDurationDropdown.value = false
276
+ showVideoRatioDropdown.value = false
277
+ }
278
+
279
+ function chooseModel(modelKey: string) {
280
+ if (createMode.value === 'image') {
281
+ selectedImageModel.value = modelKey
282
+ } else {
283
+ selectedVideoModel.value = modelKey
284
+ }
285
+ showModelDropdown.value = false
286
+ }
287
+
288
+ function applyTemplatePrompt(tplPrompt: string) {
289
+ promptText.value = tplPrompt
290
+ showTemplatePopover.value = false
291
+ }
292
+
293
+ function appendQuickTag(tag: string) {
294
+ const cur = promptText.value.trim()
295
+ promptText.value = cur ? `${cur},${tag}` : tag
296
+ showTagPopover.value = false
297
+ }
298
+
299
+ function openExpandPromptModal() {
300
+ expandedPromptDraft.value = promptText.value
301
+ showExpandModal.value = true
302
+ }
303
+
304
+ function confirmExpandedPrompt() {
305
+ promptText.value = expandedPromptDraft.value
306
+ showExpandModal.value = false
307
+ }
308
+
309
+ /**
310
+ * 打开参考素材选择器
311
+ */
312
+ async function openRefPicker(targetMode: 'style' | 'character' | 'add' | 'video-start' | 'video-end') {
313
+ closeAllPopovers()
314
+ refPickerTargetMode.value = targetMode
315
+ refPickerVisible.value = true
316
+
317
+ if (recentPickerAssets.value.length === 0 && isDamLoggedIn.value) {
318
+ loadingPickerAssets.value = true
319
+ try {
320
+ const res = await damFetchJson('/asset/recent', {
321
+ params: { current: 1, size: 12, assetType: 'IMAGE' }
322
+ })
323
+ const rawList = Array.isArray(res?.data?.records)
324
+ ? res.data.records
325
+ : Array.isArray(res?.data)
326
+ ? res.data
327
+ : []
328
+ recentPickerAssets.value = rawList
329
+ .map((r: any) => {
330
+ const u = r.fileUrl || r.imageUrl || r.url || r.previewUrl || ''
331
+ if (!u) return null
332
+ return {
333
+ id: String(r.id || Math.random()),
334
+ title: r.name || '我的资产',
335
+ url: u,
336
+ previewUrl: u
337
+ } as MaterialItem
338
+ })
339
+ .filter((i: MaterialItem | null): i is MaterialItem => Boolean(i))
340
+ } catch {
341
+ recentPickerAssets.value = []
342
+ } finally {
343
+ loadingPickerAssets.value = false
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * 添加参考图(支持外部组件直接调用或内部选择器调用)
350
+ */
351
+ function addReferenceItem(item: {
352
+ id?: string
353
+ name?: string
354
+ url: string
355
+ type?: 'style' | 'character' | 'add'
356
+ assetId?: string
357
+ sourceType?: string
358
+ }) {
359
+ if (!item.url) return
360
+
361
+ // 若当前针对视频首尾帧选择
362
+ if (refPickerTargetMode.value === 'video-start' && refPickerVisible.value) {
363
+ videoStartImage.value = {
364
+ id: item.id || `vstart-${Date.now()}`,
365
+ name: item.name || '起始帧',
366
+ url: item.url,
367
+ type: 'add',
368
+ assetId: item.assetId
369
+ }
370
+ refPickerVisible.value = false
371
+ return
372
+ }
373
+ if (refPickerTargetMode.value === 'video-end' && refPickerVisible.value) {
374
+ videoEndImage.value = {
375
+ id: item.id || `vend-${Date.now()}`,
376
+ name: item.name || '结束帧',
377
+ url: item.url,
378
+ type: 'add',
379
+ assetId: item.assetId
380
+ }
381
+ refPickerVisible.value = false
382
+ return
383
+ }
384
+
385
+ if (referenceList.value.length >= 8) {
386
+ notify('参考素材最多支持添加 8 张')
387
+ return
388
+ }
389
+ if (referenceList.value.some(r => r.url === item.url)) {
390
+ notify('该参考图已在列表中')
391
+ refPickerVisible.value = false
392
+ return
393
+ }
394
+
395
+ const refType: 'style' | 'character' | 'add' =
396
+ item.type ||
397
+ (refPickerTargetMode.value === 'style' || refPickerTargetMode.value === 'character'
398
+ ? refPickerTargetMode.value
399
+ : 'add')
400
+
401
+ const idx = referenceList.value.length + 1
402
+ referenceList.value.push({
403
+ id: item.id || `ref-${Date.now()}-${idx}`,
404
+ name: item.name || (refType === 'style' ? `风格参考-${idx}` : refType === 'character' ? `角色参考-${idx}` : `参考图-${idx}`),
405
+ url: item.url,
406
+ type: refType,
407
+ assetId: item.assetId || item.id,
408
+ assetType: 'IMAGE',
409
+ sourceType: item.sourceType || (refType === 'style' ? 'REFERENCE' : 'UPLOAD')
410
+ })
411
+ refPickerVisible.value = false
412
+ }
413
+
414
+ function removeReference(id: string) {
415
+ referenceList.value = referenceList.value.filter(r => r.id !== id)
416
+ }
417
+
418
+ function insertRefTagToPrompt(idx: number) {
419
+ const tag = `@img${idx + 1} `
420
+ if (!promptText.value.includes(`@img${idx + 1}`)) {
421
+ promptText.value = (promptText.value ? promptText.value + ' ' : '') + tag
422
+ }
423
+ }
424
+
425
+ async function handleRefFileChange(e: Event) {
426
+ const input = e.target as HTMLInputElement
427
+ const file = input.files?.[0]
428
+ input.value = ''
429
+ if (!file) return
430
+
431
+ uploadingRefFile.value = true
432
+ try {
433
+ const formData = new FormData()
434
+ formData.append('file', file)
435
+ formData.append('path', '/dam/media/')
436
+ formData.append('fileName', file.name)
437
+ formData.append('id', String(Date.now()))
438
+ formData.append('assetType', 'IMAGE')
439
+
440
+ const res = await damFetchJson('/upload/file', {
441
+ method: 'POST',
442
+ body: formData
443
+ })
444
+ const body = res?.data !== undefined ? res.data : res
445
+ const inner = body?.data !== undefined ? body.data : body
446
+ const fileUrl =
447
+ inner?.ossUrl ||
448
+ inner?.fileLink ||
449
+ inner?.fileUrl ||
450
+ inner?.url ||
451
+ body?.ossUrl ||
452
+ body?.fileLink ||
453
+ body?.fileUrl ||
454
+ body?.url ||
455
+ ''
456
+ if (fileUrl) {
457
+ addReferenceItem({
458
+ name: file.name,
459
+ url: fileUrl,
460
+ sourceType: 'UPLOAD'
461
+ })
462
+ notify('参考图上传成功')
463
+ } else {
464
+ notify('上传未返回有效图片地址')
465
+ }
466
+ } catch (err: any) {
467
+ notify(`上传参考图失败: ${err.message || '网络异常'}`)
468
+ } finally {
469
+ uploadingRefFile.value = false
470
+ }
471
+ }
472
+
473
+ /**
474
+ * 执行 AI 生图或生视频
475
+ */
476
+ async function handleGenerate() {
477
+ const rawPrompt = promptText.value.trim()
478
+ if (!rawPrompt) {
479
+ notify(createMode.value === 'image' ? '请输入您想要生成的画面描述提示词' : '请输入视频画面与运镜描述提示词')
480
+ return
481
+ }
482
+
483
+ if (createMode.value === 'image') {
484
+ const meta = currentImageModelMeta.value
485
+ if (referenceList.value.length > 0 && meta && meta.supportsRefImage === false) {
486
+ notify(`当前模型【${selectedImageModel.value}】不支持参考图,请切换为 qwen-image-3.0 或移除参考素材`)
487
+ return
488
+ }
489
+
490
+ isGenerating.value = true
491
+ try {
492
+ const targetSize = mapRatioToSize(aspectRatio.value, qualitySpec.value)
493
+ const [wStr, hStr] = targetSize.split('*')
494
+ const finalPrompt = isAiPromptOn.value
495
+ ? `${rawPrompt},高细节,大师级构图,光影质感极佳`
496
+ : rawPrompt
497
+
498
+ const refImages = referenceList.value.map((r, i) => ({
499
+ assetId: r.assetId || r.id || `ref_${Date.now()}_${i}`,
500
+ url: r.url,
501
+ assetType: 'IMAGE',
502
+ sourceType: r.sourceType || 'UPLOAD'
503
+ }))
504
+
505
+ const payload: Record<string, any> = {
506
+ sessionId: currentSessionId.value,
507
+ prompt: finalPrompt,
508
+ size: targetSize,
509
+ model: selectedImageModel.value,
510
+ provider: meta?.provider || 'DASHSCOPE',
511
+ refStrength: 0.65,
512
+ ...(refImages.length > 0
513
+ ? {
514
+ refImages,
515
+ refImageUrls: refImages.map(r => r.url)
516
+ }
517
+ : {})
518
+ }
519
+
520
+ const res = await damFetchJson('/image/generate', {
521
+ method: 'POST',
522
+ body: JSON.stringify(payload)
523
+ })
524
+
525
+ const data = res?.data?.data || res?.data || res
526
+ const code = res?.code ?? res?.data?.code ?? 0
527
+ if ((code === 0 || code === 200) && data) {
528
+ if (data.sessionId) {
529
+ currentSessionId.value = data.sessionId
530
+ }
531
+ const urls: string[] = Array.isArray(data.imageUrls) && data.imageUrls.length > 0
532
+ ? data.imageUrls
533
+ : data.imageUrl
534
+ ? [data.imageUrl]
535
+ : []
536
+
537
+ if (urls.length > 0) {
538
+ urls.forEach((u, idx) => {
539
+ const newItem: MaterialItem = {
540
+ id: data.recordId ? `${data.recordId}_${idx}` : `gen-${Date.now()}-${idx}`,
541
+ title: rawPrompt,
542
+ type: 'image',
543
+ assetType: 'IMAGE',
544
+ sourceType: 'GENERATE',
545
+ badgeLabel: 'AI 生成',
546
+ url: u,
547
+ previewUrl: u,
548
+ sourceUrl: u,
549
+ originalWidth: Number(wStr) || 1024,
550
+ originalHeight: Number(hStr) || 1024,
551
+ model: data.model || selectedImageModel.value
552
+ }
553
+ generatedResults.value.unshift(newItem)
554
+ emit('generated', newItem)
555
+ })
556
+ notify('图片生成成功!点击下方卡片即可插入画布')
557
+ } else {
558
+ throw new Error(res?.msg || '未返回有效图片地址')
559
+ }
560
+ } else {
561
+ throw new Error(res?.msg || '生成图片失败')
562
+ }
563
+ } catch (err: any) {
564
+ notify(`图片生成失败: ${err.message || '请检查网络或服务配置'}`)
565
+ } finally {
566
+ isGenerating.value = false
567
+ }
568
+ } else {
569
+ // 视频生成模式
570
+ isGenerating.value = true
571
+ try {
572
+ const payload: Record<string, any> = {
573
+ prompt: videoMotionOn.value ? `${rawPrompt},电影级平滑运镜` : rawPrompt,
574
+ model: selectedVideoModel.value,
575
+ aspectRatio: videoRatio.value,
576
+ duration: videoDuration.value === '10"' ? 10 : 5,
577
+ startImageUrl: videoStartImage.value?.url || undefined,
578
+ endImageUrl: videoEndImage.value?.url || undefined
579
+ }
580
+
581
+ const res = await damFetchJson('/video/generate', {
582
+ method: 'POST',
583
+ body: JSON.stringify(payload)
584
+ })
585
+
586
+ const data = res?.data?.data || res?.data || res
587
+ const videoUrl = data?.videoUrl || data?.fileUrl || data?.url || ''
588
+ if (videoUrl) {
589
+ const newVideo: MaterialItem = {
590
+ id: data?.recordId || `vid-${Date.now()}`,
591
+ title: rawPrompt,
592
+ type: 'video',
593
+ assetType: 'VIDEO',
594
+ sourceType: 'GENERATE',
595
+ badgeLabel: 'AI 视频',
596
+ url: videoUrl,
597
+ previewUrl: videoUrl,
598
+ sourceUrl: videoUrl,
599
+ originalWidth: videoRatio.value === '9:16' ? 720 : 1280,
600
+ originalHeight: videoRatio.value === '9:16' ? 1280 : 720,
601
+ model: selectedVideoModel.value
602
+ }
603
+ generatedResults.value.unshift(newVideo)
604
+ emit('generated', newVideo)
605
+ notify('视频生成成功!点击卡片即可插入画布')
606
+ } else {
607
+ throw new Error(res?.msg || '视频生成任务已提交或暂未返回直链,请稍后在「我的素材 - AI创作记录」中查看')
608
+ }
609
+ } catch (err: any) {
610
+ notify(err.message || '视频生成服务暂未就绪')
611
+ } finally {
612
+ isGenerating.value = false
613
+ }
614
+ }
615
+ }
616
+
617
+ onMounted(() => {
618
+ fetchSupportedImageModels()
619
+ window.addEventListener('click', closeAllPopovers)
620
+ })
621
+
622
+ onUnmounted(() => {
623
+ window.removeEventListener('click', closeAllPopovers)
624
+ })
625
+
626
+ defineExpose({
627
+ addReferenceItem,
628
+ switchCreateMode: (mode: 'image' | 'video') => {
629
+ if (mode === 'video') return
630
+ createMode.value = mode
631
+ }
632
+ })
633
+ </script>
634
+
635
+ <template>
636
+ <div class="create-material-panel">
637
+ <!-- 隐藏的参考图本地上传 input -->
638
+ <input
639
+ ref="refFileInputRef"
640
+ type="file"
641
+ accept="image/*"
642
+ style="display: none"
643
+ @change="handleRefFileChange"
644
+ />
645
+
646
+ <!-- 1. 顶部工具栏:图片生成器 / 视频生成(暂禁用) 分段切换(模板库暂隐藏) -->
647
+ <div class="create-sub-header">
648
+ <div class="mode-segmented-box">
649
+ <button
650
+ type="button"
651
+ class="mode-seg-btn"
652
+ :class="{ active: createMode === 'image' }"
653
+ @click="createMode = 'image'"
654
+ >
655
+ <span>图片生成器</span>
656
+ </button>
657
+ <button
658
+ type="button"
659
+ class="mode-seg-btn is-disabled"
660
+ disabled
661
+ title="视频生成功能暂未开放"
662
+ >
663
+ <span>视频生成</span>
664
+ </button>
665
+ </div>
666
+ </div>
667
+
668
+ <!-- 2. 主滚动表单区 -->
669
+ <div class="create-scroll-body">
670
+ <!-- ==================== 模型选择 (Model Selection) ==================== -->
671
+ <div class="control-block">
672
+ <label class="block-label">模型选择</label>
673
+ <div class="model-select-wrap" @click.stop>
674
+ <button
675
+ type="button"
676
+ class="model-dropdown-trigger"
677
+ @click="showModelDropdown = !showModelDropdown"
678
+ >
679
+ <div class="model-left-info">
680
+ <span
681
+ class="model-brand-cube"
682
+ :class="{
683
+ 'is-video': createMode === 'video',
684
+ 'is-wanx': createMode === 'image' && isWanxBrand
685
+ }"
686
+ >
687
+ <svg v-if="createMode === 'video'" viewBox="0 0 24 24" width="13" height="13" fill="currentColor">
688
+ <polygon points="6 4 20 12 6 20 6 4" />
689
+ </svg>
690
+ <svg v-else viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
691
+ <path d="M12 2L3 7v10l9 5 9-5V7l-9-5z" />
692
+ </svg>
693
+ </span>
694
+ <span class="model-name-text">
695
+ {{ createMode === 'image' ? selectedImageModel : selectedVideoModel }}
696
+ </span>
697
+ </div>
698
+ <svg class="chevron-icon" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2">
699
+ <polyline points="6 9 12 15 18 9" />
700
+ </svg>
701
+ </button>
702
+
703
+ <!-- 模型下拉菜单 -->
704
+ <transition name="pop-fade">
705
+ <div v-if="showModelDropdown" class="model-dropdown-menu">
706
+ <template v-if="createMode === 'image'">
707
+ <div
708
+ v-for="m in imageModels"
709
+ :key="m.model"
710
+ class="model-option-item"
711
+ :class="{ active: m.model === selectedImageModel }"
712
+ @click="chooseModel(m.model)"
713
+ >
714
+ <div class="opt-top-line">
715
+ <span class="opt-code">{{ m.model }}</span>
716
+ <span v-if="m.supportsRefImage" class="opt-tag">支持参考图</span>
717
+ </div>
718
+ <div v-if="m.displayName || m.description" class="opt-sub-desc">
719
+ {{ m.displayName || m.description }}
720
+ </div>
721
+ </div>
722
+ </template>
723
+ <template v-else>
724
+ <div
725
+ v-for="vm in DEFAULT_VIDEO_MODELS"
726
+ :key="vm.model"
727
+ class="model-option-item"
728
+ :class="{ active: vm.model === selectedVideoModel }"
729
+ @click="chooseModel(vm.model)"
730
+ >
731
+ <div class="opt-top-line">
732
+ <span class="opt-code">{{ vm.model }}</span>
733
+ <span class="opt-tag video">{{ vm.vendor }}</span>
734
+ </div>
735
+ <div class="opt-sub-desc">{{ vm.displayName }}</div>
736
+ </div>
737
+ </template>
738
+ </div>
739
+ </transition>
740
+ </div>
741
+ </div>
742
+
743
+ <!-- ==================== 参考素材 (References) ==================== -->
744
+ <!-- A. 图片生成模式:1:1 还原图二(风格参考 / 角色参考 / 添加素材 0/8) -->
745
+ <div v-if="createMode === 'image'" class="control-block">
746
+ <div class="block-header-line">
747
+ <label class="block-label">参考素材</label>
748
+ <span class="ref-counter">{{ referenceList.length }}/8</span>
749
+ </div>
750
+
751
+ <div class="ref-tiles-grid">
752
+ <button type="button" class="ref-action-tile" @click.stop="openRefPicker('style')">
753
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
754
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
755
+ </svg>
756
+ <span class="tile-label">风格参考</span>
757
+ </button>
758
+
759
+ <button type="button" class="ref-action-tile" @click.stop="openRefPicker('character')">
760
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
761
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
762
+ <circle cx="12" cy="7" r="4" />
763
+ </svg>
764
+ <span class="tile-label">角色参考</span>
765
+ </button>
766
+
767
+ <!-- 已添加的参考图卡片 (点击可插入 @imgN 到提示词) -->
768
+ <div
769
+ v-for="(refItem, idx) in referenceList"
770
+ :key="refItem.id"
771
+ class="ref-added-thumb"
772
+ :title="`点击在提示词中引用 @img${idx + 1}`"
773
+ @click="insertRefTagToPrompt(idx)"
774
+ >
775
+ <img :src="refItem.url" :alt="refItem.name" class="thumb-img" />
776
+ <span class="thumb-tag">
777
+ <template v-if="refItem.type === 'style'">★</template>
778
+ @img{{ idx + 1 }}
779
+ </span>
780
+ <button
781
+ type="button"
782
+ class="thumb-remove-btn"
783
+ title="移除参考图"
784
+ @click.stop="removeReference(refItem.id)"
785
+ >
786
+ ×
787
+ </button>
788
+ </div>
789
+
790
+ <button
791
+ v-if="referenceList.length < 8"
792
+ type="button"
793
+ class="ref-action-tile"
794
+ @click.stop="openRefPicker('add')"
795
+ >
796
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
797
+ <line x1="12" y1="5" x2="12" y2="19" />
798
+ <line x1="5" y1="12" x2="19" y2="12" />
799
+ </svg>
800
+ <span class="tile-label">添加素材</span>
801
+ </button>
802
+ </div>
803
+ </div>
804
+
805
+ <!-- B. 视频生成模式:起始帧 / 结束帧 -->
806
+ <div v-else class="control-block">
807
+ <div class="block-header-line">
808
+ <label class="block-label">参考素材 (首尾帧)</label>
809
+ </div>
810
+ <div class="video-frames-grid">
811
+ <div
812
+ class="video-frame-slot"
813
+ @click.stop="openRefPicker('video-start')"
814
+ >
815
+ <template v-if="videoStartImage">
816
+ <img :src="videoStartImage.url" class="slot-preview-img" />
817
+ <span class="slot-tag">起始帧</span>
818
+ <button type="button" class="slot-clear-btn" @click.stop="videoStartImage = null">×</button>
819
+ </template>
820
+ <template v-else>
821
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8">
822
+ <rect x="3" y="3" width="18" height="18" rx="2" />
823
+ <circle cx="8.5" cy="8.5" r="1.5" />
824
+ <polyline points="21 15 16 10 5 21" />
825
+ </svg>
826
+ <span>起始帧 (可选)</span>
827
+ </template>
828
+ </div>
829
+
830
+ <div
831
+ class="video-frame-slot"
832
+ @click.stop="openRefPicker('video-end')"
833
+ >
834
+ <template v-if="videoEndImage">
835
+ <img :src="videoEndImage.url" class="slot-preview-img" />
836
+ <span class="slot-tag">结束帧</span>
837
+ <button type="button" class="slot-clear-btn" @click.stop="videoEndImage = null">×</button>
838
+ </template>
839
+ <template v-else>
840
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8">
841
+ <rect x="3" y="3" width="18" height="18" rx="2" />
842
+ <circle cx="8.5" cy="8.5" r="1.5" />
843
+ <polyline points="21 15 16 10 5 21" />
844
+ </svg>
845
+ <span>结束帧 (可选)</span>
846
+ </template>
847
+ </div>
848
+ </div>
849
+ </div>
850
+
851
+ <!-- 参考素材快速选择面板 (支持画布当前选中图 / 本地上传 / 预设风格 / 我的资产) -->
852
+ <transition name="pop-fade">
853
+ <div v-if="refPickerVisible" class="ref-picker-drawer-card" @click.stop>
854
+ <div class="picker-top-bar">
855
+ <span class="picker-title">
856
+ {{ refPickerTargetMode === 'style' ? '选择风格参考' : refPickerTargetMode === 'character' ? '选择角色参考' : '添加参考素材' }}
857
+ </span>
858
+ <button type="button" class="picker-close" @click="refPickerVisible = false">×</button>
859
+ </div>
860
+
861
+ <div class="picker-quick-btns">
862
+ <button
863
+ type="button"
864
+ class="picker-action-btn primary"
865
+ :disabled="uploadingRefFile"
866
+ @click="refFileInputRef?.click()"
867
+ >
868
+ <span>{{ uploadingRefFile ? '上传中...' : '+ 本地上传图片' }}</span>
869
+ </button>
870
+
871
+ <button
872
+ v-if="canvasSelectedImageUrl"
873
+ type="button"
874
+ class="picker-action-btn canvas-pick"
875
+ @click="addReferenceItem({ name: '画布选中图', url: canvasSelectedImageUrl, sourceType: 'REFERENCE' })"
876
+ >
877
+ <span>拾取画布选中图</span>
878
+ </button>
879
+ </div>
880
+
881
+ <!-- 预设风格列表 -->
882
+ <template v-if="refPickerTargetMode === 'style'">
883
+ <div class="picker-sub-label">预设视觉风格</div>
884
+ <div class="picker-thumbs-row">
885
+ <div
886
+ v-for="st in PRESET_STYLE_REFS"
887
+ :key="st.id"
888
+ class="picker-mini-card"
889
+ @click="addReferenceItem({ id: st.id, name: st.name, url: st.url, type: 'style', sourceType: 'REFERENCE' })"
890
+ >
891
+ <img :src="st.url" :alt="st.name" />
892
+ <span>{{ st.name }}</span>
893
+ </div>
894
+ </div>
895
+ </template>
896
+
897
+ <!-- 我的最近图片资产 -->
898
+ <div class="picker-sub-label">从我的资产选择</div>
899
+ <div v-if="loadingPickerAssets" class="picker-empty-tip">正在加载最近资产...</div>
900
+ <div v-else-if="recentPickerAssets.length > 0" class="picker-thumbs-row">
901
+ <div
902
+ v-for="asset in recentPickerAssets"
903
+ :key="asset.id"
904
+ class="picker-mini-card"
905
+ @click="addReferenceItem({ id: String(asset.id), name: asset.title, url: asset.url || '', assetId: String(asset.id) })"
906
+ >
907
+ <img :src="asset.previewUrl || asset.url" :alt="asset.title" />
908
+ <span>{{ asset.title }}</span>
909
+ </div>
910
+ </div>
911
+ <div v-else class="picker-empty-tip">暂无历史图片,请点击上方「本地上传图片」</div>
912
+ </div>
913
+ </transition>
914
+
915
+ <!-- ==================== 提示词描述 (Prompt Box - 1:1 还原图二) ==================== -->
916
+ <div class="control-block">
917
+ <label class="block-label">提示词描述</label>
918
+ <div class="prompt-card-box">
919
+ <textarea
920
+ v-model="promptText"
921
+ rows="4"
922
+ class="prompt-textarea"
923
+ :placeholder="createMode === 'image' ? '描述您想要生成的画面—可输入 @ 引入参考图' : '描述您想要生成的视频画面与镜头运动...'"
924
+ ></textarea>
925
+
926
+ <div class="prompt-bottom-toolbar">
927
+ <!-- 左侧 AI 提示词增强开关 -->
928
+ <div
929
+ class="ai-enhance-switch"
930
+ @click="createMode === 'image' ? (isAiPromptOn = !isAiPromptOn) : (videoMotionOn = !videoMotionOn)"
931
+ >
932
+ <span
933
+ class="switch-track"
934
+ :class="{ 'is-on': createMode === 'image' ? isAiPromptOn : videoMotionOn }"
935
+ >
936
+ <span class="switch-thumb"></span>
937
+ </span>
938
+ <span class="switch-label">AI 提示词增强</span>
939
+ </div>
940
+
941
+ <!-- 右侧 4 个辅助图标 (Seed / Tag / Expand / Clear) -->
942
+ <div class="prompt-right-icons" @click.stop>
943
+ <!-- 1. 固定种子 -->
944
+ <div class="icon-pop-anchor">
945
+ <button
946
+ type="button"
947
+ class="tool-icon-btn"
948
+ :class="{ active: isFixedSeedOn || showSeedPopover }"
949
+ title="固定随机种子"
950
+ @click="showSeedPopover = !showSeedPopover; showTagPopover = false"
951
+ >
952
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
953
+ <path d="M12 22v-8" />
954
+ <path d="M12 14c-4 0-7-3-7-7 0 0 3 0 7 3" />
955
+ <path d="M12 14c4 0 7-3 7-7 0 0-3 0-7 3" />
956
+ </svg>
957
+ </button>
958
+ <div v-if="showSeedPopover" class="mini-floating-popover">
959
+ <div class="pop-row-between">
960
+ <span>固定随机种子 (Seed)</span>
961
+ <input v-model="isFixedSeedOn" type="checkbox" />
962
+ </div>
963
+ <p class="pop-muted-tip">开启后保持多次生成构图一致</p>
964
+ </div>
965
+ </div>
966
+
967
+ <!-- 2. 风格标签词 -->
968
+ <div class="icon-pop-anchor">
969
+ <button
970
+ type="button"
971
+ class="tool-icon-btn"
972
+ :class="{ active: showTagPopover }"
973
+ title="插入常用修饰词"
974
+ @click="showTagPopover = !showTagPopover; showSeedPopover = false"
975
+ >
976
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
977
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
978
+ <line x1="7" y1="7" x2="7.01" y2="7" />
979
+ </svg>
980
+ </button>
981
+ <div v-if="showTagPopover" class="mini-floating-popover tags-pop">
982
+ <div class="pop-card-title">点击追加风格词</div>
983
+ <div class="quick-tags-wrap">
984
+ <span
985
+ v-for="t in QUICK_STYLE_TAGS"
986
+ :key="t"
987
+ class="quick-tag-chip"
988
+ @click="appendQuickTag(t)"
989
+ >
990
+ + {{ t }}
991
+ </span>
992
+ </div>
993
+ </div>
994
+ </div>
995
+
996
+ <!-- 3. 展开全屏编辑 -->
997
+ <button
998
+ type="button"
999
+ class="tool-icon-btn"
1000
+ title="展开编辑提示词"
1001
+ @click="openExpandPromptModal"
1002
+ >
1003
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
1004
+ <polyline points="15 3 21 3 21 9" />
1005
+ <polyline points="9 21 3 21 3 15" />
1006
+ <line x1="21" y1="3" x2="14" y2="10" />
1007
+ <line x1="3" y1="21" x2="10" y2="14" />
1008
+ </svg>
1009
+ </button>
1010
+
1011
+ <!-- 4. 清空提示词 -->
1012
+ <button
1013
+ type="button"
1014
+ class="tool-icon-btn"
1015
+ title="清空提示词"
1016
+ @click="promptText = ''"
1017
+ >
1018
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
1019
+ <line x1="18" y1="6" x2="6" y2="18" />
1020
+ <line x1="6" y1="6" x2="18" y2="18" />
1021
+ </svg>
1022
+ </button>
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+ </div>
1027
+
1028
+ <!-- ==================== 参数控制胶囊行 (- 1 +, 1:1, 1K · High) ==================== -->
1029
+ <div v-if="createMode === 'image'" class="params-pills-row" @click.stop>
1030
+ <!-- 数量步进器 -->
1031
+ <div class="stepper-pill">
1032
+ <button type="button" class="step-btn" @click="batchCount = Math.max(1, batchCount - 1)">-</button>
1033
+ <span class="step-num">{{ batchCount }}</span>
1034
+ <button type="button" class="step-btn" @click="batchCount = Math.min(4, batchCount + 1)">+</button>
1035
+ </div>
1036
+
1037
+ <!-- 画幅比例选择胶囊 -->
1038
+ <div class="param-pill-anchor">
1039
+ <button
1040
+ type="button"
1041
+ class="param-select-pill"
1042
+ @click="showRatioDropdown = !showRatioDropdown; showQualityDropdown = false"
1043
+ >
1044
+ <span class="ratio-rect-icon"></span>
1045
+ <span>{{ aspectRatio }}</span>
1046
+ </button>
1047
+ <div v-if="showRatioDropdown" class="pill-dropdown-menu">
1048
+ <div
1049
+ v-for="r in (['1:1', '16:9', '9:16', '4:3', '3:4'] as const)"
1050
+ :key="r"
1051
+ class="pill-menu-item"
1052
+ :class="{ active: aspectRatio === r }"
1053
+ @click="aspectRatio = r; showRatioDropdown = false"
1054
+ >
1055
+ <span>{{ r }}</span>
1056
+ <span class="muted-sub">{{ mapRatioToSize(r, qualitySpec) }}</span>
1057
+ </div>
1058
+ </div>
1059
+ </div>
1060
+
1061
+ <!-- 清晰度规格胶囊 -->
1062
+ <div class="param-pill-anchor">
1063
+ <button
1064
+ type="button"
1065
+ class="param-select-pill"
1066
+ @click="showQualityDropdown = !showQualityDropdown; showRatioDropdown = false"
1067
+ >
1068
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
1069
+ <rect x="3" y="4" width="18" height="16" rx="2" />
1070
+ <path d="M16 2v4M8 2v4" />
1071
+ </svg>
1072
+ <span>{{ qualitySpec }}</span>
1073
+ </button>
1074
+ <div v-if="showQualityDropdown" class="pill-dropdown-menu right">
1075
+ <div
1076
+ v-for="q in (['1K · High', '1K · Fast', '2K · HD'] as const)"
1077
+ :key="q"
1078
+ class="pill-menu-item"
1079
+ :class="{ active: qualitySpec === q }"
1080
+ @click="qualitySpec = q; showQualityDropdown = false"
1081
+ >
1082
+ <span>{{ q }}</span>
1083
+ </div>
1084
+ </div>
1085
+ </div>
1086
+ </div>
1087
+
1088
+ <!-- 视频模式参数胶囊行 -->
1089
+ <div v-else class="params-pills-row" @click.stop>
1090
+ <div class="param-pill-anchor">
1091
+ <button
1092
+ type="button"
1093
+ class="param-select-pill"
1094
+ @click="showVideoDurationDropdown = !showVideoDurationDropdown; showVideoRatioDropdown = false"
1095
+ >
1096
+ <span>⏱</span>
1097
+ <span>{{ videoDuration }}</span>
1098
+ </button>
1099
+ <div v-if="showVideoDurationDropdown" class="pill-dropdown-menu">
1100
+ <div
1101
+ v-for="d in (['5-6&quot;', '10&quot;'] as const)"
1102
+ :key="d"
1103
+ class="pill-menu-item"
1104
+ :class="{ active: videoDuration === d }"
1105
+ @click="videoDuration = d; showVideoDurationDropdown = false"
1106
+ >
1107
+ <span>时长 {{ d }}</span>
1108
+ </div>
1109
+ </div>
1110
+ </div>
1111
+
1112
+ <div class="param-pill-anchor">
1113
+ <button
1114
+ type="button"
1115
+ class="param-select-pill"
1116
+ @click="showVideoRatioDropdown = !showVideoRatioDropdown; showVideoDurationDropdown = false"
1117
+ >
1118
+ <span class="ratio-rect-icon wide"></span>
1119
+ <span>{{ videoRatio }}</span>
1120
+ </button>
1121
+ <div v-if="showVideoRatioDropdown" class="pill-dropdown-menu">
1122
+ <div
1123
+ v-for="vr in (['16:9', '9:16', '1:1'] as const)"
1124
+ :key="vr"
1125
+ class="pill-menu-item"
1126
+ :class="{ active: videoRatio === vr }"
1127
+ @click="videoRatio = vr; showVideoRatioDropdown = false"
1128
+ >
1129
+ <span>{{ vr }}</span>
1130
+ </div>
1131
+ </div>
1132
+ </div>
1133
+
1134
+ <button
1135
+ type="button"
1136
+ class="param-select-pill"
1137
+ :class="{ 'is-active-blue': videoMotionOn }"
1138
+ @click="videoMotionOn = !videoMotionOn"
1139
+ >
1140
+ <span>⚡</span>
1141
+ <span>{{ videoMotionOn ? '运镜 ON' : '运镜 OFF' }}</span>
1142
+ </button>
1143
+ </div>
1144
+
1145
+ <!-- ==================== 开始生成主按钮 (1:1 还原图二蓝紫渐变按钮) ==================== -->
1146
+ <div class="generate-cta-wrap">
1147
+ <button
1148
+ type="button"
1149
+ class="start-generate-btn"
1150
+ :disabled="isGenerating"
1151
+ @click="handleGenerate"
1152
+ >
1153
+ <span class="cta-text">
1154
+ {{ isGenerating ? '正在生成中...' : '开始生成' }}
1155
+ </span>
1156
+ <span v-if="isGenerating" class="cta-spinner"></span>
1157
+ <svg v-else class="sparkle-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
1158
+ <path d="M19 2.5C19 4.7 20.3 6.5 22.5 6.5C20.3 6.5 19 8.3 19 10.5C19 8.3 17.7 6.5 15.5 6.5C17.7 6.5 19 4.7 19 2.5Z" />
1159
+ <path d="M11.5 12C11.5 13.3 12.7 14.5 14 14.5C12.7 14.5 11.5 15.7 11.5 17C11.5 15.7 10.3 14.5 9 14.5C10.3 14.5 11.5 13.3 11.5 12Z" />
1160
+ </svg>
1161
+ </button>
1162
+ </div>
1163
+
1164
+ <!-- ==================== 本次生成结果内嵌预览列表 (一键插入画布) ==================== -->
1165
+ <div v-if="generatedResults.length > 0" class="generated-results-section">
1166
+ <div class="results-header">
1167
+ <span class="results-title">本次生成结果 ({{ generatedResults.length }})</span>
1168
+ <span class="results-sub">已同步至「我的素材」</span>
1169
+ </div>
1170
+
1171
+ <div class="results-grid">
1172
+ <div
1173
+ v-for="resItem in generatedResults"
1174
+ :key="resItem.id"
1175
+ class="result-card"
1176
+ @click="emit('select', resItem)"
1177
+ >
1178
+ <video
1179
+ v-if="resItem.type === 'video'"
1180
+ :src="resItem.url"
1181
+ muted
1182
+ playsinline
1183
+ class="result-media"
1184
+ @mouseenter="($event.target as HTMLVideoElement)?.play?.()"
1185
+ @mouseleave="($event.target as HTMLVideoElement)?.pause?.()"
1186
+ />
1187
+ <img
1188
+ v-else
1189
+ :src="resItem.url"
1190
+ :alt="resItem.title"
1191
+ class="result-media"
1192
+ />
1193
+
1194
+ <span v-if="resItem.type === 'video'" class="video-badge">
1195
+ <el-icon><VideoPlay /></el-icon>
1196
+ </span>
1197
+
1198
+ <div class="result-overlay">
1199
+ <button
1200
+ type="button"
1201
+ class="res-act-btn insert"
1202
+ @click.stop="emit('select', resItem)"
1203
+ >
1204
+ <el-icon><Plus /></el-icon>
1205
+ <span>{{ hasActivePlaceholderTarget ? '插入占位符' : '插入画布' }}</span>
1206
+ </button>
1207
+ <button
1208
+ type="button"
1209
+ class="res-act-btn preview"
1210
+ title="大图预览"
1211
+ @click.stop="emit('preview', resItem, generatedResults, $event)"
1212
+ >
1213
+ <el-icon><View /></el-icon>
1214
+ </button>
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ </div>
1219
+ </div>
1220
+
1221
+ <!-- 提示词展开编辑弹窗 -->
1222
+ <el-dialog
1223
+ v-model="showExpandModal"
1224
+ title="编辑提示词描述"
1225
+ width="460px"
1226
+ append-to-body
1227
+ align-center
1228
+ >
1229
+ <el-input
1230
+ v-model="expandedPromptDraft"
1231
+ type="textarea"
1232
+ :rows="6"
1233
+ placeholder="详细描述画面主体、光影氛围、构图风格与细节..."
1234
+ />
1235
+ <template #footer>
1236
+ <el-button size="small" @click="showExpandModal = false">取消</el-button>
1237
+ <el-button type="primary" size="small" @click="confirmExpandedPrompt">应用提示词</el-button>
1238
+ </template>
1239
+ </el-dialog>
1240
+ </div>
1241
+ </template>
1242
+
1243
+ <style scoped lang="scss">
1244
+ .create-material-panel {
1245
+ flex: 1;
1246
+ min-height: 0;
1247
+ display: flex;
1248
+ flex-direction: column;
1249
+ overflow: hidden;
1250
+ background: #ffffff;
1251
+ }
1252
+
1253
+ .create-sub-header {
1254
+ display: flex;
1255
+ align-items: center;
1256
+ justify-content: space-between;
1257
+ padding: 10px 14px 8px;
1258
+ border-bottom: 1px solid #f1f5f9;
1259
+ flex-shrink: 0;
1260
+ }
1261
+
1262
+ .mode-segmented-box {
1263
+ display: inline-flex;
1264
+ background: #f1f5f9;
1265
+ padding: 2px;
1266
+ border-radius: 7px;
1267
+ }
1268
+
1269
+ .mode-seg-btn {
1270
+ border: none;
1271
+ background: transparent;
1272
+ padding: 4px 10px;
1273
+ border-radius: 5px;
1274
+ font-size: 12px;
1275
+ font-weight: 500;
1276
+ color: #64748b;
1277
+ cursor: pointer;
1278
+ transition: all 0.15s ease;
1279
+
1280
+ &:hover:not(.is-disabled) {
1281
+ color: #0f172a;
1282
+ }
1283
+
1284
+ &.active {
1285
+ background: #ffffff;
1286
+ color: #0f172a;
1287
+ font-weight: 600;
1288
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
1289
+ }
1290
+
1291
+ &.is-disabled,
1292
+ &:disabled {
1293
+ color: #94a3b8;
1294
+ cursor: not-allowed;
1295
+ opacity: 0.65;
1296
+ }
1297
+ }
1298
+
1299
+ .template-btn-wrap {
1300
+ position: relative;
1301
+ }
1302
+
1303
+ .templates-pill-btn {
1304
+ display: inline-flex;
1305
+ align-items: center;
1306
+ gap: 4px;
1307
+ height: 26px;
1308
+ padding: 0 10px;
1309
+ border-radius: 13px;
1310
+ border: 1px solid #e2e8f0;
1311
+ background: #ffffff;
1312
+ color: #475569;
1313
+ font-size: 11.5px;
1314
+ cursor: pointer;
1315
+ transition: all 0.15s ease;
1316
+
1317
+ &:hover {
1318
+ border-color: #93c5fd;
1319
+ color: #1e6fff;
1320
+ }
1321
+ }
1322
+
1323
+ .templates-popover-card {
1324
+ position: absolute;
1325
+ top: 30px;
1326
+ right: 0;
1327
+ width: 248px;
1328
+ background: #ffffff;
1329
+ border: 1px solid #e2e8f0;
1330
+ border-radius: 10px;
1331
+ box-shadow: 0 10px 28px rgba(15, 23, 42, 0.14);
1332
+ padding: 8px;
1333
+ z-index: 45;
1334
+ }
1335
+
1336
+ .pop-card-title {
1337
+ font-size: 11px;
1338
+ font-weight: 600;
1339
+ color: #94a3b8;
1340
+ padding: 2px 4px 6px;
1341
+ }
1342
+
1343
+ .tpl-item-row {
1344
+ padding: 6px 8px;
1345
+ border-radius: 6px;
1346
+ cursor: pointer;
1347
+ transition: background 0.12s ease;
1348
+
1349
+ &:hover {
1350
+ background: #eff6ff;
1351
+ }
1352
+
1353
+ .tpl-name {
1354
+ font-size: 12px;
1355
+ font-weight: 600;
1356
+ color: #1e293b;
1357
+ }
1358
+
1359
+ .tpl-desc {
1360
+ font-size: 11px;
1361
+ color: #64748b;
1362
+ margin-top: 2px;
1363
+ display: -webkit-box;
1364
+ -webkit-line-clamp: 2;
1365
+ -webkit-box-orient: vertical;
1366
+ overflow: hidden;
1367
+ }
1368
+ }
1369
+
1370
+ .create-scroll-body {
1371
+ flex: 1;
1372
+ min-height: 0;
1373
+ overflow-y: auto;
1374
+ padding: 12px 14px 20px;
1375
+ display: flex;
1376
+ flex-direction: column;
1377
+ gap: 14px;
1378
+
1379
+ &::-webkit-scrollbar {
1380
+ width: 4px;
1381
+ }
1382
+ &::-webkit-scrollbar-thumb {
1383
+ background: #cbd5e1;
1384
+ border-radius: 2px;
1385
+ }
1386
+ }
1387
+
1388
+ .control-block {
1389
+ display: flex;
1390
+ flex-direction: column;
1391
+ gap: 6px;
1392
+ }
1393
+
1394
+ .block-header-line {
1395
+ display: flex;
1396
+ align-items: center;
1397
+ justify-content: space-between;
1398
+ }
1399
+
1400
+ .block-label {
1401
+ font-size: 12px;
1402
+ font-weight: 500;
1403
+ color: #94a3b8;
1404
+ }
1405
+
1406
+ .ref-counter {
1407
+ font-size: 12px;
1408
+ color: #64748b;
1409
+ }
1410
+
1411
+ .model-select-wrap {
1412
+ position: relative;
1413
+ }
1414
+
1415
+ .model-dropdown-trigger {
1416
+ width: 100%;
1417
+ height: 38px;
1418
+ padding: 0 12px;
1419
+ border-radius: 8px;
1420
+ border: 1px solid #e2e8f0;
1421
+ background: #f8fafc;
1422
+ display: flex;
1423
+ align-items: center;
1424
+ justify-content: space-between;
1425
+ cursor: pointer;
1426
+ transition: all 0.15s ease;
1427
+
1428
+ &:hover {
1429
+ border-color: #cbd5e1;
1430
+ background: #f1f5f9;
1431
+ }
1432
+
1433
+ .model-left-info {
1434
+ display: flex;
1435
+ align-items: center;
1436
+ gap: 8px;
1437
+ min-width: 0;
1438
+ }
1439
+
1440
+ .model-brand-cube {
1441
+ width: 20px;
1442
+ height: 20px;
1443
+ border-radius: 5px;
1444
+ background: #eff6ff;
1445
+ color: #2563eb;
1446
+ display: inline-flex;
1447
+ align-items: center;
1448
+ justify-content: center;
1449
+ flex-shrink: 0;
1450
+
1451
+ &.is-wanx {
1452
+ background: #f5f3ff;
1453
+ color: #7c3aed;
1454
+ }
1455
+
1456
+ &.is-video {
1457
+ background: #ecfdf5;
1458
+ color: #059669;
1459
+ }
1460
+ }
1461
+
1462
+ .model-name-text {
1463
+ font-size: 13px;
1464
+ font-weight: 500;
1465
+ color: #0f172a;
1466
+ overflow: hidden;
1467
+ text-overflow: ellipsis;
1468
+ white-space: nowrap;
1469
+ }
1470
+
1471
+ .chevron-icon {
1472
+ color: #94a3b8;
1473
+ flex-shrink: 0;
1474
+ }
1475
+ }
1476
+
1477
+ .model-dropdown-menu {
1478
+ position: absolute;
1479
+ top: 42px;
1480
+ left: 0;
1481
+ right: 0;
1482
+ background: #ffffff;
1483
+ border: 1px solid #e2e8f0;
1484
+ border-radius: 10px;
1485
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.14);
1486
+ padding: 6px;
1487
+ z-index: 40;
1488
+ max-height: 240px;
1489
+ overflow-y: auto;
1490
+ }
1491
+
1492
+ .model-option-item {
1493
+ padding: 7px 8px;
1494
+ border-radius: 6px;
1495
+ cursor: pointer;
1496
+ transition: background 0.12s ease;
1497
+
1498
+ &:hover {
1499
+ background: #f8fafc;
1500
+ }
1501
+
1502
+ &.active {
1503
+ background: #eff6ff;
1504
+
1505
+ .opt-code {
1506
+ color: #1e6fff;
1507
+ font-weight: 600;
1508
+ }
1509
+ }
1510
+
1511
+ .opt-top-line {
1512
+ display: flex;
1513
+ align-items: center;
1514
+ justify-content: space-between;
1515
+ gap: 6px;
1516
+ }
1517
+
1518
+ .opt-code {
1519
+ font-size: 12.5px;
1520
+ font-weight: 500;
1521
+ color: #1e293b;
1522
+ }
1523
+
1524
+ .opt-tag {
1525
+ font-size: 10px;
1526
+ padding: 1px 5px;
1527
+ border-radius: 4px;
1528
+ background: #eff6ff;
1529
+ color: #2563eb;
1530
+
1531
+ &.video {
1532
+ background: #ecfdf5;
1533
+ color: #059669;
1534
+ }
1535
+ }
1536
+
1537
+ .opt-sub-desc {
1538
+ font-size: 11px;
1539
+ color: #64748b;
1540
+ margin-top: 2px;
1541
+ overflow: hidden;
1542
+ text-overflow: ellipsis;
1543
+ white-space: nowrap;
1544
+ }
1545
+ }
1546
+
1547
+ /* 参考素材方块网格 (1:1 还原图二) */
1548
+ .ref-tiles-grid {
1549
+ display: grid;
1550
+ grid-template-columns: repeat(3, 1fr);
1551
+ gap: 8px;
1552
+ }
1553
+
1554
+ .ref-action-tile {
1555
+ height: 62px;
1556
+ border-radius: 8px;
1557
+ border: 1px solid #e2e8f0;
1558
+ background: #f8fafc;
1559
+ color: #475569;
1560
+ display: flex;
1561
+ flex-direction: column;
1562
+ align-items: center;
1563
+ justify-content: center;
1564
+ gap: 5px;
1565
+ cursor: pointer;
1566
+ transition: all 0.15s ease;
1567
+
1568
+ &:hover {
1569
+ border-color: #93c5fd;
1570
+ background: #eff6ff;
1571
+ color: #1e6fff;
1572
+ }
1573
+
1574
+ .tile-label {
1575
+ font-size: 11.5px;
1576
+ font-weight: 500;
1577
+ }
1578
+ }
1579
+
1580
+ .ref-added-thumb {
1581
+ position: relative;
1582
+ height: 62px;
1583
+ border-radius: 8px;
1584
+ overflow: hidden;
1585
+ border: 1px solid #cbd5e1;
1586
+ cursor: pointer;
1587
+
1588
+ .thumb-img {
1589
+ width: 100%;
1590
+ height: 100%;
1591
+ object-fit: cover;
1592
+ display: block;
1593
+ }
1594
+
1595
+ .thumb-tag {
1596
+ position: absolute;
1597
+ left: 4px;
1598
+ bottom: 3px;
1599
+ padding: 0 4px;
1600
+ border-radius: 3px;
1601
+ background: rgba(15, 23, 42, 0.72);
1602
+ color: #ffffff;
1603
+ font-size: 10px;
1604
+ }
1605
+
1606
+ .thumb-remove-btn {
1607
+ position: absolute;
1608
+ top: 3px;
1609
+ right: 3px;
1610
+ width: 16px;
1611
+ height: 16px;
1612
+ border-radius: 50%;
1613
+ border: none;
1614
+ background: rgba(15, 23, 42, 0.7);
1615
+ color: #ffffff;
1616
+ font-size: 12px;
1617
+ line-height: 1;
1618
+ cursor: pointer;
1619
+ display: flex;
1620
+ align-items: center;
1621
+ justify-content: center;
1622
+ }
1623
+ }
1624
+
1625
+ .video-frames-grid {
1626
+ display: grid;
1627
+ grid-template-columns: 1fr 1fr;
1628
+ gap: 8px;
1629
+ }
1630
+
1631
+ .video-frame-slot {
1632
+ position: relative;
1633
+ height: 62px;
1634
+ border-radius: 8px;
1635
+ border: 1px dashed #cbd5e1;
1636
+ background: #f8fafc;
1637
+ color: #64748b;
1638
+ font-size: 12px;
1639
+ display: flex;
1640
+ align-items: center;
1641
+ justify-content: center;
1642
+ gap: 6px;
1643
+ cursor: pointer;
1644
+ overflow: hidden;
1645
+
1646
+ &:hover {
1647
+ border-color: #3b82f6;
1648
+ color: #1e6fff;
1649
+ }
1650
+
1651
+ .slot-preview-img {
1652
+ width: 100%;
1653
+ height: 100%;
1654
+ object-fit: cover;
1655
+ }
1656
+
1657
+ .slot-tag {
1658
+ position: absolute;
1659
+ left: 4px;
1660
+ bottom: 4px;
1661
+ background: rgba(15, 23, 42, 0.7);
1662
+ color: #ffffff;
1663
+ font-size: 10px;
1664
+ padding: 1px 5px;
1665
+ border-radius: 3px;
1666
+ }
1667
+
1668
+ .slot-clear-btn {
1669
+ position: absolute;
1670
+ top: 4px;
1671
+ right: 4px;
1672
+ width: 18px;
1673
+ height: 18px;
1674
+ border-radius: 50%;
1675
+ border: none;
1676
+ background: rgba(15, 23, 42, 0.7);
1677
+ color: #ffffff;
1678
+ cursor: pointer;
1679
+ }
1680
+ }
1681
+
1682
+ /* 参考图选择浮层卡片 */
1683
+ .ref-picker-drawer-card {
1684
+ background: #f8fafc;
1685
+ border: 1px solid #cbd5e1;
1686
+ border-radius: 10px;
1687
+ padding: 10px;
1688
+ display: flex;
1689
+ flex-direction: column;
1690
+ gap: 8px;
1691
+ }
1692
+
1693
+ .picker-top-bar {
1694
+ display: flex;
1695
+ align-items: center;
1696
+ justify-content: space-between;
1697
+
1698
+ .picker-title {
1699
+ font-size: 12px;
1700
+ font-weight: 600;
1701
+ color: #1e293b;
1702
+ }
1703
+
1704
+ .picker-close {
1705
+ border: none;
1706
+ background: transparent;
1707
+ font-size: 16px;
1708
+ color: #64748b;
1709
+ cursor: pointer;
1710
+ }
1711
+ }
1712
+
1713
+ .picker-quick-btns {
1714
+ display: flex;
1715
+ gap: 6px;
1716
+ }
1717
+
1718
+ .picker-action-btn {
1719
+ flex: 1;
1720
+ height: 28px;
1721
+ border-radius: 6px;
1722
+ border: 1px solid #cbd5e1;
1723
+ background: #ffffff;
1724
+ color: #334155;
1725
+ font-size: 11.5px;
1726
+ font-weight: 500;
1727
+ cursor: pointer;
1728
+
1729
+ &.primary {
1730
+ background: #1e6fff;
1731
+ border-color: #1e6fff;
1732
+ color: #ffffff;
1733
+ }
1734
+
1735
+ &.canvas-pick {
1736
+ background: #eff6ff;
1737
+ border-color: #93c5fd;
1738
+ color: #1d4ed8;
1739
+ }
1740
+ }
1741
+
1742
+ .picker-sub-label {
1743
+ font-size: 11px;
1744
+ color: #64748b;
1745
+ font-weight: 500;
1746
+ }
1747
+
1748
+ .picker-thumbs-row {
1749
+ display: grid;
1750
+ grid-template-columns: repeat(4, 1fr);
1751
+ gap: 6px;
1752
+ }
1753
+
1754
+ .picker-mini-card {
1755
+ cursor: pointer;
1756
+ border-radius: 6px;
1757
+ overflow: hidden;
1758
+ background: #ffffff;
1759
+ border: 1px solid #e2e8f0;
1760
+ text-align: center;
1761
+
1762
+ img {
1763
+ width: 100%;
1764
+ height: 44px;
1765
+ object-fit: cover;
1766
+ display: block;
1767
+ }
1768
+
1769
+ span {
1770
+ display: block;
1771
+ font-size: 10px;
1772
+ color: #475569;
1773
+ padding: 2px;
1774
+ overflow: hidden;
1775
+ text-overflow: ellipsis;
1776
+ white-space: nowrap;
1777
+ }
1778
+
1779
+ &:hover {
1780
+ border-color: #1e6fff;
1781
+ }
1782
+ }
1783
+
1784
+ .picker-empty-tip {
1785
+ font-size: 11px;
1786
+ color: #94a3b8;
1787
+ text-align: center;
1788
+ padding: 6px 0;
1789
+ }
1790
+
1791
+ /* 提示词描述框 (1:1 还原图二) */
1792
+ .prompt-card-box {
1793
+ border: 1px solid #e2e8f0;
1794
+ border-radius: 10px;
1795
+ background: #ffffff;
1796
+ padding: 10px;
1797
+ display: flex;
1798
+ flex-direction: column;
1799
+ gap: 8px;
1800
+ transition: border-color 0.15s ease;
1801
+
1802
+ &:focus-within {
1803
+ border-color: #3b82f6;
1804
+ }
1805
+ }
1806
+
1807
+ .prompt-textarea {
1808
+ width: 100%;
1809
+ border: none;
1810
+ outline: none;
1811
+ resize: none;
1812
+ font-size: 12.5px;
1813
+ line-height: 1.5;
1814
+ color: #1e293b;
1815
+ background: transparent;
1816
+ font-family: inherit;
1817
+
1818
+ &::placeholder {
1819
+ color: #94a3b8;
1820
+ }
1821
+ }
1822
+
1823
+ .prompt-bottom-toolbar {
1824
+ display: flex;
1825
+ align-items: center;
1826
+ justify-content: space-between;
1827
+ padding-top: 8px;
1828
+ border-top: 1px solid #f1f5f9;
1829
+ }
1830
+
1831
+ .ai-enhance-switch {
1832
+ display: inline-flex;
1833
+ align-items: center;
1834
+ gap: 6px;
1835
+ cursor: pointer;
1836
+
1837
+ .switch-track {
1838
+ width: 30px;
1839
+ height: 17px;
1840
+ border-radius: 9px;
1841
+ background: #94a3b8;
1842
+ padding: 2px;
1843
+ box-sizing: border-box;
1844
+ display: flex;
1845
+ align-items: center;
1846
+ transition: background 0.2s ease;
1847
+
1848
+ &.is-on {
1849
+ background: #3b5bdb;
1850
+ justify-content: flex-end;
1851
+ }
1852
+ }
1853
+
1854
+ .switch-thumb {
1855
+ width: 13px;
1856
+ height: 13px;
1857
+ border-radius: 50%;
1858
+ background: #ffffff;
1859
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
1860
+ }
1861
+
1862
+ .switch-label {
1863
+ font-size: 12px;
1864
+ font-weight: 600;
1865
+ color: #1e293b;
1866
+ }
1867
+ }
1868
+
1869
+ .prompt-right-icons {
1870
+ display: flex;
1871
+ align-items: center;
1872
+ gap: 4px;
1873
+ }
1874
+
1875
+ .icon-pop-anchor {
1876
+ position: relative;
1877
+ }
1878
+
1879
+ .tool-icon-btn {
1880
+ width: 24px;
1881
+ height: 24px;
1882
+ border-radius: 5px;
1883
+ border: none;
1884
+ background: transparent;
1885
+ color: #64748b;
1886
+ display: inline-flex;
1887
+ align-items: center;
1888
+ justify-content: center;
1889
+ cursor: pointer;
1890
+ padding: 0;
1891
+
1892
+ &:hover,
1893
+ &.active {
1894
+ background: #f1f5f9;
1895
+ color: #0f172a;
1896
+ }
1897
+ }
1898
+
1899
+ .mini-floating-popover {
1900
+ position: absolute;
1901
+ bottom: 30px;
1902
+ right: 0;
1903
+ width: 200px;
1904
+ background: #ffffff;
1905
+ border: 1px solid #e2e8f0;
1906
+ border-radius: 8px;
1907
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.14);
1908
+ padding: 8px;
1909
+ z-index: 45;
1910
+
1911
+ &.tags-pop {
1912
+ width: 220px;
1913
+ }
1914
+
1915
+ .pop-row-between {
1916
+ display: flex;
1917
+ align-items: center;
1918
+ justify-content: space-between;
1919
+ font-size: 12px;
1920
+ font-weight: 600;
1921
+ color: #1e293b;
1922
+ }
1923
+
1924
+ .pop-muted-tip {
1925
+ font-size: 11px;
1926
+ color: #64748b;
1927
+ margin: 4px 0 0;
1928
+ }
1929
+ }
1930
+
1931
+ .quick-tags-wrap {
1932
+ display: flex;
1933
+ flex-wrap: wrap;
1934
+ gap: 4px;
1935
+ }
1936
+
1937
+ .quick-tag-chip {
1938
+ font-size: 11px;
1939
+ padding: 2px 6px;
1940
+ border-radius: 4px;
1941
+ background: #f1f5f9;
1942
+ color: #334155;
1943
+ cursor: pointer;
1944
+
1945
+ &:hover {
1946
+ background: #eff6ff;
1947
+ color: #1e6fff;
1948
+ }
1949
+ }
1950
+
1951
+ /* 参数控制胶囊行 (- 1 +, 1:1, 1K · High) */
1952
+ .params-pills-row {
1953
+ display: flex;
1954
+ align-items: center;
1955
+ gap: 6px;
1956
+ flex-wrap: wrap;
1957
+ }
1958
+
1959
+ .stepper-pill {
1960
+ display: inline-flex;
1961
+ align-items: center;
1962
+ justify-content: space-between;
1963
+ height: 30px;
1964
+ width: 68px;
1965
+ padding: 0 8px;
1966
+ border-radius: 15px;
1967
+ border: 1px solid #e2e8f0;
1968
+ background: #f8fafc;
1969
+ box-sizing: border-box;
1970
+
1971
+ .step-btn {
1972
+ border: none;
1973
+ background: transparent;
1974
+ color: #64748b;
1975
+ font-size: 13px;
1976
+ cursor: pointer;
1977
+ padding: 0 2px;
1978
+
1979
+ &:hover {
1980
+ color: #0f172a;
1981
+ }
1982
+ }
1983
+
1984
+ .step-num {
1985
+ font-size: 12.5px;
1986
+ font-weight: 600;
1987
+ color: #1e293b;
1988
+ }
1989
+ }
1990
+
1991
+ .param-pill-anchor {
1992
+ position: relative;
1993
+ }
1994
+
1995
+ .param-select-pill {
1996
+ height: 30px;
1997
+ padding: 0 11px;
1998
+ border-radius: 15px;
1999
+ border: 1px solid #e2e8f0;
2000
+ background: #f8fafc;
2001
+ color: #1e293b;
2002
+ font-size: 12px;
2003
+ font-weight: 500;
2004
+ display: inline-flex;
2005
+ align-items: center;
2006
+ gap: 6px;
2007
+ cursor: pointer;
2008
+ transition: all 0.15s ease;
2009
+
2010
+ &:hover {
2011
+ border-color: #cbd5e1;
2012
+ background: #f1f5f9;
2013
+ }
2014
+
2015
+ &.is-active-blue {
2016
+ border-color: #bfdbfe;
2017
+ background: #eff6ff;
2018
+ color: #1d4ed8;
2019
+ }
2020
+
2021
+ .ratio-rect-icon {
2022
+ width: 11px;
2023
+ height: 11px;
2024
+ border: 1.6px solid #475569;
2025
+ border-radius: 2px;
2026
+
2027
+ &.wide {
2028
+ width: 13px;
2029
+ height: 9px;
2030
+ }
2031
+ }
2032
+ }
2033
+
2034
+ .pill-dropdown-menu {
2035
+ position: absolute;
2036
+ bottom: 34px;
2037
+ left: 0;
2038
+ min-width: 132px;
2039
+ background: #ffffff;
2040
+ border: 1px solid #e2e8f0;
2041
+ border-radius: 8px;
2042
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.14);
2043
+ padding: 4px;
2044
+ z-index: 45;
2045
+
2046
+ &.right {
2047
+ left: auto;
2048
+ right: 0;
2049
+ }
2050
+ }
2051
+
2052
+ .pill-menu-item {
2053
+ display: flex;
2054
+ align-items: center;
2055
+ justify-content: space-between;
2056
+ gap: 8px;
2057
+ padding: 5px 8px;
2058
+ border-radius: 5px;
2059
+ font-size: 12px;
2060
+ color: #334155;
2061
+ cursor: pointer;
2062
+
2063
+ &:hover {
2064
+ background: #f1f5f9;
2065
+ }
2066
+
2067
+ &.active {
2068
+ background: #eff6ff;
2069
+ color: #1e6fff;
2070
+ font-weight: 600;
2071
+ }
2072
+
2073
+ .muted-sub {
2074
+ font-size: 10.5px;
2075
+ color: #94a3b8;
2076
+ }
2077
+ }
2078
+
2079
+ /* 开始生成按钮 (1:1 还原图二) */
2080
+ .generate-cta-wrap {
2081
+ padding-top: 2px;
2082
+ }
2083
+
2084
+ .start-generate-btn {
2085
+ width: 100%;
2086
+ height: 40px;
2087
+ border-radius: 10px;
2088
+ border: none;
2089
+ background: linear-gradient(90deg, #3b5bdb 0%, #4c6ef5 100%);
2090
+ color: #ffffff;
2091
+ padding: 0 16px;
2092
+ display: flex;
2093
+ align-items: center;
2094
+ justify-content: space-between;
2095
+ font-size: 13.5px;
2096
+ font-weight: 600;
2097
+ cursor: pointer;
2098
+ box-shadow: 0 4px 12px rgba(59, 91, 219, 0.25);
2099
+ transition: all 0.2s ease;
2100
+
2101
+ &:hover:not(:disabled) {
2102
+ background: linear-gradient(90deg, #364fc7 0%, #4263eb 100%);
2103
+ transform: translateY(-1px);
2104
+ }
2105
+
2106
+ &:disabled {
2107
+ opacity: 0.7;
2108
+ cursor: not-allowed;
2109
+ }
2110
+
2111
+ .cta-spinner {
2112
+ width: 15px;
2113
+ height: 15px;
2114
+ border: 2px solid rgba(255, 255, 255, 0.35);
2115
+ border-top-color: #ffffff;
2116
+ border-radius: 50%;
2117
+ animation: spin 0.8s linear infinite;
2118
+ }
2119
+ }
2120
+
2121
+ /* 生成结果列表 */
2122
+ .generated-results-section {
2123
+ border-top: 1px solid #f1f5f9;
2124
+ padding-top: 12px;
2125
+ display: flex;
2126
+ flex-direction: column;
2127
+ gap: 8px;
2128
+ }
2129
+
2130
+ .results-header {
2131
+ display: flex;
2132
+ align-items: center;
2133
+ justify-content: space-between;
2134
+
2135
+ .results-title {
2136
+ font-size: 12px;
2137
+ font-weight: 600;
2138
+ color: #1e293b;
2139
+ }
2140
+
2141
+ .results-sub {
2142
+ font-size: 11px;
2143
+ color: #10b981;
2144
+ }
2145
+ }
2146
+
2147
+ .results-grid {
2148
+ display: grid;
2149
+ grid-template-columns: 1fr 1fr;
2150
+ gap: 8px;
2151
+ }
2152
+
2153
+ .result-card {
2154
+ position: relative;
2155
+ border-radius: 8px;
2156
+ overflow: hidden;
2157
+ background: #f1f5f9;
2158
+ aspect-ratio: 1 / 1;
2159
+ cursor: pointer;
2160
+
2161
+ .result-media {
2162
+ width: 100%;
2163
+ height: 100%;
2164
+ object-fit: cover;
2165
+ display: block;
2166
+ }
2167
+
2168
+ .video-badge {
2169
+ position: absolute;
2170
+ left: 6px;
2171
+ bottom: 6px;
2172
+ width: 20px;
2173
+ height: 20px;
2174
+ border-radius: 50%;
2175
+ background: rgba(15, 23, 42, 0.6);
2176
+ color: #ffffff;
2177
+ display: flex;
2178
+ align-items: center;
2179
+ justify-content: center;
2180
+ font-size: 12px;
2181
+ }
2182
+
2183
+ .result-overlay {
2184
+ position: absolute;
2185
+ inset: 0;
2186
+ background: rgba(15, 23, 42, 0.45);
2187
+ display: flex;
2188
+ align-items: center;
2189
+ justify-content: center;
2190
+ gap: 6px;
2191
+ opacity: 0;
2192
+ transition: opacity 0.15s ease;
2193
+ }
2194
+
2195
+ &:hover .result-overlay {
2196
+ opacity: 1;
2197
+ }
2198
+ }
2199
+
2200
+ .res-act-btn {
2201
+ height: 26px;
2202
+ border-radius: 13px;
2203
+ border: none;
2204
+ font-size: 11px;
2205
+ font-weight: 500;
2206
+ display: inline-flex;
2207
+ align-items: center;
2208
+ justify-content: center;
2209
+ gap: 3px;
2210
+ cursor: pointer;
2211
+
2212
+ &.insert {
2213
+ background: #1e6fff;
2214
+ color: #ffffff;
2215
+ padding: 0 9px;
2216
+ }
2217
+
2218
+ &.preview {
2219
+ width: 26px;
2220
+ background: rgba(255, 255, 255, 0.9);
2221
+ color: #1e293b;
2222
+ }
2223
+ }
2224
+
2225
+ @keyframes spin {
2226
+ from { transform: rotate(0deg); }
2227
+ to { transform: rotate(360deg); }
2228
+ }
2229
+ </style>