aegon-dangerious 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 (32) hide show
  1. package/build-report/components/AiPromptGenerateDialog.vue +349 -0
  2. package/build-report/components/CapsuleScrollbar.vue +145 -0
  3. package/build-report/components/ChapterTitleScroll.vue +292 -0
  4. package/build-report/components/PromptField.vue +314 -0
  5. package/build-report/components/RecentReportsTable.vue +408 -0
  6. package/build-report/components/ReportChapterPreview.vue +437 -0
  7. package/build-report/components/ReportChapterWorkspace.vue +923 -0
  8. package/build-report/components/ReportCollapsibleSection.vue +132 -0
  9. package/build-report/components/ReportConfigPanel.vue +609 -0
  10. package/build-report/components/ReportGenerateSettingsPanel.vue +403 -0
  11. package/build-report/components/ReportGeneratingPanel.vue +908 -0
  12. package/build-report/components/ReportHistoryList.vue +157 -0
  13. package/build-report/components/ReportHistoryPanel.vue +140 -0
  14. package/build-report/components/ReportPromptPanel.vue +445 -0
  15. package/build-report/components/ReportSectionConfirmPopover.vue +232 -0
  16. package/build-report/components/ReportSourceFileSummary.vue +544 -0
  17. package/build-report/components/ReportSourcesMoreOverlay.vue +279 -0
  18. package/build-report/components/ReportSourcesPanel.vue +1055 -0
  19. package/build-report/components/ReportTemplateDetail.vue +176 -0
  20. package/build-report/components/ReportTemplateEditorFormPanel.vue +602 -0
  21. package/build-report/components/ReportTemplatePicker.vue +802 -0
  22. package/build-report/components/ReportTemplatePromptField.vue +314 -0
  23. package/build-report/components/ReportWorkflowSidebar.vue +104 -0
  24. package/build-report/components/reportEdit.vue +334 -0
  25. package/build-report/index.vue +646 -0
  26. package/build-report/report-list.vue +407 -0
  27. package/build-report/report-template-data.ts +248 -0
  28. package/build-report/styles/report-workflow-dialog.css +33 -0
  29. package/build-report/useReportWorkflow.ts +343 -0
  30. package/build-report/utils/report-edit-route.ts +109 -0
  31. package/build-report/utils/scroll.ts +361 -0
  32. package/package.json +12 -0
