aegon-lv97 1.0.0

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.
Files changed (78) hide show
  1. package/gen-ai/build-report/chartStreamRefs.ts +227 -0
  2. package/gen-ai/build-report/coherence-check/index.vue +559 -0
  3. package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
  4. package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  5. package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  6. package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  7. package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
  8. package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
  9. package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  10. package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  11. package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  12. package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  13. package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  14. package/gen-ai/build-report/components/paramsSetting.vue +886 -0
  15. package/gen-ai/build-report/components/reportEdit.vue +2283 -0
  16. package/gen-ai/build-report/index.vue +856 -0
  17. package/gen-ai/build-report/report-domain.ts +1133 -0
  18. package/gen-ai/build-report/report-template-data.ts +295 -0
  19. package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
  20. package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  21. package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  22. package/gen-ai/common/assets/NotoSans.woff2 +0 -0
  23. package/gen-ai/common/assets/fonts.css +278 -0
  24. package/gen-ai/common/assets/pagination.css +163 -0
  25. package/gen-ai/common/directives/btnLoading.ts +455 -0
  26. package/gen-ai/common/directives/debounce.ts +98 -0
  27. package/gen-ai/common/directives/index.ts +30 -0
  28. package/gen-ai/common/en.md +417 -0
  29. package/gen-ai/common/index.ts +36 -0
  30. package/gen-ai/common/types/component-expose.ts +9 -0
  31. package/gen-ai/common/utils/api-res-status.ts +7 -0
  32. package/gen-ai/common/utils/navigation.ts +59 -0
  33. package/gen-ai/common/utils/remote-request.ts +45 -0
  34. package/gen-ai/common/utils/scroll.ts +577 -0
  35. package/gen-ai/components/AIExport/docx-export.ts +950 -0
  36. package/gen-ai/components/AIExport/docxExport.ts +2566 -0
  37. package/gen-ai/components/AIExport/index.ts +52 -0
  38. package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
  39. package/gen-ai/components/CapsuleScrollbar.vue +149 -0
  40. package/gen-ai/components/ChapterTitleScroll.vue +468 -0
  41. package/gen-ai/components/EntryHeroNavCards.vue +379 -0
  42. package/gen-ai/components/GenAiPagination.vue +52 -0
  43. package/gen-ai/components/ModelCard.vue +1114 -0
  44. package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
  45. package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  46. package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  47. package/gen-ai/components/Preview/index.ts +11 -0
  48. package/gen-ai/components/Preview/preview.ts +724 -0
  49. package/gen-ai/components/PromptField.vue +505 -0
  50. package/gen-ai/components/RecentReportsTable.vue +974 -0
  51. package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  52. package/gen-ai/components/ReportHistoryList.vue +637 -0
  53. package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  54. package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
  55. package/gen-ai/components/editor/StructureBlockView.vue +441 -0
  56. package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  57. package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
  58. package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  59. package/gen-ai/components/editor/index.ts +28 -0
  60. package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
  61. package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
  62. package/gen-ai/gen-entry/api/index.ts +32 -0
  63. package/gen-ai/gen-entry/index.vue +287 -0
  64. package/gen-ai/gen-entry/types/index.ts +4 -0
  65. package/gen-ai/i.mark +2 -0
  66. package/gen-ai/management-center/index.vue +1815 -0
  67. package/gen-ai/management-center/mockData.ts +248 -0
  68. package/gen-ai/management-center/types/index.ts +37 -0
  69. package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  70. package/gen-ai/model-manager/api/index.ts +32 -0
  71. package/gen-ai/model-manager/api/mockApi.ts +33 -0
  72. package/gen-ai/model-manager/index.vue +1583 -0
  73. package/gen-ai/model-manager/mockData.ts +177 -0
  74. package/gen-ai/model-manager/template-detail-shared.ts +154 -0
  75. package/gen-ai/model-manager/template-editor.vue +345 -0
  76. package/gen-ai/model-manager/types/index.ts +40 -0
  77. package/gen-ai/model-manager/utils.ts +25 -0
  78. package/package.json +12 -0
