vue-ops-chat 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.
@@ -0,0 +1,473 @@
1
+ <template>
2
+ <transition name="modal-fade">
3
+ <div
4
+ v-if="visible"
5
+ class="image-preview-overlay"
6
+ @click="handleBackdropClick"
7
+ >
8
+ <!-- 关闭按钮 -->
9
+ <button class="close-btn" @click="closeModal">
10
+ <SvgIcon name="close" class="close-icon" />
11
+ </button>
12
+
13
+ <!-- 图片显示区域 -->
14
+ <div class="image-container">
15
+ <button
16
+ v-if="images.length > 1 && showPrevButton"
17
+ class="nav-btn prev-btn"
18
+ @click.stop="prevImage"
19
+ >
20
+ <SvgIcon name="prev" class="nav-icon" />
21
+ </button>
22
+
23
+ <img
24
+ :src="currentImageUrl"
25
+ :alt="imageName"
26
+ class="preview-image"
27
+ :style="{ transform: 'scale(' + scale + ')' }"
28
+ @load="onImageLoad"
29
+ @error="onImageError"
30
+ />
31
+
32
+ <button
33
+ v-if="images.length > 1 && showNextButton"
34
+ class="nav-btn next-btn"
35
+ @click.stop="nextImage"
36
+ >
37
+ <SvgIcon name="next" class="nav-icon" />
38
+ </button>
39
+ </div>
40
+
41
+ <!-- 图片计数器 -->
42
+ <div v-if="images.length > 1" class="image-counter">
43
+ {{ currentIndex + 1 }} / {{ images.length }}
44
+ </div>
45
+
46
+ <!-- 缩放控制按钮 -->
47
+ <div class="zoom-controls">
48
+ <button
49
+ class="zoom-btn"
50
+ :disabled="!canZoomOut"
51
+ @click.stop="zoomOut"
52
+ title="缩小"
53
+ >
54
+ <SvgIcon name="zoomOut" class="zoom-icon" />
55
+ </button>
56
+ <button
57
+ class="zoom-btn"
58
+ @click.stop="resetZoom"
59
+ title="1:1"
60
+ >
61
+ {{ Math.round(scale * 100) }}%
62
+ </button>
63
+ <button
64
+ class="zoom-btn"
65
+ :disabled="!canZoomIn"
66
+ @click.stop="zoomIn"
67
+ title="放大"
68
+ >
69
+ <SvgIcon name="zoomIn" class="zoom-icon" />
70
+ </button>
71
+ <button
72
+ class="zoom-btn"
73
+ @click.stop="fitScreen"
74
+ title="适应屏幕"
75
+ >
76
+ <SvgIcon name="fullscreen" class="zoom-icon" />
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </transition>
81
+ </template>
82
+
83
+ <script>
84
+ import SvgIcon from './Icons/SvgIcon.vue';
85
+
86
+ export default {
87
+ name: 'ImagePreviewModal',
88
+ components: {
89
+ SvgIcon
90
+ },
91
+ props: {
92
+ visible: {
93
+ type: Boolean,
94
+ default: false
95
+ },
96
+ images: {
97
+ type: Array,
98
+ default: () => []
99
+ },
100
+ initialIndex: {
101
+ type: Number,
102
+ default: 0
103
+ }
104
+ },
105
+ data() {
106
+ return {
107
+ currentIndex: this.initialIndex,
108
+ scale: 1, // 当前缩放比例
109
+ minScale: 0.1, // 最小缩放比例
110
+ maxScale: 5, // 最大缩放比例
111
+ step: 0.1 // 缩放步长
112
+ };
113
+ },
114
+ computed: {
115
+ currentImageUrl() {
116
+ if (this.images.length === 0) return '';
117
+ const image = this.images[this.currentIndex];
118
+ return image?.url || '';
119
+ },
120
+ imageName() {
121
+ if (this.images.length === 0) return '';
122
+ const image = this.images[this.currentIndex];
123
+ return image?.name || '未命名图片';
124
+ },
125
+ showPrevButton() {
126
+ return this.currentIndex > 0;
127
+ },
128
+ showNextButton() {
129
+ return this.currentIndex < this.images.length - 1;
130
+ },
131
+ // 缩放相关计算属性
132
+ canZoomIn() {
133
+ return this.scale < this.maxScale;
134
+ },
135
+ canZoomOut() {
136
+ return this.scale > this.minScale;
137
+ },
138
+ },
139
+ watch: {
140
+ visible(newVal) {
141
+ if (newVal) {
142
+ // 每次打开时重置到初始索引
143
+ this.currentIndex = this.initialIndex;
144
+ // 禁止背景滚动
145
+ document.body.style.overflow = 'hidden';
146
+ } else {
147
+ // 恢复背景滚动
148
+ document.body.style.overflow = '';
149
+ }
150
+ },
151
+ initialIndex(newVal) {
152
+ this.currentIndex = newVal;
153
+ }
154
+ },
155
+ methods: {
156
+ /**
157
+ * 关闭模态框
158
+ */
159
+ closeModal() {
160
+ this.$emit('close');
161
+ },
162
+
163
+ /**
164
+ * 点击背景关闭
165
+ */
166
+ handleBackdropClick() {
167
+ this.closeModal();
168
+ },
169
+
170
+ /**
171
+ * 上一张图片
172
+ */
173
+ prevImage() {
174
+ if (this.currentIndex > 0) {
175
+ this.currentIndex--;
176
+ this.$emit('image-change', this.currentIndex);
177
+ }
178
+ },
179
+
180
+ /**
181
+ * 下一张图片
182
+ */
183
+ nextImage() {
184
+ if (this.currentIndex < this.images.length - 1) {
185
+ this.currentIndex++;
186
+ this.$emit('image-change', this.currentIndex);
187
+ }
188
+ },
189
+
190
+ /**
191
+ * 选择特定图片
192
+ * @param {number} index - 图片索引
193
+ */
194
+ selectImage(index) {
195
+ if (index >= 0 && index < this.images.length) {
196
+ this.currentIndex = index;
197
+ this.$emit('image-change', this.currentIndex);
198
+ }
199
+ },
200
+
201
+ /**
202
+ * 图片加载完成
203
+ */
204
+ onImageLoad() {
205
+ this.$emit('image-load', {
206
+ index: this.currentIndex,
207
+ url: this.currentImageUrl
208
+ });
209
+ },
210
+
211
+ /**
212
+ * 图片加载失败
213
+ */
214
+ onImageError() {
215
+ this.$emit('image-error', {
216
+ index: this.currentIndex,
217
+ url: this.currentImageUrl
218
+ });
219
+ },
220
+
221
+ /**
222
+ * 放大图片
223
+ */
224
+ zoomIn() {
225
+ if (this.canZoomIn) {
226
+ this.scale = Math.min(this.scale + this.step, this.maxScale);
227
+ }
228
+ },
229
+
230
+ /**
231
+ * 缩小图片
232
+ */
233
+ zoomOut() {
234
+ if (this.canZoomOut) {
235
+ this.scale = Math.max(this.scale - this.step, this.minScale);
236
+ }
237
+ },
238
+
239
+ /**
240
+ * 1:1显示
241
+ */
242
+ resetZoom() {
243
+ this.scale = 1;
244
+ },
245
+
246
+ /**
247
+ * 适应屏幕
248
+ */
249
+ fitScreen() {
250
+ this.scale = 1;
251
+ // 这里可以根据实际需要调整适配逻辑
252
+ },
253
+ }
254
+ }
255
+ </script>
256
+
257
+ <style scoped>
258
+ .image-preview-overlay {
259
+ position: fixed;
260
+ top: 0;
261
+ left: 0;
262
+ width: 100vw;
263
+ height: 100vh;
264
+ background: rgba(0, 0, 0, 0.7); /* 全局透明蒙层 */
265
+ z-index: 10002; /* 显示在最上层 */
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ backdrop-filter: blur(3px);
270
+ }
271
+
272
+ .image-counter {
273
+ position: fixed;
274
+ bottom: 20px;
275
+ left: 50%;
276
+ transform: translateX(-50%);
277
+ background: rgba(0, 0, 0, 0.7);
278
+ color: white;
279
+ padding: 6px 12px;
280
+ border-radius: 20px;
281
+ font-size: 14px;
282
+ z-index: 10003;
283
+ }
284
+
285
+ .close-btn {
286
+ position: fixed;
287
+ top: 20px;
288
+ right: 20px; /* 右侧显示 */
289
+ width: 40px;
290
+ height: 40px;
291
+ border-radius: 50%;
292
+ background: rgba(255, 255, 255, 0.9);
293
+ border: 1px solid rgba(0, 0, 0, 0.1);
294
+ cursor: pointer;
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ transition: all 0.2s ease;
299
+ z-index: 10003;
300
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
301
+ }
302
+
303
+ .close-btn:hover {
304
+ background: white;
305
+ transform: scale(1.1);
306
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
307
+ }
308
+
309
+ .close-icon {
310
+ width: 20px;
311
+ height: 20px;
312
+ color: #333; /* 深色图标 */
313
+ }
314
+
315
+ .image-container {
316
+ position: relative;
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ max-width: calc(90vw - 120px); /* 为两侧按钮留出空间 */
321
+ max-height: 90vh;
322
+ margin: 0 60px; /* 左右各60px边距 */
323
+ }
324
+
325
+ .preview-image {
326
+ max-width: 100%;
327
+ max-height: 90vh;
328
+ object-fit: contain;
329
+ border-radius: 8px;
330
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
331
+ /* 使用内联样式绑定缩放比例 */
332
+ transform-origin: center;
333
+ transition: transform 0.2s ease;
334
+ }
335
+
336
+ .nav-btn {
337
+ position: absolute;
338
+ top: 50%;
339
+ transform: translateY(-50%);
340
+ width: 48px;
341
+ height: 48px;
342
+ border-radius: 50%;
343
+ background: rgba(255, 255, 255, 0.9);
344
+ border: 1px solid rgba(0, 0, 0, 0.1);
345
+ cursor: pointer;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ transition: all 0.2s ease;
350
+ z-index: 10003;
351
+ backdrop-filter: blur(5px);
352
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
353
+ }
354
+
355
+ .nav-btn:hover {
356
+ background: white;
357
+ transform: translateY(-50%) scale(1.1);
358
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
359
+ }
360
+
361
+ .prev-btn {
362
+ left: -60px; /* 定位在图片左侧 */
363
+ }
364
+
365
+ .next-btn {
366
+ right: -60px; /* 定位在图片右侧 */
367
+ }
368
+
369
+ .nav-icon {
370
+ width: 24px;
371
+ height: 24px;
372
+ color: #333;
373
+ }
374
+
375
+ /* 缩放控制样式 */
376
+ .zoom-controls {
377
+ position: fixed;
378
+ bottom: 20px;
379
+ right: 20px;
380
+ display: flex;
381
+ gap: 10px;
382
+ z-index: 10003;
383
+ }
384
+
385
+ .zoom-btn {
386
+ width: 40px;
387
+ height: 40px;
388
+ border-radius: 50%;
389
+ background: rgba(255, 255, 255, 0.9);
390
+ border: 1px solid rgba(0, 0, 0, 0.1);
391
+ cursor: pointer;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ transition: all 0.2s ease;
396
+ font-size: 12px;
397
+ font-weight: 500;
398
+ color: #333;
399
+ backdrop-filter: blur(5px);
400
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
401
+ }
402
+
403
+ .zoom-btn:hover:not(:disabled) {
404
+ background: white;
405
+ transform: scale(1.1);
406
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
407
+ }
408
+
409
+ .zoom-btn:disabled {
410
+ opacity: 0.5;
411
+ cursor: not-allowed;
412
+ background: rgba(200, 200, 200, 0.7);
413
+ }
414
+
415
+ .zoom-icon {
416
+ width: 20px;
417
+ height: 20px;
418
+ color: #333;
419
+ }
420
+
421
+
422
+
423
+
424
+
425
+ /* 动画效果 */
426
+ .modal-fade-enter-active {
427
+ transition: all 0.3s ease;
428
+ }
429
+
430
+ .modal-fade-leave-active {
431
+ transition: all 0.3s ease;
432
+ }
433
+
434
+ .modal-fade-enter-from {
435
+ opacity: 0;
436
+ }
437
+
438
+ .modal-fade-leave-to {
439
+ opacity: 0;
440
+ }
441
+
442
+ /* 响应式设计 */
443
+ @media (max-width: 768px) {
444
+ .modal-content {
445
+ width: 95%;
446
+ height: 95%;
447
+ border-radius: 8px;
448
+ }
449
+
450
+ .nav-btn {
451
+ width: 40px;
452
+ height: 40px;
453
+ }
454
+
455
+ .nav-icon {
456
+ width: 20px;
457
+ height: 20px;
458
+ }
459
+
460
+ .prev-btn {
461
+ left: 12px;
462
+ }
463
+
464
+ .next-btn {
465
+ right: 12px;
466
+ }
467
+
468
+ .thumbnail-item {
469
+ width: 50px;
470
+ height: 50px;
471
+ }
472
+ }
473
+ </style>
@@ -0,0 +1,153 @@
1
+ <template>
2
+ <div :class="['message-bubble', message.sender]">
3
+ <!-- 助手头像 -->
4
+ <div class="message-avatar assistant-avatar" v-if="message.sender === 'assistant'">
5
+ <SvgIcon name="user" />
6
+ </div>
7
+
8
+ <!-- 用户头像 -->
9
+ <div class="message-avatar user-avatar" v-else>
10
+ <SvgIcon name="user" />
11
+ </div>
12
+ <div class="message-content">
13
+ <div class="message-text">{{ message.content }}</div>
14
+
15
+ <!-- 图片展示 -->
16
+ <MessageImages
17
+ v-if="message.images && message.images.length > 0"
18
+ :images="message.images"
19
+ @preview="handleImagePreview"
20
+ />
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import MessageImages from './MessageImages.vue';
27
+ import SvgIcon from './Icons/SvgIcon.vue';
28
+
29
+ export default {
30
+ name: 'MessageBubble',
31
+ components: {
32
+ MessageImages,
33
+ SvgIcon
34
+ },
35
+ props: {
36
+ message: {
37
+ type: Object,
38
+ required: true,
39
+ validator: (msg) => {
40
+ // 允许内容为空的情况(纯图片消息)
41
+ return msg.sender && msg.timestamp && (msg.content !== undefined);
42
+ }
43
+ }
44
+ },
45
+ methods: {
46
+ /**
47
+ * 处理图片预览事件
48
+ * @param {string} imageUrl - 要预览的图片URL
49
+ */
50
+ handleImagePreview(imageUrl) {
51
+ this.$emit('preview-image', imageUrl);
52
+ },
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <style scoped>
58
+ .message-bubble {
59
+ display: flex;
60
+ gap: 12px;
61
+ animation: messageAppear 0.3s ease;
62
+ margin-bottom: 12px;
63
+ }
64
+
65
+ .message-bubble.assistant {
66
+ align-self: flex-start;
67
+ }
68
+
69
+ .message-bubble.user {
70
+ align-self: flex-end;
71
+ flex-direction: row-reverse;
72
+ }
73
+
74
+ .message-avatar {
75
+ width: 32px;
76
+ height: 32px;
77
+ border-radius: 50%;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ flex-shrink: 0;
82
+ }
83
+
84
+ /* 助手头像样式 */
85
+ .assistant-avatar {
86
+ background: linear-gradient(135deg, #667eea, #764ba2);
87
+ }
88
+
89
+ /* 用户头像样式 */
90
+ .user-avatar {
91
+ background: linear-gradient(135deg, #4ade80, #22c55e);
92
+ border: 2px solid rgba(255, 255, 255, 0.3);
93
+ box-shadow: 0 2px 8px rgba(74, 222, 128, 0.3);
94
+ }
95
+
96
+ .message-avatar svg {
97
+ width: 16px;
98
+ height: 16px;
99
+ color: white;
100
+ }
101
+
102
+ .message-content {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 4px;
106
+ }
107
+
108
+ .message-text {
109
+ padding: 12px 16px;
110
+ border-radius: 18px;
111
+ word-wrap: break-word;
112
+ line-height: 1.5;
113
+ font-size: 14px;
114
+ }
115
+
116
+ /* 当只有图片时隐藏文本区域的样式 */
117
+ .message-text:empty {
118
+ display: none;
119
+ }
120
+
121
+ .message-text:empty + .message-images {
122
+ margin-top: 0;
123
+ }
124
+
125
+ .message-bubble.assistant .message-text {
126
+ background: white;
127
+ color: #1e293b;
128
+ border: 1px solid #e2e8f0;
129
+ border-top-left-radius: 4px;
130
+ }
131
+
132
+ .message-bubble.user .message-text {
133
+ background: linear-gradient(135deg, #667eea, #764ba2);
134
+ color: white;
135
+ border-top-right-radius: 4px;
136
+ }
137
+
138
+ .message-time {
139
+ font-size: 11px;
140
+ color: #94a3b8;
141
+ padding: 0 4px;
142
+ }
143
+
144
+ .message-bubble.user .message-time {
145
+ text-align: right;
146
+ color: rgba(255, 255, 255, 0.8);
147
+ }
148
+
149
+ @keyframes messageAppear {
150
+ from { opacity: 0; transform: translateY(10px); }
151
+ to { opacity: 1; transform: translateY(0); }
152
+ }
153
+ </style>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div class="message-images">
3
+ <div
4
+ v-for="(image, index) in images"
5
+ :key="index"
6
+ class="message-image-item"
7
+ :class="{ 'upload-failed': image.uploadSuccess === false }"
8
+ >
9
+ <img
10
+ :src="image.url"
11
+ :alt="image.name"
12
+ class="message-image"
13
+ @click="previewImage(image.url)"
14
+ />
15
+ <!-- 上传失败的提示 -->
16
+ <div v-if="image.uploadSuccess === false" class="upload-error-overlay">
17
+ <span class="error-text">上传失败</span>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ export default {
25
+ name: 'MessageImages',
26
+ props: {
27
+ images: {
28
+ type: Array,
29
+ default: () => []
30
+ }
31
+ },
32
+ methods: {
33
+ /**
34
+ * 预览图片
35
+ * @param {string} imageUrl - 图片URL
36
+ */
37
+ previewImage(imageUrl) {
38
+ this.$emit('preview', imageUrl);
39
+ }
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <style scoped>
45
+ .message-images {
46
+ display: flex;
47
+ gap: 4px;
48
+ margin-top: 8px;
49
+ flex-wrap: wrap;
50
+ justify-content: flex-end; /* 多行时靠右显示 */
51
+ }
52
+
53
+ .message-image-item {
54
+ width: 50px;
55
+ height: 50px;
56
+ border-radius: 6px;
57
+ overflow: hidden;
58
+ border: 1px solid #e2e8f0;
59
+ cursor: pointer;
60
+ transition: all 0.2s ease;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .message-image-item:hover {
65
+ transform: scale(1.05);
66
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
67
+ border-color: #94a3b8;
68
+ }
69
+
70
+ .message-image-item.upload-failed {
71
+ position: relative;
72
+ border: 2px solid #ff4757;
73
+ }
74
+
75
+ .message-image {
76
+ width: 100%;
77
+ height: 100%;
78
+ object-fit: cover;
79
+ }
80
+
81
+ /* 上传失败覆盖层 */
82
+ .upload-error-overlay {
83
+ position: absolute;
84
+ top: 0;
85
+ left: 0;
86
+ right: 0;
87
+ bottom: 0;
88
+ background: rgba(255, 71, 87, 0.8);
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ }
93
+
94
+ .error-text {
95
+ color: white;
96
+ font-size: 12px;
97
+ font-weight: 500;
98
+ text-align: center;
99
+ padding: 4px;
100
+ }
101
+
102
+ /* 响应式调整 */
103
+ @media (max-width: 480px) {
104
+ .message-images {
105
+ gap: 3px;
106
+ }
107
+
108
+ .message-image-item {
109
+ width: 50px;
110
+ height: 50px;
111
+ }
112
+ }
113
+ </style>