koishi-plugin-media-luna 0.0.5 → 0.0.6
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.
- package/client/components/GenerateView.vue +260 -27
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -97,9 +97,26 @@
|
|
|
97
97
|
|
|
98
98
|
<!-- 右侧预览区 -->
|
|
99
99
|
<div class="preview-panel">
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
<!-- 生成中状态 -->
|
|
101
|
+
<div v-if="generating" class="generating-state">
|
|
102
|
+
<div class="generating-content">
|
|
103
|
+
<div class="loader"></div>
|
|
104
|
+
<div class="generating-info">
|
|
105
|
+
<p class="generating-title">正在生成中...</p>
|
|
106
|
+
<p class="generating-timer">
|
|
107
|
+
<k-icon name="stopwatch"></k-icon>
|
|
108
|
+
已用时间: {{ formatElapsedTime(elapsedTime) }}
|
|
109
|
+
</p>
|
|
110
|
+
<p class="generating-hint" v-if="currentTaskId">任务 ID: {{ currentTaskId }}</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- 有结果 -->
|
|
116
|
+
<div v-else-if="result" class="result-container">
|
|
117
|
+
<!-- 成功状态 -->
|
|
118
|
+
<div v-if="result.success && result.output && result.output.length" class="success-result">
|
|
119
|
+
<div class="output-grid">
|
|
103
120
|
<div v-for="(asset, idx) in result.output" :key="idx" class="output-wrapper">
|
|
104
121
|
<!-- 图片 -->
|
|
105
122
|
<template v-if="asset.kind === 'image'">
|
|
@@ -136,25 +153,37 @@
|
|
|
136
153
|
</div>
|
|
137
154
|
</div>
|
|
138
155
|
<div class="result-meta">
|
|
156
|
+
<span class="meta-item success-badge">
|
|
157
|
+
<k-icon name="check-circle"></k-icon> 生成成功
|
|
158
|
+
</span>
|
|
139
159
|
<span class="meta-item" v-if="result.duration">
|
|
140
|
-
<k-icon name="stopwatch"></k-icon> 耗时: {{ (result.duration
|
|
160
|
+
<k-icon name="stopwatch"></k-icon> 耗时: {{ formatElapsedTime(result.duration) }}
|
|
141
161
|
</span>
|
|
142
162
|
<span class="meta-item" v-if="result.taskId">
|
|
143
163
|
<k-icon name="list-alt"></k-icon> 任务 ID: {{ result.taskId }}
|
|
144
164
|
</span>
|
|
145
165
|
</div>
|
|
146
166
|
</div>
|
|
167
|
+
|
|
168
|
+
<!-- 失败状态 -->
|
|
147
169
|
<div v-else class="error-result">
|
|
148
|
-
<
|
|
149
|
-
|
|
170
|
+
<div class="error-content">
|
|
171
|
+
<k-icon name="exclamation-triangle" class="error-icon"></k-icon>
|
|
172
|
+
<div class="error-info">
|
|
173
|
+
<p class="error-title">生成失败</p>
|
|
174
|
+
<p class="error-msg">{{ result.error || '未知错误' }}</p>
|
|
175
|
+
<p class="error-meta" v-if="result.taskId">任务 ID: {{ result.taskId }}</p>
|
|
176
|
+
<p class="error-meta" v-if="result.duration">耗时: {{ formatElapsedTime(result.duration) }}</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<k-button class="retry-btn" @click="generate">
|
|
180
|
+
<template #icon><k-icon name="refresh"></k-icon></template>
|
|
181
|
+
重新生成
|
|
182
|
+
</k-button>
|
|
150
183
|
</div>
|
|
151
184
|
</div>
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
<div class="loader"></div>
|
|
155
|
-
<p>正在生成中,请稍候...</p>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
186
|
+
<!-- 空状态 -->
|
|
158
187
|
<div v-else class="empty-state">
|
|
159
188
|
<k-icon name="image" class="empty-icon"></k-icon>
|
|
160
189
|
<p>在左侧配置并点击生成</p>
|
|
@@ -178,7 +207,7 @@
|
|
|
178
207
|
</template>
|
|
179
208
|
|
|
180
209
|
<script setup lang="ts">
|
|
181
|
-
import { ref, onMounted } from 'vue'
|
|
210
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
182
211
|
import { message } from '@koishijs/client'
|
|
183
212
|
import { ChannelConfig, PresetData, GenerationResult, ClientFileData } from '../types'
|
|
184
213
|
import { channelApi, presetApi, generateApi, taskApi } from '../api'
|
|
@@ -202,6 +231,12 @@ const fileInput = ref<HTMLInputElement>()
|
|
|
202
231
|
const historyGalleryRef = ref<InstanceType<typeof HistoryGallery>>()
|
|
203
232
|
let fileUid = 0
|
|
204
233
|
|
|
234
|
+
// 计时器相关
|
|
235
|
+
const elapsedTime = ref(0)
|
|
236
|
+
const currentTaskId = ref<number | null>(null)
|
|
237
|
+
let timerInterval: ReturnType<typeof setInterval> | null = null
|
|
238
|
+
let startTime = 0
|
|
239
|
+
|
|
205
240
|
const form = ref({
|
|
206
241
|
channel: undefined as number | undefined,
|
|
207
242
|
prompt: '',
|
|
@@ -216,6 +251,33 @@ const lightboxImages = ref<string[]>([])
|
|
|
216
251
|
const lightboxIndex = ref(0)
|
|
217
252
|
const lightboxPrompt = ref('') // 存储最终提示词
|
|
218
253
|
|
|
254
|
+
// 格式化耗时
|
|
255
|
+
const formatElapsedTime = (ms: number) => {
|
|
256
|
+
if (ms < 1000) return `${ms}ms`
|
|
257
|
+
const seconds = ms / 1000
|
|
258
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
|
259
|
+
const minutes = Math.floor(seconds / 60)
|
|
260
|
+
const remainingSeconds = (seconds % 60).toFixed(0)
|
|
261
|
+
return `${minutes}m ${remainingSeconds}s`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 开始计时
|
|
265
|
+
const startTimer = () => {
|
|
266
|
+
startTime = Date.now()
|
|
267
|
+
elapsedTime.value = 0
|
|
268
|
+
timerInterval = setInterval(() => {
|
|
269
|
+
elapsedTime.value = Date.now() - startTime
|
|
270
|
+
}, 100)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 停止计时
|
|
274
|
+
const stopTimer = () => {
|
|
275
|
+
if (timerInterval) {
|
|
276
|
+
clearInterval(timerInterval)
|
|
277
|
+
timerInterval = null
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
219
281
|
// 打开图片预览
|
|
220
282
|
const openImagePreview = (index: number) => {
|
|
221
283
|
if (result.value?.output) {
|
|
@@ -324,6 +386,38 @@ const removeFile = (index: number) => {
|
|
|
324
386
|
}
|
|
325
387
|
}
|
|
326
388
|
|
|
389
|
+
// 尝试通过 taskId 获取结果
|
|
390
|
+
const fetchTaskResult = async (taskId: number): Promise<GenerationResult | null> => {
|
|
391
|
+
try {
|
|
392
|
+
const task = await taskApi.get(taskId)
|
|
393
|
+
|
|
394
|
+
// 获取最终提示词
|
|
395
|
+
lightboxPrompt.value = (task.middlewareLogs as any)?.preset?.transformedPrompt
|
|
396
|
+
|| task.requestSnapshot?.prompt
|
|
397
|
+
|| ''
|
|
398
|
+
|
|
399
|
+
if (task.status === 'success' && task.responseSnapshot && task.responseSnapshot.length > 0) {
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
output: task.responseSnapshot,
|
|
403
|
+
taskId: task.id,
|
|
404
|
+
duration: task.duration || undefined
|
|
405
|
+
}
|
|
406
|
+
} else if (task.status === 'failed') {
|
|
407
|
+
const errorInfo = (task.middlewareLogs as any)?._error
|
|
408
|
+
return {
|
|
409
|
+
success: false,
|
|
410
|
+
error: errorInfo?.message || '生成失败',
|
|
411
|
+
taskId: task.id,
|
|
412
|
+
duration: task.duration || undefined
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null
|
|
416
|
+
} catch {
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
327
421
|
const generate = async () => {
|
|
328
422
|
if (!form.value.channel) {
|
|
329
423
|
message.warning('请选择渠道')
|
|
@@ -332,6 +426,8 @@ const generate = async () => {
|
|
|
332
426
|
|
|
333
427
|
generating.value = true
|
|
334
428
|
result.value = null
|
|
429
|
+
currentTaskId.value = null
|
|
430
|
+
startTimer()
|
|
335
431
|
|
|
336
432
|
try {
|
|
337
433
|
const params: any = {
|
|
@@ -353,30 +449,64 @@ const generate = async () => {
|
|
|
353
449
|
}
|
|
354
450
|
|
|
355
451
|
const res = await generateApi.generate(params)
|
|
356
|
-
result.value = res
|
|
357
452
|
|
|
358
|
-
//
|
|
453
|
+
// 更新 taskId
|
|
454
|
+
if (res.taskId) {
|
|
455
|
+
currentTaskId.value = res.taskId
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 如果成功,直接使用结果
|
|
359
459
|
if (res.success) {
|
|
460
|
+
result.value = res
|
|
360
461
|
historyGalleryRef.value?.refresh()
|
|
361
|
-
|
|
462
|
+
|
|
463
|
+
// 获取最终提示词
|
|
362
464
|
if (res.taskId) {
|
|
363
465
|
try {
|
|
364
466
|
const task = await taskApi.get(res.taskId)
|
|
365
|
-
// 优先使用预设中间件处理后的最终提示词
|
|
366
467
|
lightboxPrompt.value = (task.middlewareLogs as any)?.preset?.transformedPrompt
|
|
367
468
|
|| task.requestSnapshot?.prompt
|
|
368
469
|
|| ''
|
|
369
470
|
} catch {
|
|
370
|
-
// 如果获取失败,使用输入的提示词
|
|
371
471
|
lightboxPrompt.value = form.value.prompt
|
|
372
472
|
}
|
|
373
473
|
} else {
|
|
374
474
|
lightboxPrompt.value = form.value.prompt
|
|
375
475
|
}
|
|
476
|
+
} else {
|
|
477
|
+
// API 返回失败,但可能任务实际成功了,尝试通过 taskId 获取
|
|
478
|
+
if (res.taskId) {
|
|
479
|
+
// 等待一小段时间让后端完成处理
|
|
480
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
481
|
+
const taskResult = await fetchTaskResult(res.taskId)
|
|
482
|
+
if (taskResult && taskResult.success) {
|
|
483
|
+
result.value = taskResult
|
|
484
|
+
historyGalleryRef.value?.refresh()
|
|
485
|
+
} else {
|
|
486
|
+
result.value = res
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
result.value = res
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (e: any) {
|
|
493
|
+
// 请求异常,尝试通过 taskId 恢复
|
|
494
|
+
if (currentTaskId.value) {
|
|
495
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
496
|
+
const taskResult = await fetchTaskResult(currentTaskId.value)
|
|
497
|
+
if (taskResult) {
|
|
498
|
+
result.value = taskResult
|
|
499
|
+
if (taskResult.success) {
|
|
500
|
+
historyGalleryRef.value?.refresh()
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
result.value = { success: false, error: e.message || '请求失败' }
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
result.value = { success: false, error: e.message || '请求失败' }
|
|
376
507
|
}
|
|
377
|
-
} catch (e) {
|
|
378
|
-
result.value = { success: false, error: '请求失败' }
|
|
379
508
|
} finally {
|
|
509
|
+
stopTimer()
|
|
380
510
|
generating.value = false
|
|
381
511
|
}
|
|
382
512
|
}
|
|
@@ -391,6 +521,10 @@ const handleHistorySelect = (task: { prompt: string }) => {
|
|
|
391
521
|
onMounted(() => {
|
|
392
522
|
fetchData()
|
|
393
523
|
})
|
|
524
|
+
|
|
525
|
+
onUnmounted(() => {
|
|
526
|
+
stopTimer()
|
|
527
|
+
})
|
|
394
528
|
</script>
|
|
395
529
|
|
|
396
530
|
<style scoped>
|
|
@@ -568,7 +702,7 @@ onMounted(() => {
|
|
|
568
702
|
}
|
|
569
703
|
|
|
570
704
|
/* States */
|
|
571
|
-
.empty-state
|
|
705
|
+
.empty-state {
|
|
572
706
|
text-align: center;
|
|
573
707
|
color: var(--k-color-text-description);
|
|
574
708
|
display: flex;
|
|
@@ -585,14 +719,34 @@ onMounted(() => {
|
|
|
585
719
|
color: var(--k-color-text);
|
|
586
720
|
}
|
|
587
721
|
|
|
722
|
+
/* 生成中状态 - 增强样式 */
|
|
723
|
+
.generating-state {
|
|
724
|
+
display: flex;
|
|
725
|
+
flex-direction: column;
|
|
726
|
+
align-items: center;
|
|
727
|
+
justify-content: center;
|
|
728
|
+
height: 100%;
|
|
729
|
+
width: 100%;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.generating-content {
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-direction: column;
|
|
735
|
+
align-items: center;
|
|
736
|
+
gap: 1.5rem;
|
|
737
|
+
padding: 2rem;
|
|
738
|
+
background: linear-gradient(135deg, rgba(var(--k-color-primary-rgb), 0.05) 0%, rgba(var(--k-color-primary-rgb), 0.02) 100%);
|
|
739
|
+
border-radius: 16px;
|
|
740
|
+
border: 1px solid rgba(var(--k-color-primary-rgb), 0.1);
|
|
741
|
+
}
|
|
742
|
+
|
|
588
743
|
.loader {
|
|
589
744
|
border: 4px solid var(--k-color-bg-2);
|
|
590
745
|
border-top: 4px solid var(--k-color-active);
|
|
591
746
|
border-radius: 50%;
|
|
592
|
-
width:
|
|
593
|
-
height:
|
|
747
|
+
width: 48px;
|
|
748
|
+
height: 48px;
|
|
594
749
|
animation: spin 1s linear infinite;
|
|
595
|
-
margin: 0 auto 1rem;
|
|
596
750
|
}
|
|
597
751
|
|
|
598
752
|
@keyframes spin {
|
|
@@ -600,6 +754,35 @@ onMounted(() => {
|
|
|
600
754
|
100% { transform: rotate(360deg); }
|
|
601
755
|
}
|
|
602
756
|
|
|
757
|
+
.generating-info {
|
|
758
|
+
text-align: center;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.generating-title {
|
|
762
|
+
font-size: 1.1rem;
|
|
763
|
+
font-weight: 600;
|
|
764
|
+
color: var(--k-color-text);
|
|
765
|
+
margin: 0 0 0.75rem 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.generating-timer {
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: center;
|
|
771
|
+
justify-content: center;
|
|
772
|
+
gap: 0.5rem;
|
|
773
|
+
font-size: 1.5rem;
|
|
774
|
+
font-weight: 700;
|
|
775
|
+
color: var(--k-color-active);
|
|
776
|
+
margin: 0 0 0.5rem 0;
|
|
777
|
+
font-variant-numeric: tabular-nums;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.generating-hint {
|
|
781
|
+
font-size: 0.85rem;
|
|
782
|
+
color: var(--k-color-text-description);
|
|
783
|
+
margin: 0;
|
|
784
|
+
}
|
|
785
|
+
|
|
603
786
|
/* Result */
|
|
604
787
|
.result-container {
|
|
605
788
|
width: 100%;
|
|
@@ -682,6 +865,7 @@ onMounted(() => {
|
|
|
682
865
|
margin-top: 1.5rem;
|
|
683
866
|
display: flex;
|
|
684
867
|
gap: 1.5rem;
|
|
868
|
+
flex-wrap: wrap;
|
|
685
869
|
color: var(--k-color-text-description);
|
|
686
870
|
font-size: 0.9rem;
|
|
687
871
|
border-top: 1px solid var(--k-color-border);
|
|
@@ -694,14 +878,63 @@ onMounted(() => {
|
|
|
694
878
|
gap: 0.5rem;
|
|
695
879
|
}
|
|
696
880
|
|
|
881
|
+
.success-badge {
|
|
882
|
+
color: var(--k-color-success, #67c23a);
|
|
883
|
+
font-weight: 600;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* 错误状态 - 增强样式 */
|
|
697
887
|
.error-result {
|
|
698
|
-
|
|
699
|
-
|
|
888
|
+
display: flex;
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
align-items: center;
|
|
891
|
+
justify-content: center;
|
|
892
|
+
height: 100%;
|
|
893
|
+
gap: 1.5rem;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.error-content {
|
|
897
|
+
display: flex;
|
|
898
|
+
flex-direction: column;
|
|
899
|
+
align-items: center;
|
|
900
|
+
gap: 1rem;
|
|
901
|
+
padding: 2rem;
|
|
902
|
+
background: linear-gradient(135deg, rgba(245, 108, 108, 0.08) 0%, rgba(245, 108, 108, 0.02) 100%);
|
|
903
|
+
border-radius: 16px;
|
|
904
|
+
border: 1px solid rgba(245, 108, 108, 0.2);
|
|
700
905
|
}
|
|
701
906
|
|
|
702
907
|
.error-icon {
|
|
703
908
|
font-size: 3rem;
|
|
704
|
-
|
|
909
|
+
color: var(--k-color-error, #f56c6c);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.error-info {
|
|
913
|
+
text-align: center;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.error-title {
|
|
917
|
+
font-size: 1.1rem;
|
|
918
|
+
font-weight: 600;
|
|
919
|
+
color: var(--k-color-error, #f56c6c);
|
|
920
|
+
margin: 0 0 0.5rem 0;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.error-msg {
|
|
924
|
+
color: var(--k-color-text);
|
|
925
|
+
margin: 0 0 0.5rem 0;
|
|
926
|
+
max-width: 400px;
|
|
927
|
+
word-break: break-word;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.error-meta {
|
|
931
|
+
font-size: 0.85rem;
|
|
932
|
+
color: var(--k-color-text-description);
|
|
933
|
+
margin: 0;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.retry-btn {
|
|
937
|
+
margin-top: 0.5rem;
|
|
705
938
|
}
|
|
706
939
|
|
|
707
940
|
/* Upload Area */
|
|
@@ -810,4 +1043,4 @@ onMounted(() => {
|
|
|
810
1043
|
.file-link:hover {
|
|
811
1044
|
background-color: var(--k-color-bg-3);
|
|
812
1045
|
}
|
|
813
|
-
</style>
|
|
1046
|
+
</style>
|