@@ -0,0 +1,1114 @@
1
+ <template>
2
+ <article ref="cardRef" class="model-card" :class="{ 'model-card--no-actions': !showActions }">
3
+ <header class="model-card__header">
4
+ <div class="model-card__title-block">
5
+ <h3 ref="titleRef" class="model-card__title">{{ displayTitle }}</h3>
6
+ <el-popover
7
+ v-if="titleOverflow"
8
+ placement="bottom"
9
+ :width="320"
10
+ trigger="click"
11
+ popper-class="model-card-overflow-popover"
12
+ >
13
+ <template #reference>
14
+ <button type="button" class="model-card__ellipsis" title="查看完整標題" @click.stop>
15
+ ...
16
+ </button>
17
+ </template>
18
+ <div class="model-card-overflow-popover__text">{{ template.name }}</div>
19
+ </el-popover>
20
+ </div>
21
+ <div class="model-card__meta-row">
22
+ <div class="model-card__tags">
23
+ <div v-if="industryTag" class="model-card__tag-col">
24
+ <span ref="industryTagWrapRef" class="model-card__tag model-card__tag--industry">
25
+ <span ref="industryTagRef" class="model-card__tag-text">{{ displayIndustry }}</span>
26
+ <el-popover
27
+ v-if="industryOverflow"
28
+ placement="bottom"
29
+ :width="320"
30
+ trigger="click"
31
+ popper-class="model-card-overflow-popover"
32
+ >
33
+ <template #reference>
34
+ <button
35
+ type="button"
36
+ class="model-card__ellipsis"
37
+ title="查看完整行業"
38
+ @click.stop
39
+ >
40
+ ...
41
+ </button>
42
+ </template>
43
+ <div class="model-card-overflow-popover__text">{{ industryTag }}</div>
44
+ </el-popover>
45
+ </span>
46
+ </div>
47
+ <div class="model-card__tag-col">
48
+ <span class="model-card__tag model-card__tag--chapters"
49
+ >{{ template.chapters.length }} 章節</span
50
+ >
51
+ </div>
52
+ </div>
53
+ <el-popover
54
+ placement="bottom-end"
55
+ :width="popoverWidth"
56
+ trigger="click"
57
+ :popper-style="popoverPopperStyle"
58
+ popper-class="model-card-preview-popover"
59
+ @show="onPopoverShow"
60
+ >
61
+ <template #reference>
62
+ <button
63
+ type="button"
64
+ class="model-card__view"
65
+ title="查看完整資訊"
66
+ @click.stop="syncPopoverMetrics"
67
+ >
68
+ <el-icon :size="15"><View /></el-icon>
69
+ </button>
70
+ </template>
71
+ <div
72
+ class="model-card-preview"
73
+ :class="{ 'is-scrollable': previewNeedsScroll }"
74
+ :style="{ maxHeight: `${popoverMaxHeight}px` }"
75
+ >
76
+ <el-scrollbar
77
+ ref="previewScrollbarRef"
78
+ class="model-card-preview__scroll"
79
+ :max-height="popoverMaxHeight"
80
+ >
81
+ <div class="model-card-preview__inner">
82
+ <h4 class="model-card-preview__title">{{ template.name }}</h4>
83
+ <div class="model-card-preview__tags">
84
+ <span
85
+ v-if="industryTag"
86
+ class="model-card-preview__tag model-card-preview__tag--industry"
87
+ >{{ industryTag }}</span
88
+ >
89
+ <span class="model-card-preview__tag model-card-preview__tag--chapters"
90
+ >{{ template.chapters.length }} 章節</span
91
+ >
92
+ </div>
93
+ <p class="model-card-preview__desc">{{ template.description }}</p>
94
+ </div>
95
+ </el-scrollbar>
96
+ </div>
97
+ </el-popover>
98
+ </div>
99
+ </header>
100
+
101
+ <div class="model-card__desc-block">
102
+ <p ref="descRef" class="model-card__desc">{{ displayDesc }}</p>
103
+ <el-popover
104
+ v-if="descOverflow"
105
+ placement="bottom"
106
+ :width="360"
107
+ trigger="click"
108
+ popper-class="model-card-overflow-popover"
109
+ >
110
+ <template #reference>
111
+ <button type="button" class="model-card__ellipsis" title="查看完整描述" @click.stop>
112
+ ...
113
+ </button>
114
+ </template>
115
+ <div class="model-card-overflow-popover__text">{{ template.description }}</div>
116
+ </el-popover>
117
+ </div>
118
+
119
+ <section class="model-card__chapters" aria-label="章節結構">
120
+ <span class="model-card__chapters-label">章節結構</span>
121
+ <div
122
+ ref="chaptersBoxRef"
123
+ class="model-card__chapters-box"
124
+ :class="{ 'is-scrollable': needsScroll }"
125
+ >
126
+ <el-scrollbar ref="scrollbarRef" class="model-card__chapters-scroll">
127
+ <ul class="model-card__chapters-list">
128
+ <li v-for="(chapter, index) in parsedChapters" :key="`${template.id}-${index}`">
129
+ <span class="model-card__chapter-no">{{ chapter.no }}.</span>
130
+ <span class="model-card__chapter-text">{{ chapter.text }}</span>
131
+ </li>
132
+ </ul>
133
+ </el-scrollbar>
134
+ </div>
135
+ </section>
136
+
137
+ <div class="model-card__meta">
138
+ <p class="model-card__meta-line">
139
+ <span class="model-card__meta-label">作者</span>
140
+ <span class="model-card__meta-value">{{ shortName(template.author) }}</span>
141
+ <span class="model-card__meta-label"> · 審批</span>
142
+ <span class="model-card__meta-value">{{ shortName(template.approver) }}</span>
143
+ </p>
144
+ <p class="model-card__meta-line">
145
+ <span class="model-card__meta-label">最後更新</span>
146
+ <span class="model-card__meta-value">{{ formatUpdatedAt(template.updatedAt) }}</span>
147
+ </p>
148
+ </div>
149
+
150
+ <footer v-if="showActions" ref="footerRef" class="model-card__footer">
151
+ <div class="model-card__footer-left">
152
+ <button type="button" class="model-card__action" @click.stop="$emit('edit', template)">
153
+ <el-icon :size="15"><EditPen /></el-icon>
154
+ <span>編輯</span>
155
+ </button>
156
+ <span class="model-card__footer-sep" aria-hidden="true" />
157
+ <button
158
+ type="button"
159
+ class="model-card__action"
160
+ :title="template.locked ? `${template.locker} 已鎖定` : '鎖定模板'"
161
+ @click.stop="$emit('lock', template)"
162
+ >
163
+ <el-icon :size="15">
164
+ <Lock v-if="template.locked" />
165
+ <Unlock v-else />
166
+ </el-icon>
167
+ <span>{{ template.locked ? '解鎖' : '鎖定' }}</span>
168
+ </button>
169
+ </div>
170
+ <button
171
+ type="button"
172
+ class="model-card__action model-card__action--danger"
173
+ @click.stop="$emit('delete', template)"
174
+ >
175
+ <el-icon :size="15"><Delete /></el-icon>
176
+ <span>刪除</span>
177
+ </button>
178
+ </footer>
179
+ </article>
180
+ </template>
181
+
182
+ <script lang="ts">
183
+ export interface ModelCardItem {
184
+ id: string
185
+ name: string
186
+ description: string
187
+ chapters: string[]
188
+ author: string
189
+ approver: string
190
+ updatedAt: string
191
+ unit: string
192
+ locked: boolean
193
+ locker: string
194
+ industry?: string
195
+ }
196
+ </script>
197
+
198
+ <script setup lang="ts">
199
+ import { Delete, EditPen, Lock, Unlock, View } from '@element-plus/icons-vue'
200
+ import type { ScrollbarInstance } from 'element-plus'
201
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
202
+
203
+ const props = withDefaults(
204
+ defineProps<{
205
+ template: ModelCardItem
206
+ /** 是否顯示底部編輯 / 鎖定 / 刪除操作欄 */
207
+ showActions?: boolean
208
+ }>(),
209
+ { showActions: true }
210
+ )
211
+
212
+ defineEmits<{
213
+ edit: [template: ModelCardItem]
214
+ lock: [template: ModelCardItem]
215
+ delete: [template: ModelCardItem]
216
+ }>()
217
+
218
+ const industryTag = computed(() => props.template.industry?.trim() || props.template.unit)
219
+
220
+ const parsedChapters = computed(() =>
221
+ props.template.chapters.map((chapter, index) => {
222
+ const matched = chapter.match(/^(\d+)\.\s*(.*)$/)
223
+ if (matched) {
224
+ return { no: matched[1]!, text: matched[2] || '' }
225
+ }
226
+ return { no: String(index + 1), text: chapter }
227
+ })
228
+ )
229
+
230
+ const titleRef = ref<HTMLElement | null>(null)
231
+ const industryTagRef = ref<HTMLElement | null>(null)
232
+ const industryTagWrapRef = ref<HTMLElement | null>(null)
233
+ const descRef = ref<HTMLElement | null>(null)
234
+ const displayTitle = ref(props.template.name || '')
235
+ const displayIndustry = ref(industryTag.value || '')
236
+ const displayDesc = ref(props.template.description || '')
237
+ const titleOverflow = ref(false)
238
+ const industryOverflow = ref(false)
239
+ const descOverflow = ref(false)
240
+
241
+ const ELLIPSIS_RESERVE = 32
242
+
243
+ function readFont(el: HTMLElement) {
244
+ const cs = getComputedStyle(el)
245
+ return `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`
246
+ }
247
+
248
+ function measureSingleLineWidth(text: string, font: string) {
249
+ const probe = document.createElement('span')
250
+ probe.style.cssText = [
251
+ 'position:absolute',
252
+ 'visibility:hidden',
253
+ 'pointer-events:none',
254
+ 'left:-99999px',
255
+ 'top:0',
256
+ 'white-space:nowrap',
257
+ `font:${font}`,
258
+ ].join(';')
259
+ probe.textContent = text
260
+ document.body.appendChild(probe)
261
+ const width = probe.offsetWidth
262
+ probe.remove()
263
+ return width
264
+ }
265
+
266
+ function fitSingleLine(full: string, maxWidth: number, font: string) {
267
+ if (!full || maxWidth <= 0) return { text: '', overflow: full.length > 0 }
268
+ if (measureSingleLineWidth(full, font) <= maxWidth - 1) {
269
+ return { text: full, overflow: false }
270
+ }
271
+ const budget = Math.max(0, maxWidth - ELLIPSIS_RESERVE - 1)
272
+ let lo = 0
273
+ let hi = full.length
274
+ while (lo < hi) {
275
+ const mid = Math.ceil((lo + hi) / 2)
276
+ if (measureSingleLineWidth(full.slice(0, mid), font) <= budget) lo = mid
277
+ else hi = mid - 1
278
+ }
279
+ return { text: full.slice(0, Math.max(0, lo)), overflow: true }
280
+ }
281
+
282
+ function fitMultiline(full: string, width: number, maxHeight: number, styles: CSSStyleDeclaration) {
283
+ if (!full || width <= 0 || maxHeight <= 0) return { text: '', overflow: full.length > 0 }
284
+
285
+ const probe = document.createElement('div')
286
+ probe.style.cssText = [
287
+ 'position:absolute',
288
+ 'visibility:hidden',
289
+ 'pointer-events:none',
290
+ 'left:-99999px',
291
+ 'top:0',
292
+ `width:${Math.floor(width)}px`,
293
+ `font:${styles.font}`,
294
+ `font-size:${styles.fontSize}`,
295
+ `font-weight:${styles.fontWeight}`,
296
+ `font-family:${styles.fontFamily}`,
297
+ `line-height:${styles.lineHeight}`,
298
+ 'word-break:break-word',
299
+ 'overflow-wrap:anywhere',
300
+ 'white-space:normal',
301
+ 'box-sizing:border-box',
302
+ ].join(';')
303
+ document.body.appendChild(probe)
304
+
305
+ const fits = (text: string) => {
306
+ probe.textContent = text
307
+ return probe.scrollHeight <= maxHeight + 1
308
+ }
309
+
310
+ try {
311
+ if (fits(full)) return { text: full, overflow: false }
312
+ let lo = 0
313
+ let hi = full.length
314
+ while (lo < hi) {
315
+ const mid = Math.ceil((lo + hi) / 2)
316
+ if (fits(full.slice(0, mid))) lo = mid
317
+ else hi = mid - 1
318
+ }
319
+ return { text: full.slice(0, Math.max(0, lo)), overflow: true }
320
+ } finally {
321
+ probe.remove()
322
+ }
323
+ }
324
+
325
+ function updateTextOverflow() {
326
+ const name = props.template.name || ''
327
+ const industry = industryTag.value || ''
328
+ const description = props.template.description || ''
329
+
330
+ const titleEl = titleRef.value
331
+ const titleBlock = titleEl?.parentElement
332
+ if (titleEl && titleBlock) {
333
+ const fitted = fitSingleLine(name, titleBlock.clientWidth, readFont(titleEl))
334
+ displayTitle.value = fitted.text
335
+ titleOverflow.value = fitted.overflow
336
+ } else {
337
+ displayTitle.value = name
338
+ titleOverflow.value = false
339
+ }
340
+
341
+ const industryEl = industryTagRef.value
342
+ const industryWrap = industryTagWrapRef.value
343
+ if (industryEl && industryWrap) {
344
+ const cs = getComputedStyle(industryWrap)
345
+ const pad = (Number.parseFloat(cs.paddingLeft) || 0) + (Number.parseFloat(cs.paddingRight) || 0)
346
+ const fitted = fitSingleLine(industry, industryWrap.clientWidth - pad, readFont(industryEl))
347
+ displayIndustry.value = fitted.text
348
+ industryOverflow.value = fitted.overflow
349
+ } else {
350
+ displayIndustry.value = industry
351
+ industryOverflow.value = false
352
+ }
353
+
354
+ const descEl = descRef.value
355
+ const descBlock = descEl?.parentElement
356
+ if (descEl && descBlock) {
357
+ const cs = getComputedStyle(descEl)
358
+ const maxHeight = Number.parseFloat(cs.maxHeight) || descEl.clientHeight
359
+ let fitted = fitMultiline(description, descBlock.clientWidth, maxHeight, cs)
360
+ if (fitted.overflow) {
361
+ fitted = fitMultiline(
362
+ description,
363
+ Math.max(0, descBlock.clientWidth - ELLIPSIS_RESERVE),
364
+ maxHeight,
365
+ cs
366
+ )
367
+ }
368
+ displayDesc.value = fitted.text
369
+ descOverflow.value = fitted.overflow
370
+ } else {
371
+ displayDesc.value = description
372
+ descOverflow.value = false
373
+ }
374
+ }
375
+
376
+ const cardRef = ref<HTMLElement | null>(null)
377
+ const chaptersBoxRef = ref<HTMLElement | null>(null)
378
+ const footerRef = ref<HTMLElement | null>(null)
379
+ const scrollbarRef = ref<ScrollbarInstance>()
380
+ const previewScrollbarRef = ref<ScrollbarInstance>()
381
+ const needsScroll = ref(false)
382
+ const previewNeedsScroll = ref(false)
383
+ const popoverMaxHeight = ref(240)
384
+ const popoverWidth = ref(320)
385
+
386
+ const popoverPopperStyle = computed(() => ({
387
+ padding: '0',
388
+ maxHeight: `${popoverMaxHeight.value}px`,
389
+ width: `${popoverWidth.value}px`,
390
+ }))
391
+
392
+ let resizeObserver: ResizeObserver | null = null
393
+
394
+ function getWrapEl(instance?: ScrollbarInstance | null) {
395
+ return instance?.wrapRef ?? null
396
+ }
397
+
398
+ function updateNeedsScroll() {
399
+ const wrap = getWrapEl(scrollbarRef.value)
400
+ if (!wrap) {
401
+ needsScroll.value = false
402
+ return
403
+ }
404
+ needsScroll.value = wrap.scrollHeight > wrap.clientHeight + 1
405
+ }
406
+
407
+ function updatePreviewNeedsScroll() {
408
+ const wrap = getWrapEl(previewScrollbarRef.value)
409
+ if (!wrap) {
410
+ previewNeedsScroll.value = false
411
+ return
412
+ }
413
+ previewNeedsScroll.value = wrap.scrollHeight > wrap.clientHeight + 1
414
+ }
415
+
416
+ function syncPopoverMetrics() {
417
+ const card = cardRef.value
418
+ const footer = footerRef.value
419
+ const viewBtn = card?.querySelector('.model-card__view') as HTMLElement | null
420
+ if (!card || !viewBtn) return
421
+
422
+ const cardRect = card.getBoundingClientRect()
423
+ const bottomBound = footer ? footer.getBoundingClientRect().top : cardRect.bottom
424
+ const viewBottom = viewBtn.getBoundingClientRect().bottom
425
+ // 有操作欄時底界為 footer 上邊;選擇模板頁無操作欄時用卡片底邊
426
+ popoverMaxHeight.value = Math.max(120, Math.floor(bottomBound - viewBottom - 4))
427
+ popoverWidth.value = Math.max(240, Math.floor(cardRect.width - 40))
428
+ }
429
+
430
+ function onPopoverShow() {
431
+ syncPopoverMetrics()
432
+ nextTick(() => {
433
+ syncPopoverMetrics()
434
+ updatePreviewNeedsScroll()
435
+ requestAnimationFrame(() => updatePreviewNeedsScroll())
436
+ })
437
+ }
438
+
439
+ function bindOverflowObserver() {
440
+ resizeObserver?.disconnect()
441
+ const wrap = getWrapEl(scrollbarRef.value)
442
+ resizeObserver = new ResizeObserver(() => {
443
+ updateNeedsScroll()
444
+ syncPopoverMetrics()
445
+ updateTextOverflow()
446
+ })
447
+ if (wrap) {
448
+ resizeObserver.observe(wrap)
449
+ const view = wrap.querySelector('.el-scrollbar__view')
450
+ if (view) resizeObserver.observe(view)
451
+ }
452
+ if (cardRef.value) resizeObserver.observe(cardRef.value)
453
+ if (titleRef.value) resizeObserver.observe(titleRef.value)
454
+ if (industryTagWrapRef.value) resizeObserver.observe(industryTagWrapRef.value)
455
+ if (descRef.value) resizeObserver.observe(descRef.value)
456
+ updateNeedsScroll()
457
+ updateTextOverflow()
458
+ }
459
+
460
+ function shortName(value: string) {
461
+ return (
462
+ value
463
+ .replace(/([^)]*)/g, '')
464
+ .replace(/\([^)]*\)/g, '')
465
+ .trim() || value
466
+ )
467
+ }
468
+
469
+ function formatUpdatedAt(raw: string) {
470
+ const match = raw.match(/(\d{4})[/-](\d{1,2})[/-](\d{1,2})/)
471
+ if (!match) return raw
472
+ return `${match[1]} 年 ${Number(match[2])} 月 ${Number(match[3])} 日`
473
+ }
474
+
475
+ watch(
476
+ () =>
477
+ [
478
+ props.template.name,
479
+ props.template.description,
480
+ props.template.industry,
481
+ props.template.chapters,
482
+ ] as const,
483
+ () =>
484
+ nextTick(() => {
485
+ bindOverflowObserver()
486
+ updateNeedsScroll()
487
+ updateTextOverflow()
488
+ }),
489
+ { deep: true }
490
+ )
491
+
492
+ onMounted(() => {
493
+ nextTick(() => {
494
+ bindOverflowObserver()
495
+ updateNeedsScroll()
496
+ updateTextOverflow()
497
+ syncPopoverMetrics()
498
+ })
499
+ window.addEventListener('resize', syncPopoverMetrics, { passive: true })
500
+ })
501
+
502
+ onBeforeUnmount(() => {
503
+ resizeObserver?.disconnect()
504
+ resizeObserver = null
505
+ window.removeEventListener('resize', syncPopoverMetrics)
506
+ })
507
+ </script>
508
+
509
+ <style scoped>
510
+ .model-card {
511
+ display: flex;
512
+ flex-direction: column;
513
+ min-width: 0;
514
+ min-height: 0;
515
+ height: 100%;
516
+ padding: 20px 20px 0;
517
+ border: 1px solid var(--el-border-color-light, #e4e7ed);
518
+ border-radius: 0.625rem;
519
+ background: #fff;
520
+ box-sizing: border-box;
521
+ transition: box-shadow var(--el-transition-duration, 0.3s);
522
+ }
523
+
524
+ .model-card--no-actions {
525
+ padding-bottom: 16px;
526
+ }
527
+
528
+ .model-card:hover {
529
+ box-shadow: var(--el-box-shadow-light, 0 0 12px rgba(0, 0, 0, 0.12));
530
+ }
531
+
532
+ .model-card__header {
533
+ flex-shrink: 0;
534
+ display: flex;
535
+ flex-direction: column;
536
+ gap: 10px;
537
+ min-width: 0;
538
+ }
539
+
540
+ .model-card__title-block {
541
+ display: inline-flex;
542
+ align-items: baseline;
543
+ justify-content: center;
544
+ max-width: 100%;
545
+ min-width: 0;
546
+ width: 100%;
547
+ }
548
+
549
+ .model-card__title {
550
+ margin: 0;
551
+ flex: 0 1 auto;
552
+ min-width: 0;
553
+ font-size: 16px;
554
+ font-weight: 500;
555
+ line-height: 1.4;
556
+ color: #303133;
557
+ text-align: center;
558
+ overflow: hidden;
559
+ white-space: nowrap;
560
+ }
561
+
562
+ .model-card__ellipsis {
563
+ flex-shrink: 0;
564
+ margin: 0 0 0 4px;
565
+ padding: 0;
566
+ border: none;
567
+ background: none;
568
+ color: #c53355;
569
+ font-family: inherit;
570
+ font-size: inherit;
571
+ font-weight: 900;
572
+ letter-spacing: 0.08em;
573
+ line-height: inherit;
574
+ cursor: pointer;
575
+ }
576
+
577
+ .model-card__ellipsis:hover {
578
+ color: #a82b48;
579
+ }
580
+
581
+ .model-card__meta-row {
582
+ position: relative;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ min-height: 28px;
587
+ min-width: 0;
588
+ }
589
+
590
+ .model-card__tags {
591
+ display: flex;
592
+ flex-wrap: nowrap;
593
+ align-items: center;
594
+ justify-content: center;
595
+ gap: 8px;
596
+ width: 100%;
597
+ max-width: 100%;
598
+ min-width: 0;
599
+ padding: 0 32px;
600
+ box-sizing: border-box;
601
+ }
602
+
603
+ .model-card__tag-col {
604
+ flex: 1 1 0;
605
+ min-width: 0;
606
+ width: 0;
607
+ display: flex;
608
+ align-items: center;
609
+ justify-content: center;
610
+ }
611
+
612
+ .model-card__tag {
613
+ display: inline-flex;
614
+ align-items: center;
615
+ justify-content: center;
616
+ min-width: 0;
617
+ max-width: 100%;
618
+ height: 28px;
619
+ padding: 0 10px;
620
+ border: 1px solid transparent;
621
+ border-radius: 4px;
622
+ background: #f9fafb;
623
+ font-size: 14px;
624
+ font-weight: 400;
625
+ line-height: 26px;
626
+ color: #606266;
627
+ text-align: center;
628
+ box-sizing: border-box;
629
+ cursor: default;
630
+ transition:
631
+ color 0.2s ease,
632
+ background-color 0.2s ease;
633
+ }
634
+
635
+ .model-card__tag--industry {
636
+ width: 100%;
637
+ }
638
+
639
+ .model-card__tag--chapters {
640
+ width: 100%;
641
+ white-space: nowrap;
642
+ }
643
+
644
+ .model-card__tag-text {
645
+ flex: 0 1 auto;
646
+ min-width: 0;
647
+ overflow: hidden;
648
+ white-space: nowrap;
649
+ }
650
+
651
+ .model-card__tag .model-card__ellipsis {
652
+ flex-shrink: 0;
653
+ font-size: 14px;
654
+ line-height: 26px;
655
+ }
656
+
657
+ .model-card__tag:hover {
658
+ color: #c53355;
659
+ border-color: transparent;
660
+ background: #fdf0f2;
661
+ }
662
+
663
+ .model-card__view {
664
+ position: absolute;
665
+ top: 0;
666
+ right: 0;
667
+ flex-shrink: 0;
668
+ display: inline-flex;
669
+ align-items: center;
670
+ justify-content: center;
671
+ width: 28px;
672
+ height: 28px;
673
+ margin: 0;
674
+ padding: 0;
675
+ border: 0;
676
+ border-radius: 4px;
677
+ background: transparent;
678
+ color: #606266;
679
+ cursor: pointer;
680
+ }
681
+
682
+ .model-card__view:hover {
683
+ color: #c53355;
684
+ background: #fdf0f2;
685
+ }
686
+
687
+ .model-card__desc-block {
688
+ display: inline-flex;
689
+ align-items: flex-end;
690
+ justify-content: center;
691
+ flex-shrink: 0;
692
+ margin-top: 14px;
693
+ max-width: 100%;
694
+ min-width: 0;
695
+ width: 100%;
696
+ }
697
+
698
+ .model-card__desc {
699
+ margin: 0;
700
+ flex: 0 1 auto;
701
+ min-width: 0;
702
+ max-height: calc(14px * 1.7 * 3);
703
+ font-size: 14px;
704
+ font-weight: 400;
705
+ line-height: 1.7;
706
+ color: #606266;
707
+ text-align: center;
708
+ overflow: hidden;
709
+ word-break: break-word;
710
+ }
711
+
712
+ .model-card__desc-block .model-card__ellipsis {
713
+ flex-shrink: 0;
714
+ font-size: 14px;
715
+ line-height: 1.7;
716
+ }
717
+
718
+ .model-card__chapters {
719
+ display: flex;
720
+ flex-direction: column;
721
+ gap: 8px;
722
+ margin-top: 16px;
723
+ flex: 1;
724
+ min-height: 0;
725
+ }
726
+
727
+ .model-card__chapters-label {
728
+ flex-shrink: 0;
729
+ font-size: 14px;
730
+ font-weight: 500;
731
+ line-height: 1;
732
+ color: #606266;
733
+ }
734
+
735
+ .model-card__chapters-box {
736
+ flex: 1;
737
+ min-height: 0;
738
+ border: 1px solid #e5e7eb;
739
+ border-radius: 6px;
740
+ background: #fff;
741
+ box-sizing: border-box;
742
+ overflow: hidden;
743
+ transition: background-color 0.18s ease;
744
+ }
745
+
746
+ .model-card__chapters-box.is-scrollable {
747
+ background: #f9fafb;
748
+ }
749
+
750
+ .model-card__chapters-scroll {
751
+ height: 100%;
752
+ overflow: visible;
753
+ }
754
+
755
+ .model-card__chapters-scroll :deep(.el-scrollbar) {
756
+ height: 100%;
757
+ max-height: 100%;
758
+ overflow: visible;
759
+ }
760
+
761
+ .model-card__chapters-scroll :deep(.el-scrollbar__wrap) {
762
+ overflow-x: hidden !important;
763
+ overflow-y: auto !important;
764
+ max-height: 100% !important;
765
+ box-sizing: border-box !important;
766
+ scrollbar-width: none !important;
767
+ -ms-overflow-style: none !important;
768
+ }
769
+
770
+ .model-card__chapters-scroll :deep(.el-scrollbar__wrap::-webkit-scrollbar) {
771
+ width: 0 !important;
772
+ height: 0 !important;
773
+ display: none !important;
774
+ }
775
+
776
+ .model-card__chapters-scroll :deep(.el-scrollbar__view) {
777
+ box-sizing: border-box;
778
+ }
779
+
780
+ .model-card__chapters-scroll :deep(.el-scrollbar__bar.is-horizontal) {
781
+ display: none !important;
782
+ }
783
+
784
+ .model-card__chapters-scroll :deep(.el-scrollbar__bar.is-vertical) {
785
+ width: 8px !important;
786
+ right: 4px !important;
787
+ top: 2px !important;
788
+ bottom: 2px !important;
789
+ height: auto !important;
790
+ opacity: 0 !important;
791
+ pointer-events: none;
792
+ z-index: 10;
793
+ transition: opacity 0.18s ease;
794
+ }
795
+
796
+ .model-card__chapters-box.is-scrollable
797
+ .model-card__chapters-scroll
798
+ :deep(.el-scrollbar__bar.is-vertical) {
799
+ opacity: 1 !important;
800
+ pointer-events: auto;
801
+ }
802
+
803
+ .model-card__chapters-scroll :deep(.el-scrollbar__bar.is-vertical .el-scrollbar__thumb) {
804
+ display: block !important;
805
+ width: 8px !important;
806
+ height: 50px !important;
807
+ min-height: 50px !important;
808
+ border-radius: 4px !important;
809
+ background-color: #999 !important;
810
+ opacity: 1 !important;
811
+ }
812
+
813
+ .model-card__chapters-list {
814
+ margin: 0;
815
+ padding: 12px 14px;
816
+ list-style: none;
817
+ }
818
+
819
+ .model-card__chapters-list > li {
820
+ display: grid;
821
+ grid-template-columns: auto minmax(0, 1fr);
822
+ column-gap: 4px;
823
+ align-items: start;
824
+ font-size: 14px;
825
+ font-weight: 500;
826
+ line-height: 1.7;
827
+ color: #303133;
828
+ }
829
+
830
+ .model-card__chapters-list > li + li {
831
+ margin-top: 2px;
832
+ }
833
+
834
+ .model-card__chapter-no {
835
+ flex-shrink: 0;
836
+ white-space: nowrap;
837
+ }
838
+
839
+ .model-card__chapter-text {
840
+ min-width: 0;
841
+ word-break: break-word;
842
+ overflow-wrap: anywhere;
843
+ }
844
+
845
+ .model-card__meta {
846
+ flex-shrink: 0;
847
+ margin-top: 16px;
848
+ padding-bottom: 14px;
849
+ }
850
+
851
+ .model-card__meta-line {
852
+ margin: 0;
853
+ font-size: 14px;
854
+ font-weight: 500;
855
+ line-height: 1.7;
856
+ }
857
+
858
+ .model-card__meta-label {
859
+ color: #606266;
860
+ }
861
+
862
+ .model-card__meta-value {
863
+ color: #303133;
864
+ }
865
+
866
+ .model-card__meta-label + .model-card__meta-value {
867
+ margin-left: 4px;
868
+ }
869
+
870
+ .model-card__footer {
871
+ flex-shrink: 0;
872
+ display: flex;
873
+ align-items: center;
874
+ justify-content: space-between;
875
+ gap: 12px;
876
+ margin-top: auto;
877
+ padding: 12px 0 16px;
878
+ border-top: 1px solid #e5e7eb;
879
+ }
880
+
881
+ .model-card__footer-left {
882
+ display: inline-flex;
883
+ align-items: center;
884
+ gap: 8px;
885
+ min-width: 0;
886
+ }
887
+
888
+ .model-card__footer-sep {
889
+ flex-shrink: 0;
890
+ width: 1px;
891
+ height: 14px;
892
+ background: #e5e7eb;
893
+ }
894
+
895
+ .model-card__action {
896
+ display: inline-flex;
897
+ align-items: center;
898
+ justify-content: center;
899
+ gap: 6px;
900
+ width: 72px;
901
+ height: 28px;
902
+ padding: 0 8px;
903
+ border: 1px solid transparent;
904
+ border-radius: 4px;
905
+ background: transparent;
906
+ font-size: 15px;
907
+ font-weight: 500;
908
+ line-height: 1;
909
+ color: #606266;
910
+ cursor: pointer;
911
+ box-sizing: border-box;
912
+ transition:
913
+ color 0.2s ease,
914
+ border-color 0.2s ease,
915
+ background-color 0.2s ease;
916
+ }
917
+
918
+ .model-card__action:hover,
919
+ .model-card__action--danger:hover {
920
+ color: #c53355;
921
+ border-color: #c53355;
922
+ background: #fdf0f2;
923
+ }
924
+ </style>
925
+
926
+ <style>
927
+ .model-card-overflow-popover.el-popover.el-popper {
928
+ padding: 12px 14px;
929
+ border: 1px solid #ebeef5;
930
+ border-radius: 6px;
931
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
932
+ box-sizing: border-box;
933
+ word-break: break-word;
934
+ overflow-wrap: anywhere;
935
+ }
936
+
937
+ .model-card-overflow-popover__text {
938
+ display: block;
939
+ width: 100%;
940
+ max-height: none;
941
+ overflow: visible;
942
+ font-size: 14px;
943
+ font-weight: 400;
944
+ line-height: 1.6;
945
+ color: #303133;
946
+ white-space: normal;
947
+ word-break: break-word;
948
+ overflow-wrap: anywhere;
949
+ }
950
+
951
+ .model-card-preview-popover.el-popover.el-popper {
952
+ padding: 0 !important;
953
+ border: 1px solid #ebeef5;
954
+ border-radius: 6px;
955
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
956
+ overflow: hidden;
957
+ box-sizing: border-box;
958
+ }
959
+
960
+ .model-card-preview {
961
+ height: 100%;
962
+ max-height: inherit;
963
+ background: #fff;
964
+ box-sizing: border-box;
965
+ overflow: hidden;
966
+ transition: background-color 0.18s ease;
967
+ }
968
+
969
+ .model-card-preview.is-scrollable {
970
+ background: #f9fafb;
971
+ }
972
+
973
+ .model-card-preview__scroll {
974
+ height: 100%;
975
+ max-height: inherit;
976
+ }
977
+
978
+ .model-card-preview__scroll .el-scrollbar {
979
+ height: 100%;
980
+ max-height: inherit;
981
+ }
982
+
983
+ .model-card-preview__scroll .el-scrollbar__wrap {
984
+ overflow-x: hidden !important;
985
+ overflow-y: auto !important;
986
+ max-height: inherit !important;
987
+ box-sizing: border-box !important;
988
+ scrollbar-width: none !important;
989
+ -ms-overflow-style: none !important;
990
+ }
991
+
992
+ .model-card-preview__scroll .el-scrollbar__wrap::-webkit-scrollbar {
993
+ width: 0 !important;
994
+ height: 0 !important;
995
+ display: none !important;
996
+ }
997
+
998
+ .model-card-preview__scroll .el-scrollbar__bar.is-horizontal {
999
+ display: none !important;
1000
+ }
1001
+
1002
+ .model-card-preview__scroll .el-scrollbar__bar.is-vertical {
1003
+ width: 8px !important;
1004
+ right: 4px !important;
1005
+ top: 2px !important;
1006
+ bottom: 2px !important;
1007
+ height: auto !important;
1008
+ opacity: 0 !important;
1009
+ pointer-events: none;
1010
+ z-index: 10;
1011
+ transition: opacity 0.18s ease;
1012
+ }
1013
+
1014
+ .model-card-preview.is-scrollable .model-card-preview__scroll .el-scrollbar__bar.is-vertical {
1015
+ opacity: 1 !important;
1016
+ pointer-events: auto;
1017
+ }
1018
+
1019
+ .model-card-preview__scroll .el-scrollbar__bar.is-vertical .el-scrollbar__thumb {
1020
+ display: block !important;
1021
+ width: 8px !important;
1022
+ height: 50px !important;
1023
+ min-height: 50px !important;
1024
+ border-radius: 4px !important;
1025
+ background-color: #999 !important;
1026
+ opacity: 1 !important;
1027
+ }
1028
+
1029
+ .model-card-preview__inner {
1030
+ padding: 16px;
1031
+ box-sizing: border-box;
1032
+ }
1033
+
1034
+ .model-card-preview__title {
1035
+ margin: 0;
1036
+ font-size: 16px;
1037
+ font-weight: 500;
1038
+ line-height: 1.4;
1039
+ color: #303133;
1040
+ text-align: center;
1041
+ }
1042
+
1043
+ .model-card-preview__tags {
1044
+ display: flex;
1045
+ flex-direction: column;
1046
+ flex-wrap: nowrap;
1047
+ align-items: stretch;
1048
+ justify-content: flex-start;
1049
+ gap: 8px;
1050
+ margin-top: 10px;
1051
+ min-width: 0;
1052
+ width: 100%;
1053
+ }
1054
+
1055
+ .model-card-preview__tag {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ justify-content: center;
1059
+ width: 100%;
1060
+ min-width: 0;
1061
+ min-height: 28px;
1062
+ height: auto;
1063
+ padding: 4px 10px;
1064
+ border: 1px solid transparent;
1065
+ border-radius: 4px;
1066
+ background: #f9fafb;
1067
+ font-size: 14px;
1068
+ font-weight: 400;
1069
+ line-height: 1.4;
1070
+ color: #606266;
1071
+ text-align: center;
1072
+ box-sizing: border-box;
1073
+ white-space: normal;
1074
+ word-break: break-word;
1075
+ overflow-wrap: anywhere;
1076
+ transition:
1077
+ color 0.2s ease,
1078
+ border-color 0.2s ease,
1079
+ background-color 0.2s ease;
1080
+ }
1081
+
1082
+ .model-card-preview__tag--industry {
1083
+ display: block;
1084
+ width: 100%;
1085
+ white-space: normal;
1086
+ word-break: break-word;
1087
+ overflow-wrap: anywhere;
1088
+ text-align: center;
1089
+ }
1090
+
1091
+ .model-card-preview__tag--chapters {
1092
+ flex: none;
1093
+ display: flex;
1094
+ align-items: center;
1095
+ justify-content: center;
1096
+ white-space: nowrap;
1097
+ }
1098
+
1099
+ .model-card-preview__tag:hover {
1100
+ color: #c53355;
1101
+ border-color: transparent;
1102
+ background: #fdf0f2;
1103
+ }
1104
+
1105
+ .model-card-preview__desc {
1106
+ margin: 12px 0 0;
1107
+ font-size: 14px;
1108
+ font-weight: 400;
1109
+ line-height: 1.7;
1110
+ color: #606266;
1111
+ text-align: center;
1112
+ word-break: break-word;
1113
+ }
1114
+ </style>