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.
@@ -97,9 +97,26 @@
97
97
 
98
98
  <!-- 右侧预览区 -->
99
99
  <div class="preview-panel">
100
- <div v-if="result" class="result-container">
101
- <div v-if="result.success" class="success-result">
102
- <div class="output-grid" v-if="result.output && result.output.length">
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 / 1000).toFixed(2) }}s
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
- <k-icon name="warning" class="error-icon"></k-icon>
149
- <div class="error-msg">{{ result.error || '生成失败' }}</div>
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
- <div v-else-if="generating" class="generating-state">
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, .generating-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: 40px;
593
- height: 40px;
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
- text-align: center;
699
- color: var(--k-color-error);
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
- margin-bottom: 1rem;
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>