@xilonglab/vue-main 1.2.6 → 1.2.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xilonglab/vue-main",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "xilong vue main",
5
5
  "main": "packages/index.js",
6
6
  "scripts": {
@@ -55,13 +55,13 @@ defineExpose({ click })
55
55
 
56
56
 
57
57
  <template>
58
- <el-button class="xl-button xl-async-button" ref="btnRef" :size="size" :type="type" :disabled="disabled" :loading="loading"
58
+ <xl-button class="xl-async-button" ref="btnRef" :size="size" :type="type" :disabled="disabled" :loading="loading"
59
59
  :icon="icon" @click="handleClick">
60
60
  <span v-if="!loading">
61
61
  {{ l }}
62
62
  <slot />
63
63
  </span>
64
- </el-button>
64
+ </xl-button>
65
65
  </template>
66
66
 
67
67
 
@@ -89,25 +89,59 @@ defineExpose({ show })
89
89
  border-radius: 7px 7px 0 0;
90
90
  padding: 14px 20px 10px;
91
91
  margin: 0!important;
92
- background: transparent linear-gradient(90deg, #073052 0%, rgb(100, 100, 100) 100%) 0% 0% no-repeat padding-box !important;
92
+ background: linear-gradient(135deg, #064e3b 0%, #059669 50%, #047857 100%) !important;
93
+ position: relative;
94
+ overflow: hidden;
95
+
96
+ &::before {
97
+ content: '';
98
+ position: absolute;
99
+ top: 0;
100
+ left: 0;
101
+ right: 0;
102
+ bottom: 0;
103
+ background-image:
104
+ linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px),
105
+ linear-gradient(180deg, rgba(255,255,255,0.1) 1px, transparent 1px);
106
+ background-size: 20px 20px;
107
+ opacity: 0.3;
108
+ pointer-events: none;
109
+ }
93
110
 
94
111
  .el-dialog__headerbtn {
95
112
  top: 14px;
113
+ right: 16px;
114
+ width: 24px;
115
+ height: 24px;
116
+ border-radius: 50%;
117
+ border: 1px solid rgba(255,255,255,0.3);
118
+ background: rgba(255,255,255,0.1);
119
+ transition: all 0.3s ease;
120
+
121
+ &:hover {
122
+ background: rgba(255,255,255,0.2);
123
+ border-color: rgba(255,255,255,0.5);
124
+ transform: scale(1.05);
125
+ }
126
+
127
+ .el-dialog__close {
128
+ color: white !important;
129
+ font-size: 14px;
130
+ font-weight: bold;
131
+ }
96
132
  }
97
133
 
98
- .el-dialog__title,
99
- .el-dialog__close {
100
- color: rgb(245, 245, 245) !important;
134
+ .el-dialog__title {
135
+ color: white !important;
136
+ font-weight: 500;
137
+ font-size: 16px;
138
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3);
101
139
  }
102
-
103
- .el-dialog__headerbtn {
104
- top: 3px !important;
105
- }
106
-
107
140
  }
108
141
 
109
142
  .el-dialog__body {
110
143
  padding: 10px 10px 5px 10px;
144
+ background-color: #f8f9fa !important;
111
145
  }
112
146
  }
113
147
  </style>
@@ -1,36 +1,504 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "XlImagePreviewDialog" })
3
3
 
4
- import { ref } from 'vue'
5
-
4
+ import { ref, computed, nextTick } from 'vue'
5
+ import { ElMessage } from 'element-plus'
6
+ import {
7
+ ZoomIn,
8
+ ZoomOut,
9
+ Refresh,
10
+ RefreshRight,
11
+ FullScreen,
12
+ Download,
13
+ Loading,
14
+ Picture
15
+ } from '@element-plus/icons-vue'
6
16
 
