@huyooo/ai-chat-frontend-vue 0.1.6 → 0.1.7

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 (159) hide show
  1. package/README.md +367 -0
  2. package/dist/adapter.d.ts +7 -7
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/components/ChatPanel.vue.d.ts +120 -9
  5. package/dist/components/ChatPanel.vue.d.ts.map +1 -1
  6. package/dist/components/common/ConfirmDialog.vue.d.ts +30 -0
  7. package/dist/components/common/ConfirmDialog.vue.d.ts.map +1 -0
  8. package/dist/components/common/CopyButton.vue.d.ts +18 -0
  9. package/dist/components/common/CopyButton.vue.d.ts.map +1 -0
  10. package/dist/components/common/IndexingSettings.vue.d.ts +3 -0
  11. package/dist/components/common/IndexingSettings.vue.d.ts.map +1 -0
  12. package/dist/components/common/SettingsPanel.vue.d.ts +16 -0
  13. package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -0
  14. package/dist/components/common/Toast.vue.d.ts +18 -0
  15. package/dist/components/common/Toast.vue.d.ts.map +1 -0
  16. package/dist/components/common/ToggleSwitch.vue.d.ts +10 -0
  17. package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -0
  18. package/dist/components/{chat/ui → header}/ChatHeader.vue.d.ts +5 -3
  19. package/dist/components/header/ChatHeader.vue.d.ts.map +1 -0
  20. package/dist/components/input/AtFilePicker.vue.d.ts +21 -0
  21. package/dist/components/input/AtFilePicker.vue.d.ts.map +1 -0
  22. package/dist/components/{ChatInput.vue.d.ts → input/ChatInput.vue.d.ts} +16 -14
  23. package/dist/components/input/ChatInput.vue.d.ts.map +1 -0
  24. package/dist/components/input/DropdownSelector.vue.d.ts +42 -0
  25. package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -0
  26. package/dist/components/input/ImagePreviewModal.vue.d.ts +17 -0
  27. package/dist/components/input/ImagePreviewModal.vue.d.ts.map +1 -0
  28. package/dist/components/input/at-views/AtBranchView.vue.d.ts +18 -0
  29. package/dist/components/input/at-views/AtBranchView.vue.d.ts.map +1 -0
  30. package/dist/components/input/at-views/AtBrowserView.vue.d.ts +18 -0
  31. package/dist/components/input/at-views/AtBrowserView.vue.d.ts.map +1 -0
  32. package/dist/components/input/at-views/AtChatsView.vue.d.ts +18 -0
  33. package/dist/components/input/at-views/AtChatsView.vue.d.ts.map +1 -0
  34. package/dist/components/input/at-views/AtDocsView.vue.d.ts +18 -0
  35. package/dist/components/input/at-views/AtDocsView.vue.d.ts.map +1 -0
  36. package/dist/components/input/at-views/AtFilesView.vue.d.ts +23 -0
  37. package/dist/components/input/at-views/AtFilesView.vue.d.ts.map +1 -0
  38. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts +18 -0
  39. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts.map +1 -0
  40. package/dist/components/message/MessageBubble.vue.d.ts +45 -0
  41. package/dist/components/message/MessageBubble.vue.d.ts.map +1 -0
  42. package/dist/components/message/PartsRenderer.vue.d.ts +15 -0
  43. package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -0
  44. package/dist/components/message/WelcomeMessage.vue.d.ts +14 -0
  45. package/dist/components/message/WelcomeMessage.vue.d.ts.map +1 -0
  46. package/dist/components/message/blocks/CodeBlock.vue.d.ts +11 -0
  47. package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +1 -0
  48. package/dist/components/{chat/SearchResultBlock.vue.d.ts → message/blocks/TextBlock.vue.d.ts} +3 -4
  49. package/dist/components/message/blocks/TextBlock.vue.d.ts.map +1 -0
  50. package/dist/components/message/blocks/index.d.ts +6 -0
  51. package/dist/components/message/blocks/index.d.ts.map +1 -0
  52. package/dist/components/message/parts/CollapsibleCard.vue.d.ts +45 -0
  53. package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -0
  54. package/dist/components/{chat/ToolCallBlock.vue.d.ts → message/parts/ErrorPart.vue.d.ts} +4 -5
  55. package/dist/components/message/parts/ErrorPart.vue.d.ts.map +1 -0
  56. package/dist/components/{chat/ThinkingBlock.vue.d.ts → message/parts/ImagePart.vue.d.ts} +3 -3
  57. package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -0
  58. package/dist/components/message/parts/SearchPart.vue.d.ts +12 -0
  59. package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -0
  60. package/dist/components/{chat/messages/ExecutionSteps.vue.d.ts → message/parts/TextPart.vue.d.ts} +2 -9
  61. package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -0
  62. package/dist/components/message/parts/ThinkingPart.vue.d.ts +12 -0
  63. package/dist/components/message/parts/ThinkingPart.vue.d.ts.map +1 -0
  64. package/dist/components/message/parts/ToolCallPart.vue.d.ts +19 -0
  65. package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -0
  66. package/dist/components/message/parts/ToolResultPart.vue.d.ts +14 -0
  67. package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +1 -0
  68. package/dist/components/message/parts/index.d.ts +12 -0
  69. package/dist/components/message/parts/index.d.ts.map +1 -0
  70. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +4 -0
  71. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +1 -0
  72. package/dist/components/message/tool-results/SearchResults.vue.d.ts +4 -0
  73. package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +1 -0
  74. package/dist/components/message/tool-results/WeatherCard.vue.d.ts +4 -0
  75. package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +1 -0
  76. package/dist/components/message/tool-results/index.d.ts +7 -0
  77. package/dist/components/message/tool-results/index.d.ts.map +1 -0
  78. package/dist/components/message/welcome-types.d.ts +28 -0
  79. package/dist/components/message/welcome-types.d.ts.map +1 -0
  80. package/dist/composables/useChat.d.ts +99 -44
  81. package/dist/composables/useChat.d.ts.map +1 -1
  82. package/dist/composables/useImageUpload.d.ts +55 -0
  83. package/dist/composables/useImageUpload.d.ts.map +1 -0
  84. package/dist/index.d.ts +25 -26
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +55871 -1252
  87. package/dist/style.css +1 -1
  88. package/dist/types/index.d.ts +113 -53
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/utils/fileIcon.d.ts +13 -0
  91. package/dist/utils/fileIcon.d.ts.map +1 -0
  92. package/package.json +12 -6
  93. package/src/adapter.ts +12 -70
  94. package/src/components/ChatPanel.vue +329 -110
  95. package/src/components/common/ConfirmDialog.vue +208 -0
  96. package/src/components/common/CopyButton.vue +71 -0
  97. package/src/components/common/IndexingSettings.vue +580 -0
  98. package/src/components/common/SettingsPanel.vue +293 -0
  99. package/src/components/common/Toast.vue +90 -0
  100. package/src/components/common/ToggleSwitch.vue +75 -0
  101. package/src/components/{chat/ui → header}/ChatHeader.vue +170 -93
  102. package/src/components/input/AtFilePicker.vue +657 -0
  103. package/src/components/input/ChatInput.vue +653 -0
  104. package/src/components/input/DropdownSelector.vue +322 -0
  105. package/src/components/input/ImagePreviewModal.vue +238 -0
  106. package/src/components/input/at-views/AtBranchView.vue +63 -0
  107. package/src/components/input/at-views/AtBrowserView.vue +63 -0
  108. package/src/components/input/at-views/AtChatsView.vue +63 -0
  109. package/src/components/input/at-views/AtDocsView.vue +63 -0
  110. package/src/components/input/at-views/AtFilesView.vue +255 -0
  111. package/src/components/input/at-views/AtTerminalsView.vue +63 -0
  112. package/src/components/message/ContentRenderer.vue +61 -0
  113. package/src/components/message/MessageBubble.vue +411 -0
  114. package/src/components/message/PartsRenderer.vue +101 -0
  115. package/src/components/message/ToolResultRenderer.vue +27 -0
  116. package/src/components/message/WelcomeMessage.vue +308 -0
  117. package/src/components/message/blocks/CodeBlock.vue +113 -0
  118. package/src/components/message/blocks/TextBlock.vue +21 -0
  119. package/src/components/message/blocks/index.ts +6 -0
  120. package/src/components/message/parts/CollapsibleCard.vue +135 -0
  121. package/src/components/message/parts/ErrorPart.vue +51 -0
  122. package/src/components/message/parts/ImagePart.vue +98 -0
  123. package/src/components/message/parts/SearchPart.vue +101 -0
  124. package/src/components/message/parts/TextPart.vue +28 -0
  125. package/src/components/message/parts/ThinkingPart.vue +54 -0
  126. package/src/components/message/parts/ToolCallPart.vue +460 -0
  127. package/src/components/message/parts/ToolResultPart.vue +78 -0
  128. package/src/components/message/parts/index.ts +13 -0
  129. package/src/components/message/tool-results/DefaultToolResult.vue +43 -0
  130. package/src/components/message/tool-results/SearchResults.vue +133 -0
  131. package/src/components/message/tool-results/WeatherCard.vue +139 -0
  132. package/src/components/message/tool-results/index.ts +7 -0
  133. package/src/components/message/welcome-types.ts +47 -0
  134. package/src/composables/useChat.ts +807 -155
  135. package/src/composables/useImageUpload.ts +228 -0
  136. package/src/index.ts +93 -46
  137. package/src/styles.css +47 -0
  138. package/src/types/index.ts +146 -98
  139. package/src/utils/fileIcon.ts +49 -0
  140. package/dist/components/ChatInput.vue.d.ts.map +0 -1
  141. package/dist/components/chat/SearchResultBlock.vue.d.ts.map +0 -1
  142. package/dist/components/chat/ThinkingBlock.vue.d.ts.map +0 -1
  143. package/dist/components/chat/ToolCallBlock.vue.d.ts.map +0 -1
  144. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts.map +0 -1
  145. package/dist/components/chat/messages/MessageBubble.vue.d.ts +0 -28
  146. package/dist/components/chat/messages/MessageBubble.vue.d.ts.map +0 -1
  147. package/dist/components/chat/ui/ChatHeader.vue.d.ts.map +0 -1
  148. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts +0 -7
  149. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts.map +0 -1
  150. package/dist/preload/preload.d.ts +0 -6
  151. package/dist/preload/preload.d.ts.map +0 -1
  152. package/src/components/ChatInput.vue +0 -649
  153. package/src/components/chat/SearchResultBlock.vue +0 -155
  154. package/src/components/chat/ThinkingBlock.vue +0 -109
  155. package/src/components/chat/ToolCallBlock.vue +0 -213
  156. package/src/components/chat/messages/ExecutionSteps.vue +0 -281
  157. package/src/components/chat/messages/MessageBubble.vue +0 -272
  158. package/src/components/chat/ui/WelcomeMessage.vue +0 -135
  159. package/src/preload/preload.ts +0 -79