@@ -0,0 +1,292 @@
1
+ <template>
2
+ <component
3
+ :is="rootTag"
4
+ class="chapter-title-scroll"
5
+ :class="{
6
+ 'is-truncatable': isTruncatable,
7
+ 'is-animating': isAnimating,
8
+ 'is-dragging': isDragging,
9
+ 'is-active': active,
10
+ }"
11
+ @click="handleClick"
12
+ @wheel.prevent="handleWheel"
13
+ >
14
+ <div
15
+ ref="viewportRef"
16
+ class="chapter-title-scroll__viewport"
17
+ @pointerdown="handlePointerDown"
18
+ >
19
+ <span
20
+ ref="textRef"
21
+ class="chapter-title-scroll__text"
22
+ :style="{ transform: `translateX(${-offset}px)` }"
23
+ >
24
+ <template v-if="!showFull && isTruncatable">{{ truncatedPrefix }}</template>
25
+ <template v-else>{{ title }}</template>
26
+ </span>
27
+ <span
28
+ v-if="!showFull && isTruncatable"
29
+ class="chapter-title-scroll__ellipsis"
30
+ aria-hidden="true"
31
+ >...</span>
32
+ </div>
33
+ </component>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
38
+
39
+ const TITLE_MAX = 19
40
+ /** 自動滾動速度(px/s),較慢 */
41
+ const SCROLL_SPEED_PX_PER_SEC = 22
42
+ const END_PAUSE_MS = 280
43
+ const DRAG_CLICK_THRESHOLD_PX = 4
44
+
45
+ const props = defineProps({
46
+ title: { type: String, required: true },
47
+ rootTag: { type: String, default: 'li' },
48
+ active: { type: Boolean, default: false },
49
+ })
50
+
51
+ const viewportRef = ref(null as HTMLElement | null)
52
+ const textRef = ref(null as HTMLElement | null)
53
+ const offset = ref(0)
54
+ const showFull = ref(false)
55
+ const isAnimating = ref(false)
56
+ const isDragging = ref(false)
57
+
58
+ let animFrameId = 0
59
+ let endPauseTimer = 0
60
+ let dragStartX = 0
61
+ let dragStartOffset = 0
62
+ let pointerMoved = false
63
+ let activePointerMove: ((event: PointerEvent) => void) | null = null
64
+ let activePointerUp: ((event: PointerEvent) => void) | null = null
65
+
66
+ const isTruncatable = computed(() => props.title.length > TITLE_MAX)
67
+ const truncatedPrefix = computed(() =>
68
+ isTruncatable.value ? props.title.slice(0, TITLE_MAX) : props.title,
69
+ )
70
+
71
+ function getMaxOffset() {
72
+ const viewport = viewportRef.value
73
+ const text = textRef.value
74
+ if (!viewport || !text) return 0
75
+ return Math.max(0, text.scrollWidth - viewport.clientWidth)
76
+ }
77
+
78
+ function clampOffset(value: number) {
79
+ return Math.min(getMaxOffset(), Math.max(0, value))
80
+ }
81
+
82
+ function cancelAnimation() {
83
+ if (animFrameId) {
84
+ cancelAnimationFrame(animFrameId)
85
+ animFrameId = 0
86
+ }
87
+ if (endPauseTimer) {
88
+ clearTimeout(endPauseTimer)
89
+ endPauseTimer = 0
90
+ }
91
+ isAnimating.value = false
92
+ }
93
+
94
+ function removePointerListeners() {
95
+ if (activePointerMove) {
96
+ window.removeEventListener('pointermove', activePointerMove)
97
+ activePointerMove = null
98
+ }
99
+ if (activePointerUp) {
100
+ window.removeEventListener('pointerup', activePointerUp)
101
+ window.removeEventListener('pointercancel', activePointerUp)
102
+ activePointerUp = null
103
+ }
104
+ }
105
+
106
+ function resetToInitial() {
107
+ cancelAnimation()
108
+ removePointerListeners()
109
+ isDragging.value = false
110
+ offset.value = 0
111
+ showFull.value = false
112
+ }
113
+
114
+ async function ensureFullMode() {
115
+ if (!showFull.value) {
116
+ showFull.value = true
117
+ offset.value = 0
118
+ await nextTick()
119
+ }
120
+ }
121
+
122
+ async function startAutoScroll() {
123
+ if (!isTruncatable.value || isAnimating.value) return
124
+
125
+ cancelAnimation()
126
+ showFull.value = true
127
+ offset.value = 0
128
+ await nextTick()
129
+
130
+ const maxOffset = getMaxOffset()
131
+ if (maxOffset <= 0) {
132
+ resetToInitial()
133
+ return
134
+ }
135
+
136
+ isAnimating.value = true
137
+ const startTime = performance.now()
138
+
139
+ const tick = (now: number) => {
140
+ const elapsedSec = (now - startTime) / 1000
141
+ const nextOffset = Math.min(maxOffset, elapsedSec * SCROLL_SPEED_PX_PER_SEC)
142
+ offset.value = nextOffset
143
+
144
+ if (nextOffset >= maxOffset) {
145
+ endPauseTimer = window.setTimeout(() => {
146
+ resetToInitial()
147
+ }, END_PAUSE_MS)
148
+ return
149
+ }
150
+
151
+ animFrameId = requestAnimationFrame(tick)
152
+ }
153
+
154
+ animFrameId = requestAnimationFrame(tick)
155
+ }
156
+
157
+ function handleClick() {
158
+ if (!isTruncatable.value || isAnimating.value || pointerMoved) {
159
+ pointerMoved = false
160
+ return
161
+ }
162
+ resetToInitial()
163
+ void nextTick(() => {
164
+ void startAutoScroll()
165
+ })
166
+ }
167
+
168
+ function handleWheel(event: WheelEvent) {
169
+ if (!isTruncatable.value) return
170
+
171
+ cancelAnimation()
172
+
173
+ void ensureFullMode().then(() => {
174
+ const delta = (event.deltaY !== 0 ? event.deltaY : event.deltaX) * 0.6
175
+ offset.value = clampOffset(offset.value + delta)
176
+ })
177
+ }
178
+
179
+ function handlePointerDown(event: PointerEvent) {
180
+ if (!isTruncatable.value || event.button !== 0) return
181
+
182
+ event.preventDefault()
183
+ cancelAnimation()
184
+
185
+ void ensureFullMode().then(() => {
186
+ isDragging.value = true
187
+ pointerMoved = false
188
+ dragStartX = event.clientX
189
+ dragStartOffset = offset.value
190
+
191
+ activePointerMove = (moveEvent: PointerEvent) => {
192
+ const delta = dragStartX - moveEvent.clientX
193
+ if (Math.abs(delta) > DRAG_CLICK_THRESHOLD_PX) {
194
+ pointerMoved = true
195
+ }
196
+ offset.value = clampOffset(dragStartOffset + delta)
197
+ }
198
+
199
+ activePointerUp = () => {
200
+ isDragging.value = false
201
+ removePointerListeners()
202
+ if (pointerMoved) {
203
+ window.setTimeout(() => {
204
+ pointerMoved = false
205
+ }, 0)
206
+ }
207
+ }
208
+
209
+ window.addEventListener('pointermove', activePointerMove)
210
+ window.addEventListener('pointerup', activePointerUp)
211
+ window.addEventListener('pointercancel', activePointerUp)
212
+ })
213
+ }
214
+
215
+ onBeforeUnmount(() => {
216
+ resetToInitial()
217
+ })
218
+ </script>
219
+
220
+ <style scoped>
221
+ .chapter-title-scroll {
222
+ --cts-lh-chapter: clamp(18px, calc(22 * 100vw / 1920), 22px);
223
+ --cts-fs-chapter: clamp(12px, calc(14 * 100vw / 1920), 14px);
224
+ --cts-fs-chapter-ellipsis: clamp(14px, calc(16 * 100vw / 1920), 16px);
225
+ height: var(--cts-lh-chapter);
226
+ line-height: var(--cts-lh-chapter);
227
+ list-style: none;
228
+ font-size: var(--cts-fs-chapter);
229
+ color: #606266;
230
+ cursor: default;
231
+ user-select: none;
232
+ }
233
+
234
+ .chapter-title-scroll.is-truncatable {
235
+ cursor: grab;
236
+ }
237
+
238
+ .chapter-title-scroll.is-dragging {
239
+ cursor: grabbing;
240
+ }
241
+
242
+ .chapter-title-scroll__viewport {
243
+ position: relative;
244
+ overflow: hidden;
245
+ height: var(--cts-lh-chapter);
246
+ line-height: var(--cts-lh-chapter);
247
+ scrollbar-width: none;
248
+ -ms-overflow-style: none;
249
+ touch-action: none;
250
+ }
251
+
252
+ .chapter-title-scroll__viewport::-webkit-scrollbar {
253
+ display: none;
254
+ width: 0;
255
+ height: 0;
256
+ }
257
+
258
+ .chapter-title-scroll__text {
259
+ display: inline-block;
260
+ white-space: nowrap;
261
+ will-change: transform;
262
+ color: #606266;
263
+ }
264
+
265
+ .chapter-title-scroll.is-dragging .chapter-title-scroll__text {
266
+ transition: none;
267
+ }
268
+
269
+ .chapter-title-scroll.is-active,
270
+ .chapter-title-scroll.is-active .chapter-title-scroll__text {
271
+ color: #c53355;
272
+ font-weight: 600;
273
+ }
274
+
275
+ .chapter-title-scroll.is-active .chapter-title-scroll__ellipsis {
276
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #fff 45%);
277
+ }
278
+
279
+ .chapter-title-scroll__ellipsis {
280
+ position: absolute;
281
+ right: 0;
282
+ top: 0;
283
+ padding-left: 8px;
284
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #fff 45%);
285
+ color: #c53355;
286
+ font-weight: 900;
287
+ font-size: var(--cts-fs-chapter-ellipsis);
288
+ line-height: var(--cts-lh-chapter);
289
+ letter-spacing: 0.05em;
290
+ pointer-events: none;
291
+ }
292
+ </style>
@@ -0,0 +1,314 @@
1
+ <template>
2
+ <label class="gen-ai-prompt-field" :class="{ 'gen-ai-prompt-field--row': layout === 'row' }" :style="fieldStyle">
3
+ <span class="gen-ai-prompt-field__label">
4
+ {{ label }}<span v-if="required" class="gen-ai-prompt-field__required">*</span>
5
+ </span>
6
+ <div class="gen-ai-prompt-field__control" :style="controlStyle">
7
+ <div class="gen-ai-prompt-field__box" :class="{ 'gen-ai-prompt-field__box--constrained': isConstrained }"
8
+ :style="boxStyle">
9
+ <div class="gen-ai-prompt-field__body" :class="{ 'gen-ai-prompt-field__body--scroll': isConstrained }">
10
+ <el-input :model-value="modelValue" type="textarea" :autosize="textareaAutosize"
11
+ :rows="isConstrained ? rows : undefined" :maxlength="maxlength" :placeholder="placeholder"
12
+ :disabled="disabled" resize="none" :class="[
13
+ 'gen-ai-prompt-field__textarea',
14
+ { 'gen-ai-prompt-field__textarea--fill': isConstrained },
15
+ ]" @update:model-value="emit('update:modelValue', $event)" />
16
+ </div>
17
+ <div class="gen-ai-prompt-field__toolbar">
18
+ <button type="button" class="gen-ai-prompt-field__ai" :disabled="disabled" @click="openAiDialog">
19
+ AI+
20
+ </button>
21
+ </div>
22
+ </div>
23
+ <p class="gen-ai-prompt-field__count">{{ wordCountText }}</p>
24
+ </div>
25
+ </label>
26
+
27
+ <AiPromptGenerateDialog v-model="aiDialogVisible" :initial-title="chapterTitle"
28
+ :initial-description="chapterDescription" @confirm="handleAiConfirm" />
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed, ref, type StyleValue } from 'vue';
33
+ import AiPromptGenerateDialog from './AiPromptGenerateDialog.vue'
34
+
35
+ const props = withDefaults(
36
+ defineProps<{
37
+ modelValue: string
38
+ label?: string
39
+ required?: boolean
40
+ maxlength?: number
41
+ rows?: number
42
+ maxRows?: number
43
+ chapterTitle?: string
44
+ chapterDescription?: string
45
+ /** 外层可指定宽度,默认随父级撑满 */
46
+ width?: string
47
+ /** 外层可指定整体高度(含底栏),内部文字区域滚动 */
48
+ height?: string
49
+ minHeight?: string
50
+ /** 外层可指定最大高度,超出后内部滚动 */
51
+ maxHeight?: string
52
+ placeholder?: string
53
+ disabled?: boolean
54
+ layout?: 'row' | 'stack'
55
+ labelWidth?: string
56
+ fieldGap?: string
57
+ rootStyle?: StyleValue
58
+ }>(),
59
+ {
60
+ label: 'Prompt',
61
+ required: true,
62
+ maxlength: 1300,
63
+ rows: 8,
64
+ maxRows: 16,
65
+ placeholder: '',
66
+ disabled: false,
67
+ layout: 'row',
68
+ chapterTitle: '',
69
+ chapterDescription: '',
70
+ },
71
+ )
72
+
73
+ const emit = defineEmits<{
74
+ 'update:modelValue': [value: string]
75
+ 'ai-assist': [prompt: string]
76
+ }>()
77
+
78
+ const aiDialogVisible = ref(false)
79
+
80
+ function openAiDialog() {
81
+ if (props.disabled) return
82
+ aiDialogVisible.value = true
83
+ }
84
+
85
+ function handleAiConfirm(prompt: string) {
86
+ emit('update:modelValue', prompt)
87
+ emit('ai-assist', prompt)
88
+ }
89
+
90
+ /** 指定 height / maxHeight 时改为固定容器 + 内部滚动 */
91
+ const isConstrained = computed(() => !!(props.height || props.maxHeight))
92
+
93
+ const textareaAutosize = computed(() => {
94
+ if (isConstrained.value) return false
95
+ return {
96
+ minRows: props.rows,
97
+ maxRows: props.maxRows,
98
+ }
99
+ })
100
+
101
+ const wordCountText = computed(() => `${props.modelValue.length} / ${props.maxlength}`)
102
+
103
+ function pickSizeStyle(
104
+ width?: string,
105
+ height?: string,
106
+ minHeight?: string,
107
+ maxHeight?: string,
108
+ ): StyleValue {
109
+ const style: Record<string, string> = {}
110
+ if (width) style.width = width
111
+ if (height) style.height = height
112
+ if (minHeight) style.minHeight = minHeight
113
+ if (maxHeight) style.maxHeight = maxHeight
114
+ return style
115
+ }
116
+
117
+ const fieldStyle = computed((): StyleValue => {
118
+ const style: Record<string, string> = {}
119
+ if (props.labelWidth) style['--gen-ai-prompt-field-label-w'] = props.labelWidth
120
+ if (props.fieldGap) style['--gen-ai-prompt-field-gap'] = props.fieldGap
121
+ if (!props.rootStyle) return style
122
+ if (typeof props.rootStyle === 'string') return [style, props.rootStyle]
123
+ if (Array.isArray(props.rootStyle)) return [style, ...props.rootStyle]
124
+ return { ...style, ...props.rootStyle }
125
+ })
126
+
127
+ const controlStyle = computed(() => pickSizeStyle(props.width))
128
+ const boxStyle = computed(() =>
129
+ pickSizeStyle(props.width, props.height, props.minHeight, props.maxHeight),
130
+ )
131
+
132
+ export interface PromptFieldLayoutBind {
133
+ labelWidth?: string
134
+ fieldGap?: string
135
+ width?: string
136
+ height?: string
137
+ minHeight?: string
138
+ maxHeight?: string
139
+ rootStyle?: StyleValue
140
+ }
141
+ </script>
142
+
143
+ <style scoped>
144
+ .gen-ai-prompt-field {
145
+ margin: 0;
146
+ }
147
+
148
+ .gen-ai-prompt-field--row {
149
+ --gen-ai-prompt-field-label-w: 100px;
150
+ --gen-ai-prompt-field-gap: 16px;
151
+ display: grid;
152
+ grid-template-columns: var(--gen-ai-prompt-field-label-w) minmax(0, 1fr);
153
+ column-gap: var(--gen-ai-prompt-field-gap);
154
+ align-items: start;
155
+ }
156
+
157
+ .gen-ai-prompt-field__label {
158
+ flex-shrink: 0;
159
+ padding-top: 8px;
160
+ font-size: 14px;
161
+ font-weight: 500;
162
+ font-family: Arial, sans-serif;
163
+ color: #303133;
164
+ white-space: nowrap;
165
+ }
166
+
167
+ .gen-ai-prompt-field__required {
168
+ margin-left: 2px;
169
+ color: #f56c6c;
170
+ }
171
+
172
+ .gen-ai-prompt-field__control {
173
+ min-width: 0;
174
+ width: 100%;
175
+ }
176
+
177
+ .gen-ai-prompt-field__box {
178
+ display: flex;
179
+ flex-direction: column;
180
+ width: 100%;
181
+ border: 1px solid #dcdfe6;
182
+ border-radius: 8px;
183
+ background: #fff;
184
+ overflow: hidden;
185
+ transition: border-color 0.2s ease;
186
+ }
187
+
188
+ .gen-ai-prompt-field__box--constrained {
189
+ min-height: 0;
190
+ }
191
+
192
+ .gen-ai-prompt-field__box:focus-within {
193
+ border-color: #c0c4cc;
194
+ }
195
+
196
+ .gen-ai-prompt-field__body {
197
+ flex: 1 1 auto;
198
+ min-height: 0;
199
+ /* 右侧 1px:滚动条贴边框;左侧 12px 与 textarea 内边距共同保证文字两端留白一致 */
200
+ padding: 12px 2px 4px 12px;
201
+ }
202
+
203
+ .gen-ai-prompt-field__body--scroll {
204
+ display: flex;
205
+ flex-direction: column;
206
+ overflow: hidden;
207
+ }
208
+
209
+ .gen-ai-prompt-field__textarea {
210
+ width: 100%;
211
+ }
212
+
213
+ .gen-ai-prompt-field__textarea :deep(.el-textarea),
214
+ .gen-ai-prompt-field__textarea :deep(.el-textarea__wrapper) {
215
+ padding: 0;
216
+ margin: 0;
217
+ box-shadow: none;
218
+ background: transparent;
219
+ }
220
+
221
+ .gen-ai-prompt-field__textarea :deep(.el-textarea__inner) {
222
+ padding: 0 10px 0 0;
223
+ margin: 0;
224
+ border: none;
225
+ box-shadow: none;
226
+ background: transparent;
227
+ font-size: 14px;
228
+ line-height: 1.6;
229
+ color: #303133;
230
+ resize: none;
231
+ box-sizing: border-box;
232
+ }
233
+
234
+ .gen-ai-prompt-field__textarea :deep(.el-textarea__inner:focus) {
235
+ box-shadow: none;
236
+ }
237
+
238
+ .gen-ai-prompt-field__textarea--fill {
239
+ flex: 1 1 auto;
240
+ min-height: 0;
241
+ height: 100%;
242
+ display: flex;
243
+ flex-direction: column;
244
+ }
245
+
246
+ .gen-ai-prompt-field__textarea--fill :deep(.el-textarea) {
247
+ flex: 1 1 auto;
248
+ min-height: 0;
249
+ height: 100%;
250
+ display: flex;
251
+ flex-direction: column;
252
+ }
253
+
254
+ .gen-ai-prompt-field__textarea--fill :deep(.el-textarea__inner) {
255
+ flex: 1 1 auto;
256
+ min-height: 0 !important;
257
+ height: 100% !important;
258
+ overflow-y: auto !important;
259
+ }
260
+
261
+ .gen-ai-prompt-field__textarea :deep(.el-textarea__inner::-webkit-scrollbar) {
262
+ width: 6px;
263
+ }
264
+
265
+ .gen-ai-prompt-field__textarea :deep(.el-textarea__inner::-webkit-scrollbar-thumb) {
266
+ border-radius: 3px;
267
+ background: #c0c4cc;
268
+ }
269
+
270
+ .gen-ai-prompt-field__toolbar {
271
+ flex-shrink: 0;
272
+ display: flex;
273
+ justify-content: flex-end;
274
+ align-items: center;
275
+ padding: 0 12px 10px;
276
+ }
277
+
278
+ .gen-ai-prompt-field__ai {
279
+ flex-shrink: 0;
280
+ width: 40px;
281
+ height: 40px;
282
+ padding: 0;
283
+ border: none;
284
+ border-radius: 50%;
285
+ background: linear-gradient(135deg, #7b61ff 0%, #5b8def 100%);
286
+ color: #fff;
287
+ font-size: 12px;
288
+ font-weight: 700;
289
+ line-height: 1;
290
+ cursor: pointer;
291
+ box-shadow: 0 2px 8px rgba(91, 141, 239, 0.35);
292
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
293
+ }
294
+
295
+ .gen-ai-prompt-field__ai:hover:not(:disabled) {
296
+ transform: translateY(-1px);
297
+ box-shadow: 0 4px 12px rgba(91, 141, 239, 0.45);
298
+ }
299
+
300
+ .gen-ai-prompt-field__ai:disabled {
301
+ opacity: 0.45;
302
+ cursor: not-allowed;
303
+ transform: none;
304
+ box-shadow: 0 2px 8px rgba(91, 141, 239, 0.35);
305
+ }
306
+
307
+ .gen-ai-prompt-field__count {
308
+ margin: 4px 0 0;
309
+ text-align: right;
310
+ font-size: 12px;
311
+ color: #909399;
312
+ line-height: 1;
313
+ }
314
+ </style>