7
17
  const props = defineProps({
8
18
  url: {
9
19
  type: String,
10
20
  default: ''
21
+ },
22
+ title: {
23
+ type: String,
24
+ default: '图片预览'
25
+ },
26
+ showToolbar: {
27
+ type: Boolean,
28
+ default: true
29
+ },
30
+ allowDownload: {
31
+ type: Boolean,
32
+ default: true
33
+ },
34
+ allowFullscreen: {
35
+ type: Boolean,
36
+ default: true
11
37
  }
12
38
  })
13
39
 
40
+ const emit = defineEmits(['close', 'download'])
41
+
14
42
  const visible = ref(false)
43
+ const loading = ref(false)
44
+ const error = ref(false)
45
+ const scale = ref(1)
46
+ const rotation = ref(0)
47
+ const isFullscreen = ref(false)
48
+ const imageLoaded = ref(false)
15
49
 
50
+ // 计算样式
51
+ const imageStyle = computed(() => ({
52
+ transform: `scale(${scale.value}) rotate(${rotation.value}deg)`,
53
+ transition: 'transform 0.3s ease'
54
+ }))
16
55
 
17
- defineExpose({
18
- show() {
19
- visible.value = true
56
+ // 计算缩放百分比
57
+ const zoomPercentage = computed(() => {
58
+ const scaleValue = scale.value || 1
59
+ return Math.round(scaleValue * 100)
60
+ })
61
+
62
+ // 显示对话框
63
+ const show = () => {
64
+ visible.value = true
65
+ resetImageState()
66
+ }
67
+
68
+ // 隐藏对话框
69
+ const hide = () => {
70
+ visible.value = false
71
+ emit('close')
72
+ }
73
+
74
+ // 重置图片状态
75
+ const resetImageState = () => {
76
+ scale.value = 1
77
+ rotation.value = 0
78
+ isFullscreen.value = false
79
+ imageLoaded.value = false
80
+ error.value = false
81
+ }
82
+
83
+ // 图片加载完成
84
+ const onImageLoad = () => {
85
+ loading.value = false
86
+ imageLoaded.value = true
87
+ error.value = false
88
+ }
89
+
90
+ // 图片加载失败
91
+ const onImageError = () => {
92
+ loading.value = false
93
+ error.value = true
94
+ ElMessage.error('图片加载失败')
95
+ }
96
+
97
+ // 图片开始加载
98
+ const onImageLoadStart = () => {
99
+ loading.value = true
100
+ error.value = false
101
+ }
102
+
103
+ // 缩放功能
104
+ const zoomIn = () => {
105
+ const currentScale = scale.value || 1
106
+ if (currentScale < 3) {
107
+ scale.value = Math.min(3, currentScale + 0.2)
20
108
  }
109
+ }
110
+
111
+ const zoomOut = () => {
112
+ const currentScale = scale.value || 1
113
+ if (currentScale > 0.2) {
114
+ scale.value = Math.max(0.2, currentScale - 0.2)
115
+ }
116
+ }
117
+
118
+ const resetZoom = () => {
119
+ scale.value = 1
120
+ }
121
+
122
+ // 旋转功能
123
+ const rotate = () => {
124
+ rotation.value += 90
125
+ }
126
+
127
+ // 全屏切换
128
+ const toggleFullscreen = () => {
129
+ isFullscreen.value = !isFullscreen.value
130
+ }
131
+
132
+ // 下载图片
133
+ const downloadImage = () => {
134
+ if (!props.url) return
135
+
136
+ const link = document.createElement('a')
137
+ link.href = props.url
138
+ link.download = `image_${Date.now()}.jpg`
139
+ document.body.appendChild(link)
140
+ link.click()
141
+ document.body.removeChild(link)
142
+
143
+ emit('download', props.url)
144
+ }
145
+
146
+ // 键盘事件处理
147
+ const handleKeydown = (event) => {
148
+ if (!visible.value) return
149
+
150
+ switch (event.key) {
151
+ case 'Escape':
152
+ hide()
153
+ break
154
+ case '+':
155
+ case '=':
156
+ zoomIn()
157
+ break
158
+ case '-':
159
+ zoomOut()
160
+ break
161
+ case '0':
162
+ resetZoom()
163
+ break
164
+ case 'r':
165
+ case 'R':
166
+ rotate()
167
+ break
168
+ case 'f':
169
+ case 'F':
170
+ toggleFullscreen()
171
+ break
172
+ }
173
+ }
174
+
175
+ // 监听键盘事件
176
+ const addKeyboardListener = () => {
177
+ document.addEventListener('keydown', handleKeydown)
178
+ }
179
+
180
+ const removeKeyboardListener = () => {
181
+ document.removeEventListener('keydown', handleKeydown)
182
+ }
183
+
184
+ // 监听对话框显示状态
185
+ const watchVisible = () => {
186
+ if (visible.value) {
187
+ addKeyboardListener()
188
+ } else {
189
+ removeKeyboardListener()
190
+ }
191
+ }
192
+
193
+ // 鼠标滚轮缩放
194
+ const handleWheel = (event) => {
195
+ if (!imageLoaded.value) return
196
+
197
+ event.preventDefault()
198
+ const currentScale = scale.value || 1
199
+ const delta = event.deltaY > 0 ? -0.1 : 0.1
200
+ const newScale = Math.max(0.1, Math.min(3, currentScale + delta))
201
+ scale.value = newScale
202
+ }
203
+
204
+ // 暴露方法
205
+ defineExpose({
206
+ show,
207
+ hide
21
208
  })
22
- </script>
23
209
 
210
+ // 监听visible变化
211
+ watchVisible()
212
+ </script>
24
213
 
25
214
  <template>
26
- <el-dialog class="xl-image-preview-dialog xl-dialog" v-model="visible" width="50%">
27
- <el-image :src="url"></el-image>
215
+ <el-dialog
216
+ class="xl-image-preview-dialog"
217
+ v-model="visible"
218
+ :width="isFullscreen ? '100%' : '80%'"
219
+ :fullscreen="isFullscreen"
220
+ :title="title"
221
+ :show-close="true"
222
+ :close-on-click-modal="true"
223
+ :close-on-press-escape="true"
224
+ @close="hide"
225
+ @opened="watchVisible"
226
+ @closed="removeKeyboardListener"
227
+ >
228
+ <template #header>
229
+ <div class="dialog-header">
230
+ <span class="dialog-title">{{ title }}</span>
231
+ <div class="header-actions" v-if="showToolbar">
232
+ <el-tooltip content="放大 (Ctrl + +)" placement="bottom">
233
+ <el-button
234
+ type="text"
235
+ :icon="ZoomIn"
236
+ @click="zoomIn"
237
+ :disabled="!imageLoaded"
238
+ />
239
+ </el-tooltip>
240
+ <el-tooltip content="缩小 (Ctrl + -)" placement="bottom">
241
+ <el-button
242
+ type="text"
243
+ :icon="ZoomOut"
244
+ @click="zoomOut"
245
+ :disabled="!imageLoaded"
246
+ />
247
+ </el-tooltip>
248
+ <el-tooltip content="重置缩放 (Ctrl + 0)" placement="bottom">
249
+ <el-button
250
+ type="text"
251
+ :icon="Refresh"
252
+ @click="resetZoom"
253
+ :disabled="!imageLoaded"
254
+ />
255
+ </el-tooltip>
256
+ <el-tooltip content="旋转 (R)" placement="bottom">
257
+ <el-button
258
+ type="text"
259
+ :icon="RefreshRight"
260
+ @click="rotate"
261
+ :disabled="!imageLoaded"
262
+ />
263
+ </el-tooltip>
264
+ <el-tooltip content="全屏 (F)" placement="bottom" v-if="allowFullscreen">
265
+ <el-button
266
+ type="text"
267
+ :icon="FullScreen"
268
+ @click="toggleFullscreen"
269
+ />
270
+ </el-tooltip>
271
+ <el-tooltip content="下载图片" placement="bottom" v-if="allowDownload">
272
+ <el-button
273
+ type="text"
274
+ :icon="Download"
275
+ @click="downloadImage"
276
+ :disabled="!imageLoaded"
277
+ />
278
+ </el-tooltip>
279
+ </div>
280
+ </div>
281
+ </template>
282
+
283
+ <div class="image-container" :class="{ 'fullscreen': isFullscreen }">
284
+ <!-- 加载状态 -->
285
+ <div v-if="loading" class="loading-container">
286
+ <el-icon class="loading-icon"><Loading /></el-icon>
287
+ <p>图片加载中...</p>
288
+ </div>
289
+
290
+ <!-- 错误状态 -->
291
+ <div v-else-if="error" class="error-container">
292
+ <el-icon class="error-icon"><Picture /></el-icon>
293
+ <p>图片加载失败</p>
294
+ <el-button type="primary" @click="onImageLoadStart">重新加载</el-button>
295
+ </div>
296
+
297
+ <!-- 图片显示 -->
298
+ <div v-else class="image-wrapper" @wheel="handleWheel">
299
+ <el-image
300
+ :src="url"
301
+ :style="imageStyle"
302
+ :fit="'contain'"
303
+ :preview-src-list="[]"
304
+ :initial-index="0"
305
+ @load="onImageLoad"
306
+ @error="onImageError"
307
+ @loadstart="onImageLoadStart"
308
+ class="preview-image"
309
+ />
310
+ </div>
311
+
312
+ <!-- 缩放信息 -->
313
+ <div v-if="imageLoaded && showToolbar" class="zoom-info">
314
+ {{ zoomPercentage }}%
315
+ </div>
316
+ </div>
317
+
318
+ <!-- 操作提示 -->
319
+ <div v-if="showToolbar" class="operation-tips">
320
+ <span>快捷键:+/- 缩放,0 重置,R 旋转,F 全屏,ESC 关闭</span>
321
+ </div>
28
322
  </el-dialog>
29
323
  </template>
30
-
31
324
 
32
-
33
- <style lang="less"></style>
325
+ <style lang="less" scoped>
326
+ .xl-image-preview-dialog {
327
+ .dialog-header {
328
+ display: flex;
329
+ justify-content: space-between;
330
+ align-items: center;
331
+ width: 100%;
332
+
333
+ .dialog-title {
334
+ font-size: 16px;
335
+ font-weight: 500;
336
+ color: #303133;
337
+ }
338
+
339
+ .header-actions {
340
+ display: flex;
341
+ gap: 8px;
342
+
343
+ .el-button {
344
+ padding: 4px 8px;
345
+ font-size: 16px;
346
+
347
+ &:hover {
348
+ background-color: #f5f7fa;
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ .image-container {
355
+ position: relative;
356
+ min-height: 400px;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ background: #f8f9fa;
361
+ border-radius: 8px;
362
+ overflow: hidden;
363
+
364
+ &.fullscreen {
365
+ min-height: calc(100vh - 120px);
366
+ }
367
+ }
368
+
369
+ .loading-container,
370
+ .error-container {
371
+ display: flex;
372
+ flex-direction: column;
373
+ align-items: center;
374
+ justify-content: center;
375
+ height: 300px;
376
+ color: #909399;
377
+
378
+ .loading-icon,
379
+ .error-icon {
380
+ font-size: 48px;
381
+ margin-bottom: 16px;
382
+ }
383
+
384
+ p {
385
+ margin: 8px 0 16px;
386
+ font-size: 14px;
387
+ }
388
+ }
389
+
390
+ .image-wrapper {
391
+ position: relative;
392
+ width: 100%;
393
+ height: 100%;
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: center;
397
+ cursor: grab;
398
+
399
+ &:active {
400
+ cursor: grabbing;
401
+ }
402
+
403
+ .preview-image {
404
+ max-width: 100%;
405
+ max-height: 100%;
406
+ border-radius: 4px;
407
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
408
+ transition: all 0.3s ease;
409
+
410
+ &:hover {
411
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
412
+ }
413
+ }
414
+ }
415
+
416
+ .zoom-info {
417
+ position: absolute;
418
+ top: 16px;
419
+ right: 16px;
420
+ background: rgba(0, 0, 0, 0.7);
421
+ color: white;
422
+ padding: 4px 12px;
423
+ border-radius: 4px;
424
+ font-size: 12px;
425
+ font-weight: 500;
426
+ }
427
+
428
+ .operation-tips {
429
+ margin-top: 16px;
430
+ padding: 8px 16px;
431
+ background: #f0f9ff;
432
+ border: 1px solid #b3d8ff;
433
+ border-radius: 4px;
434
+ font-size: 12px;
435
+ color: #409eff;
436
+ text-align: center;
437
+ }
438
+ }
439
+
440
+ // 响应式设计
441
+ @media (max-width: 768px) {
442
+ .xl-image-preview-dialog {
443
+ .dialog-header {
444
+ flex-direction: column;
445
+ gap: 12px;
446
+
447
+ .header-actions {
448
+ flex-wrap: wrap;
449
+ justify-content: center;
450
+ }
451
+ }
452
+
453
+ .image-container {
454
+ min-height: 300px;
455
+ }
456
+
457
+ .operation-tips {
458
+ font-size: 11px;
459
+ padding: 6px 12px;
460
+ }
461
+ }
462
+ }
463
+
464
+ // 动画效果
465
+ .image-wrapper {
466
+ .preview-image {
467
+ animation: fadeIn 0.3s ease-in-out;
468
+ }
469
+ }
470
+
471
+ @keyframes fadeIn {
472
+ from {
473
+ opacity: 0;
474
+ transform: scale(0.9);
475
+ }
476
+ to {
477
+ opacity: 1;
478
+ transform: scale(1);
479
+ }
480
+ }
481
+
482
+ // 滚动条样式
483
+ .image-container::-webkit-scrollbar {
484
+ width: 6px;
485
+ height: 6px;
486
+ }
487
+
488
+ .image-container::-webkit-scrollbar-track {
489
+ background: #f1f1f1;
490
+ border-radius: 3px;
491
+ }
492
+
493
+ .image-container::-webkit-scrollbar-thumb {
494
+ background: #c1c1c1;
495
+ border-radius: 3px;
496
+ }
497
+
498
+ .image-container::-webkit-scrollbar-thumb:hover {
499
+ background: #a8a8a8;
500
+ }
501
+ </style>
34
502
 
35
503
 
36
504
 
@@ -10,7 +10,7 @@ const props = defineProps({
10
10
  type: Number,
11
11
  default: 0,
12
12
  },
13
- frames: {
13
+ tabs: {
14
14
  type: Array,
15
15
  default: () => [],
16
16
  required: true
@@ -32,20 +32,20 @@ watch(tabIndex, (newVal) => {
32
32
  <div class="xl-tab-view">
33
33
  <el-tabs class="tabs" v-model="tabIndex">
34
34
  <el-tab-pane
35
- v-for="(frame, index) in props.frames"
36
- :key="frame.label"
37
- :label="frame.label"
35
+ v-for="(tab, index) in props.tabs"
36
+ :key="tab.label"
37
+ :label="tab.label"
38
38
  :name="String(index)"
39
39
  />
40
40
  </el-tabs>
41
41
  <component
42
- v-for="(frame, index) in frames"
43
- :key="frame.label"
42
+ v-for="(tab, index) in tabs"
43
+ :key="tab.label"
44
44
  v-show="tabIndex === String(index)"
45
- :is="frame.entity"
45
+ :is="tab.entity"
46
46
  :tab-index="tabIndex"
47
47
  :data="data"
48
- class="frame"
48
+ class="tab"
49
49
  />
50
50
  </div>
51
51
  </template>
@@ -71,7 +71,7 @@ watch(tabIndex, (newVal) => {
71
71
  }
72
72
  }
73
73
 
74
- .frame {
74
+ .tab {
75
75
  flex-grow: 1;
76
76
  overflow-y: scroll;
77
77
  display: flex;