@@ -0,0 +1,322 @@
1
+ <template>
2
+ <div
3
+ ref="selectorRef"
4
+ :class="['dropdown-selector', { disabled: disabled }]"
5
+ @click.stop="toggleMenu"
6
+ >
7
+ <Icon v-if="currentOption?.icon" :icon="currentOption.icon" width="14" />
8
+ <span class="selector-text">{{ currentOption?.label || placeholder }}</span>
9
+ <Icon icon="lucide:chevron-down" width="14" class="chevron" />
10
+
11
+ <div v-if="menuOpen" :class="['dropdown-menu', `dropdown-${dropdownDirection}`, align === 'right' && 'dropdown-align-right']" @click.stop>
12
+ <!-- 空状态 -->
13
+ <div v-if="!hasOptions" class="dropdown-empty">暂无数据</div>
14
+
15
+ <!-- 分组模式:使用后端返回的分组数据 -->
16
+ <template v-else-if="groupedOptions && Object.keys(groupedOptions).length > 0">
17
+ <template v-for="(groupItems, groupName) in groupedOptions" :key="groupName">
18
+ <div class="group-title">{{ groupName }}</div>
19
+ <button
20
+ v-for="opt in groupItems"
21
+ :key="opt.value"
22
+ :class="['dropdown-item', { active: value === opt.value }]"
23
+ @click="selectOption(opt.value)"
24
+ >
25
+ <Icon v-if="opt.icon" :icon="opt.icon" width="14" />
26
+ <span class="option-label">{{ opt.label }}</span>
27
+ <span class="option-right">
28
+ <Icon v-if="value === opt.value" icon="lucide:check" width="14" class="check-icon" />
29
+ </span>
30
+ </button>
31
+ </template>
32
+ </template>
33
+
34
+ <!-- 扁平列表模式:无分组数据时使用 -->
35
+ <template v-else>
36
+ <button
37
+ v-for="opt in sortedOptions"
38
+ :key="opt.value"
39
+ :class="['dropdown-item', { active: value === opt.value }]"
40
+ @click="selectOption(opt.value)"
41
+ >
42
+ <Icon v-if="opt.icon" :icon="opt.icon" width="14" />
43
+ <span class="option-label">{{ opt.label }}</span>
44
+ <span class="option-right">
45
+ <Icon v-if="value === opt.value" icon="lucide:check" width="14" class="check-icon" />
46
+ </span>
47
+ </button>
48
+ </template>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { ref, computed, onMounted, onUnmounted } from 'vue';
55
+ import { Icon } from '@iconify/vue';
56
+
57
+ /** 下拉选项 */
58
+ export interface DropdownOption {
59
+ value: string;
60
+ label: string;
61
+ icon?: string;
62
+ /** 分组名称(由后端决定,前端只负责渲染,必填) */
63
+ group?: string; // 在 DropdownOption 中保持可选,因为不是所有选项都需要分组
64
+ }
65
+
66
+ /** 分组后的选项(由后端提供) */
67
+ export interface GroupedOptions {
68
+ [groupName: string]: DropdownOption[];
69
+ }
70
+
71
+ const props = withDefaults(
72
+ defineProps<{
73
+ /** 当前选中的值 */
74
+ value: string;
75
+ /** 选项列表(扁平列表) */
76
+ options?: DropdownOption[];
77
+ /** 分组后的选项(优先级高于 options,由后端提供) */
78
+ groupedOptions?: GroupedOptions;
79
+ /** 占位符文本 */
80
+ placeholder?: string;
81
+ /** 容器引用,用于计算下拉方向 */
82
+ containerRef?: HTMLElement | null;
83
+ /** 是否禁用 */
84
+ disabled?: boolean;
85
+ /** 下拉菜单对齐方式 */
86
+ align?: 'left' | 'right';
87
+ }>(),
88
+ {
89
+ placeholder: '请选择',
90
+ containerRef: null,
91
+ disabled: false,
92
+ align: 'left',
93
+ }
94
+ );
95
+
96
+ const emit = defineEmits<{
97
+ select: [value: string];
98
+ }>();
99
+
100
+ const selectorRef = ref<HTMLDivElement | null>(null);
101
+ const menuOpen = ref(false);
102
+ const dropdownDirection = ref<'up' | 'down'>('up');
103
+
104
+ // 检查是否有选项
105
+ const hasOptions = computed(() => {
106
+ if (props.groupedOptions && Object.keys(props.groupedOptions).length > 0) {
107
+ return Object.values(props.groupedOptions).some(group => group.length > 0);
108
+ }
109
+ return props.options && props.options.length > 0;
110
+ });
111
+
112
+ // 当前选中的选项(从扁平列表或分组数据中查找)
113
+ const currentOption = computed(() => {
114
+ if (props.groupedOptions) {
115
+ for (const group of Object.values(props.groupedOptions)) {
116
+ const found = group.find(opt => opt.value === props.value);
117
+ if (found) return found;
118
+ }
119
+ }
120
+ return props.options?.find(opt => opt.value === props.value);
121
+ });
122
+
123
+ // 排序后的选项(扁平列表模式使用)
124
+ const sortedOptions = computed(() => {
125
+ if (!props.options) return [];
126
+ return [...props.options].sort((a, b) => a.label.localeCompare(b.label));
127
+ });
128
+
129
+ /**
130
+ * 计算下拉方向(根据可用空间)
131
+ */
132
+ function calculateDropdownDirection(): 'up' | 'down' {
133
+ if (!selectorRef.value) return 'up';
134
+ const rect = selectorRef.value.getBoundingClientRect();
135
+ const spaceAbove = rect.top;
136
+ const spaceBelow = window.innerHeight - rect.bottom;
137
+ // 如果上方空间小于 200px 且下方空间更大,则向下弹出
138
+ return spaceAbove < 200 && spaceBelow > spaceAbove ? 'down' : 'up';
139
+ }
140
+
141
+ /**
142
+ * 切换菜单
143
+ */
144
+ function toggleMenu() {
145
+ if (props.disabled) return;
146
+ if (!menuOpen.value) {
147
+ dropdownDirection.value = calculateDropdownDirection();
148
+ }
149
+ menuOpen.value = !menuOpen.value;
150
+ }
151
+
152
+ /**
153
+ * 选择选项
154
+ */
155
+ function selectOption(value: string) {
156
+ emit('select', value);
157
+ menuOpen.value = false;
158
+ }
159
+
160
+ /**
161
+ * 点击外部关闭菜单
162
+ */
163
+ function handleClickOutside(event: MouseEvent) {
164
+ const target = event.target as HTMLElement;
165
+ if (selectorRef.value && !selectorRef.value.contains(target)) {
166
+ menuOpen.value = false;
167
+ }
168
+ }
169
+
170
+ onMounted(() => {
171
+ document.addEventListener('click', handleClickOutside);
172
+ });
173
+
174
+ onUnmounted(() => {
175
+ document.removeEventListener('click', handleClickOutside);
176
+ });
177
+ </script>
178
+
179
+ <style scoped>
180
+ .dropdown-selector {
181
+ position: relative;
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 4px;
185
+ padding: 4px 8px;
186
+ background: var(--chat-muted, #3c3c3c);
187
+ border: none;
188
+ border-radius: 6px;
189
+ font-size: 14px;
190
+ color: var(--chat-text-muted, #888);
191
+ cursor: pointer;
192
+ transition: all 0.15s;
193
+ }
194
+
195
+ .dropdown-selector:hover:not(.disabled) {
196
+ background: var(--chat-muted-hover, #444);
197
+ color: var(--chat-text, #ccc);
198
+ }
199
+
200
+ .dropdown-selector.disabled {
201
+ opacity: 0.6;
202
+ cursor: not-allowed;
203
+ }
204
+
205
+ .selector-text {
206
+ /* max-width: 150px; */
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ white-space: nowrap;
210
+ }
211
+
212
+ .chevron {
213
+ color: var(--chat-text-muted, #666);
214
+ }
215
+
216
+ /* 下拉菜单 */
217
+ .dropdown-menu {
218
+ position: absolute;
219
+ left: 0;
220
+ right: auto;
221
+ min-width: 180px;
222
+ max-height: 320px;
223
+ overflow-y: auto;
224
+ background: var(--chat-dropdown-bg, #252526);
225
+ border: 1px solid rgba(255, 255, 255, 0.1);
226
+ border-radius: 8px;
227
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
228
+ z-index: 9999;
229
+ padding: 4px 8px 4px 4px;
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 2px;
233
+ }
234
+
235
+ /* 向上弹出 */
236
+ .dropdown-menu.dropdown-up {
237
+ bottom: 100%;
238
+ top: auto;
239
+ margin-bottom: 4px;
240
+ }
241
+
242
+ /* 向下弹出 */
243
+ .dropdown-menu.dropdown-down {
244
+ top: 100%;
245
+ bottom: auto;
246
+ margin-top: 4px;
247
+ }
248
+
249
+ /* 右对齐 */
250
+ .dropdown-menu.dropdown-align-right {
251
+ left: auto;
252
+ right: 0;
253
+ }
254
+
255
+ .dropdown-item {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 8px;
259
+ width: 100%;
260
+ padding: 8px 10px;
261
+ border: none;
262
+ background: transparent;
263
+ border-radius: 4px;
264
+ font-size: 14px;
265
+ color: var(--chat-text-muted, #999);
266
+ cursor: pointer;
267
+ transition: all 0.15s;
268
+ white-space: nowrap;
269
+ text-align: left;
270
+ }
271
+
272
+ .dropdown-item:hover {
273
+ background: rgba(255, 255, 255, 0.08);
274
+ color: var(--chat-text, #ccc);
275
+ }
276
+
277
+ .dropdown-item.active {
278
+ background: rgba(255, 255, 255, 0.1);
279
+ color: var(--chat-text, #fff);
280
+ }
281
+
282
+ .check-icon {
283
+ margin-left: auto;
284
+ color: var(--chat-text, #ccc);
285
+ flex-shrink: 0;
286
+ }
287
+
288
+ /* 空状态 */
289
+ .dropdown-empty {
290
+ padding: 12px 10px;
291
+ font-size: 12px;
292
+ color: var(--chat-text-muted, #666);
293
+ text-align: center;
294
+ }
295
+
296
+ /* 分组标题 */
297
+ .group-title {
298
+ padding: 8px 10px 4px;
299
+ margin-top: 4px;
300
+ font-size: 11px;
301
+ color: var(--chat-text-muted, #666);
302
+ font-weight: 500;
303
+ }
304
+
305
+ .group-title:first-child {
306
+ margin-top: 0;
307
+ }
308
+
309
+ /* 选项标签 */
310
+ .option-label {
311
+ flex: 1;
312
+ text-align: left;
313
+ }
314
+
315
+ /* 右侧容器(勾选图标) */
316
+ .option-right {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: 8px;
320
+ margin-left: auto;
321
+ }
322
+ </style>
@@ -0,0 +1,238 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="fade">
4
+ <div v-if="visible" class="image-preview-modal" @click="$emit('close')">
5
+ <!-- 顶部栏 -->
6
+ <div class="preview-header" @click.stop>
7
+ <div v-if="images.length > 1" class="preview-counter">
8
+ {{ currentIndex + 1 }} / {{ images.length }}
9
+ </div>
10
+ <button class="preview-close-btn" title="关闭 (Esc)" @click="$emit('close')">
11
+ <Icon icon="lucide:x" width="18" />
12
+ </button>
13
+ </div>
14
+
15
+ <!-- 左导航 -->
16
+ <button
17
+ v-if="images.length > 1"
18
+ class="preview-nav-btn preview-nav-prev"
19
+ :class="{ disabled: currentIndex <= 0 }"
20
+ :disabled="currentIndex <= 0"
21
+ @click.stop="prev"
22
+ >
23
+ <Icon icon="lucide:chevron-left" width="20" />
24
+ </button>
25
+
26
+ <!-- 主图片区域 -->
27
+ <div class="preview-main" @click.stop>
28
+ <img :src="currentSrc" alt="预览" class="preview-image" />
29
+ </div>
30
+
31
+ <!-- 右导航 -->
32
+ <button
33
+ v-if="images.length > 1"
34
+ class="preview-nav-btn preview-nav-next"
35
+ :class="{ disabled: currentIndex >= images.length - 1 }"
36
+ :disabled="currentIndex >= images.length - 1"
37
+ @click.stop="next"
38
+ >
39
+ <Icon icon="lucide:chevron-right" width="20" />
40
+ </button>
41
+ </div>
42
+ </Transition>
43
+ </Teleport>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ import { computed, watch, onMounted, onUnmounted } from 'vue';
48
+ import { Icon } from '@iconify/vue';
49
+
50
+ const props = defineProps<{
51
+ visible: boolean;
52
+ images: string[];
53
+ initialIndex?: number;
54
+ }>();
55
+
56
+ const emit = defineEmits<{
57
+ close: [];
58
+ 'update:index': [index: number];
59
+ }>();
60
+
61
+ // 当前索引(内部状态)
62
+ const currentIndex = defineModel<number>('index', { default: 0 });
63
+
64
+ // 当前显示的图片
65
+ const currentSrc = computed(() => props.images[currentIndex.value] || '');
66
+
67
+ // 上一张
68
+ function prev() {
69
+ if (currentIndex.value > 0) {
70
+ currentIndex.value--;
71
+ }
72
+ }
73
+
74
+ // 下一张
75
+ function next() {
76
+ if (currentIndex.value < props.images.length - 1) {
77
+ currentIndex.value++;
78
+ }
79
+ }
80
+
81
+ // 键盘导航
82
+ function handleKeydown(e: KeyboardEvent) {
83
+ if (!props.visible) return;
84
+ if (e.key === 'Escape') {
85
+ emit('close');
86
+ } else if (e.key === 'ArrowLeft') {
87
+ prev();
88
+ } else if (e.key === 'ArrowRight') {
89
+ next();
90
+ }
91
+ }
92
+
93
+ // 重置索引
94
+ watch(
95
+ () => props.visible,
96
+ (v) => {
97
+ if (v && props.initialIndex !== undefined) {
98
+ currentIndex.value = props.initialIndex;
99
+ }
100
+ }
101
+ );
102
+
103
+ onMounted(() => {
104
+ document.addEventListener('keydown', handleKeydown);
105
+ });
106
+
107
+ onUnmounted(() => {
108
+ document.removeEventListener('keydown', handleKeydown);
109
+ });
110
+ </script>
111
+
112
+ <style scoped>
113
+ /* 过渡动画 */
114
+ .fade-enter-active,
115
+ .fade-leave-active {
116
+ transition: opacity 0.2s ease;
117
+ }
118
+
119
+ .fade-enter-from,
120
+ .fade-leave-to {
121
+ opacity: 0;
122
+ }
123
+
124
+ /* 遮罩层 */
125
+ .image-preview-modal {
126
+ position: fixed;
127
+ inset: 0;
128
+ z-index: 99999;
129
+ display: flex;
130
+ flex-direction: column;
131
+ background: rgb(61 61 61 / 40%);
132
+ backdrop-filter: blur(8px);
133
+ }
134
+
135
+ /* 顶部栏 */
136
+ .preview-header {
137
+ position: relative;
138
+ z-index: 10;
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ gap: 12px;
143
+ padding: 16px 20px;
144
+ min-height: 52px;
145
+ }
146
+
147
+ .preview-counter {
148
+ color: rgba(255, 255, 255, 0.85);
149
+ font-size: 14px;
150
+ font-variant-numeric: tabular-nums;
151
+ }
152
+
153
+ .preview-close-btn {
154
+ position: absolute;
155
+ right: 16px;
156
+ top: 50%;
157
+ transform: translateY(-50%);
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ height: 36px;
162
+ padding: 0 12px;
163
+ background: rgba(255, 255, 255, 0.1);
164
+ border: none;
165
+ border-radius: 8px;
166
+ color: #fff;
167
+ cursor: pointer;
168
+ transition: all 0.15s;
169
+ }
170
+
171
+ .preview-close-btn:hover {
172
+ background: rgba(255, 255, 255, 0.2);
173
+ transform: translateY(-50%) scale(1.05);
174
+ }
175
+
176
+ .preview-close-btn:active {
177
+ transform: translateY(-50%) scale(0.95);
178
+ }
179
+
180
+ /* 导航按钮 */
181
+ .preview-nav-btn {
182
+ position: absolute;
183
+ top: 50%;
184
+ transform: translateY(-50%);
185
+ z-index: 10;
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ height: 40px;
190
+ padding: 0 12px;
191
+ background: rgba(255, 255, 255, 0.1);
192
+ border: none;
193
+ border-radius: 8px;
194
+ color: #fff;
195
+ cursor: pointer;
196
+ transition: all 0.15s;
197
+ }
198
+
199
+ .preview-nav-btn:hover:not(.disabled) {
200
+ background: rgba(255, 255, 255, 0.2);
201
+ transform: translateY(-50%) scale(1.1);
202
+ }
203
+
204
+ .preview-nav-btn:active:not(.disabled) {
205
+ transform: translateY(-50%) scale(0.95);
206
+ }
207
+
208
+ .preview-nav-btn.disabled {
209
+ opacity: 0.4;
210
+ cursor: not-allowed;
211
+ }
212
+
213
+ .preview-nav-prev {
214
+ left: 24px;
215
+ }
216
+
217
+ .preview-nav-next {
218
+ right: 24px;
219
+ }
220
+
221
+ /* 主图片区域 */
222
+ .preview-main {
223
+ flex: 1;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ overflow: hidden;
228
+ padding: 0 80px;
229
+ }
230
+
231
+ .preview-image {
232
+ max-width: calc(100% - 80px);
233
+ max-height: calc(100vh - 140px);
234
+ object-fit: contain;
235
+ border-radius: 8px;
236
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
237
+ }
238
+ </style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="at-placeholder-view">
3
+ <Icon icon="lucide:git-branch" width="32" class="at-placeholder-icon" />
4
+ <div class="at-placeholder-title">分支差异</div>
5
+ <div class="at-placeholder-desc">功能待实现</div>
6
+ <div class="at-placeholder-hint">将支持引用当前分支与主分支的差异</div>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { Icon } from '@iconify/vue';
12
+
13
+ defineProps<{
14
+ query: string;
15
+ activeIndex: number;
16
+ }>();
17
+
18
+ defineEmits<{
19
+ select: [path: string];
20
+ 'set-active': [index: number];
21
+ 'update-count': [count: number];
22
+ }>();
23
+
24
+ // 暴露给父组件(空实现)
25
+ defineExpose({
26
+ getActivePath: () => null,
27
+ confirmActive: () => {},
28
+ });
29
+ </script>
30
+
31
+ <style scoped>
32
+ .at-placeholder-view {
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: center;
37
+ padding: 32px 16px;
38
+ text-align: center;
39
+ }
40
+
41
+ .at-placeholder-icon {
42
+ color: #555;
43
+ margin-bottom: 12px;
44
+ }
45
+
46
+ .at-placeholder-title {
47
+ font-size: 14px;
48
+ font-weight: 500;
49
+ color: #888;
50
+ margin-bottom: 8px;
51
+ }
52
+
53
+ .at-placeholder-desc {
54
+ font-size: 13px;
55
+ color: #666;
56
+ margin-bottom: 4px;
57
+ }
58
+
59
+ .at-placeholder-hint {
60
+ font-size: 11px;
61
+ color: #555;
62
+ }
63
+ </style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="at-placeholder-view">
3
+ <Icon icon="lucide:globe" width="32" class="at-placeholder-icon" />
4
+ <div class="at-placeholder-title">网页</div>
5
+ <div class="at-placeholder-desc">功能待实现</div>
6
+ <div class="at-placeholder-hint">将支持引用网页内容、URL 等</div>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { Icon } from '@iconify/vue';
12
+
13
+ defineProps<{
14
+ query: string;
15
+ activeIndex: number;
16
+ }>();
17
+
18
+ defineEmits<{
19
+ select: [path: string];
20
+ 'set-active': [index: number];
21
+ 'update-count': [count: number];
22
+ }>();
23
+
24
+ // 暴露给父组件(空实现)
25
+ defineExpose({
26
+ getActivePath: () => null,
27
+ confirmActive: () => {},
28
+ });
29
+ </script>
30
+
31
+ <style scoped>
32
+ .at-placeholder-view {
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ justify-content: center;
37
+ padding: 32px 16px;
38
+ text-align: center;
39
+ }
40
+
41
+ .at-placeholder-icon {
42
+ color: #555;
43
+ margin-bottom: 12px;
44
+ }
45
+
46
+ .at-placeholder-title {
47
+ font-size: 14px;
48
+ font-weight: 500;
49
+ color: #888;
50
+ margin-bottom: 8px;
51
+ }
52
+
53
+ .at-placeholder-desc {
54
+ font-size: 13px;
55
+ color: #666;
56
+ margin-bottom: 4px;
57
+ }
58
+
59
+ .at-placeholder-hint {
60
+ font-size: 11px;
61
+ color: #555;
62
+ }
63
+ </style>