haiwei-module-admin 1.2.2 → 1.2.3
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,31 +1,27 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="file-preview">
|
|
3
|
-
<!-- 当fullPath有效时显示内容 -->
|
|
4
3
|
<template v-if="hasValidFullPath">
|
|
5
|
-
<!-- 图片预览链接 -->
|
|
6
4
|
<a v-if="isImage" class="preview-link" @click="showPreviewDialog">
|
|
7
5
|
查看
|
|
8
6
|
</a>
|
|
9
|
-
|
|
10
|
-
<!-- 非图片文件下载 -->
|
|
11
7
|
<nm-file-download v-else :url="downloadUrl" :private="isPrivate" :fileName="fileName" :text="downloadText" :icon="downloadIcon" :size="downloadSize" :type="downloadType" @click="onDownload" />
|
|
12
8
|
</template>
|
|
13
|
-
|
|
14
|
-
<!-- 图片预览对话框 -->
|
|
15
9
|
<nm-dialog
|
|
16
10
|
v-if="isImage"
|
|
17
11
|
ref="previewDialog"
|
|
18
12
|
:visible.sync="previewVisible"
|
|
19
13
|
:title="previewTitle"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
icon="photo"
|
|
15
|
+
width="1200px"
|
|
16
|
+
height="80%"
|
|
17
|
+
fullscreen
|
|
18
|
+
draggable
|
|
19
|
+
no-scrollbar
|
|
23
20
|
:footer="false"
|
|
24
21
|
custom-class="image-preview-dialog"
|
|
25
22
|
@closed="resetState"
|
|
26
23
|
>
|
|
27
24
|
<div class="preview-wrapper">
|
|
28
|
-
<!-- 使用CSS实现图片居中,而不是JS计算 -->
|
|
29
25
|
<div class="preview-container" ref="previewContainer">
|
|
30
26
|
<img
|
|
31
27
|
:src="previewUrl"
|
|
@@ -40,8 +36,6 @@
|
|
|
40
36
|
@dblclick="resetImage"
|
|
41
37
|
/>
|
|
42
38
|
</div>
|
|
43
|
-
|
|
44
|
-
<!-- 操作按钮栏 -->
|
|
45
39
|
<div class="toolbar">
|
|
46
40
|
<button class="toolbar-btn" @click="zoomIn">
|
|
47
41
|
<span class="icon">+</span>
|
|
@@ -51,11 +45,18 @@
|
|
|
51
45
|
<span class="icon">-</span>
|
|
52
46
|
<span class="text">缩小</span>
|
|
53
47
|
</button>
|
|
48
|
+
<button class="toolbar-btn" @click="rotateLeft">
|
|
49
|
+
<span class="icon">↺</span>
|
|
50
|
+
<span class="text">左转90°</span>
|
|
51
|
+
</button>
|
|
52
|
+
<button class="toolbar-btn" @click="rotateRight">
|
|
53
|
+
<span class="icon">↻</span>
|
|
54
|
+
<span class="text">右转90°</span>
|
|
55
|
+
</button>
|
|
54
56
|
<button class="toolbar-btn" @click="resetImage">
|
|
55
57
|
<span class="icon">⟳</span>
|
|
56
58
|
<span class="text">重置</span>
|
|
57
59
|
</button>
|
|
58
|
-
<!-- 非图片文件下载 -->
|
|
59
60
|
<nm-file-download
|
|
60
61
|
v-if="hasValidFullPath"
|
|
61
62
|
:url="downloadUrl"
|
|
@@ -89,147 +90,116 @@ export default {
|
|
|
89
90
|
default: () => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico']
|
|
90
91
|
}
|
|
91
92
|
},
|
|
92
|
-
|
|
93
93
|
data() {
|
|
94
94
|
return {
|
|
95
95
|
previewVisible: false,
|
|
96
96
|
previewUrl: '',
|
|
97
97
|
fileInfo: null,
|
|
98
|
-
|
|
99
|
-
// 图片状态
|
|
100
98
|
imageLoading: false,
|
|
101
99
|
imageError: false,
|
|
102
100
|
loadingTimeout: null,
|
|
103
101
|
|
|
104
|
-
//
|
|
102
|
+
// 图片原始尺寸(transform 计算需要)
|
|
103
|
+
imgNaturalWidth: 0,
|
|
104
|
+
imgNaturalHeight: 0,
|
|
105
|
+
|
|
106
|
+
// 变换状态
|
|
105
107
|
position: { x: 0, y: 0 },
|
|
106
108
|
scale: 1,
|
|
107
109
|
minScale: 0.1,
|
|
108
110
|
maxScale: 5,
|
|
111
|
+
rotation: 0,
|
|
112
|
+
|
|
113
|
+
// 自适应比例
|
|
114
|
+
initScale: 1,
|
|
109
115
|
|
|
110
|
-
// 拖拽状态
|
|
111
116
|
isDragging: false,
|
|
112
|
-
startPosition: { x: 0, y: 0 }
|
|
117
|
+
startPosition: { x: 0, y: 0 },
|
|
118
|
+
enableTransition: true
|
|
113
119
|
}
|
|
114
120
|
},
|
|
115
|
-
|
|
116
121
|
computed: {
|
|
117
|
-
|
|
122
|
+
rotationRad() {
|
|
123
|
+
return (this.rotation * Math.PI) / 180
|
|
124
|
+
},
|
|
118
125
|
normalizedPath() {
|
|
119
126
|
if (!this.fullPath) return null
|
|
120
127
|
return this.fullPath.replace(/\\/g, '/')
|
|
121
128
|
},
|
|
122
|
-
|
|
123
|
-
// 缓存文件名,避免重复提取
|
|
124
129
|
fileName() {
|
|
125
130
|
if (!this.normalizedPath) return ''
|
|
126
131
|
const filename = this.normalizedPath.split('/').pop()
|
|
127
132
|
return filename || ''
|
|
128
133
|
},
|
|
129
|
-
|
|
130
|
-
// 缓存文件扩展名
|
|
131
134
|
fileExt() {
|
|
132
135
|
const filename = this.fileName
|
|
133
136
|
if (!filename) return ''
|
|
134
137
|
const dotIndex = filename.lastIndexOf('.')
|
|
135
138
|
return dotIndex > -1 ? filename.slice(dotIndex + 1).toLowerCase() : ''
|
|
136
139
|
},
|
|
137
|
-
|
|
138
140
|
hasValidFullPath() {
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
return false
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 检查是否是目录路径(以斜杠结尾)
|
|
145
|
-
if (this.fullPath.endsWith('/') || this.fullPath.endsWith('\\')) {
|
|
146
|
-
return false
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 使用已计算的文件名
|
|
141
|
+
if (!this.fullPath || this.fullPath.trim() === '') return false
|
|
142
|
+
if (this.fullPath.endsWith('/') || this.fullPath.endsWith('\\')) return false
|
|
150
143
|
const filename = this.fileName
|
|
151
|
-
|
|
152
|
-
// 文件名不能为空
|
|
153
|
-
if (!filename || filename.trim() === '') {
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
|
|
144
|
+
if (!filename || filename.trim() === '') return false
|
|
157
145
|
return true
|
|
158
146
|
},
|
|
159
|
-
|
|
160
147
|
isImage() {
|
|
161
148
|
return this.imageExtensions.includes(this.fileExt)
|
|
162
149
|
},
|
|
163
|
-
|
|
164
150
|
downloadUrl() {
|
|
165
|
-
// console.log('this.fileInfo?.url ', this.fileInfo?.url)
|
|
166
151
|
return this.fileInfo?.url || this.fullPath
|
|
167
152
|
},
|
|
168
|
-
|
|
169
153
|
imageStyle() {
|
|
170
154
|
return {
|
|
171
|
-
transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.scale})`,
|
|
172
|
-
|
|
155
|
+
transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.scale}) rotate(${this.rotation}deg)`,
|
|
156
|
+
transformOrigin: 'center center',
|
|
157
|
+
cursor: this.isDragging ? 'grabbing' : 'grab',
|
|
158
|
+
transition: this.enableTransition ? 'transform 0.2s ease' : 'none'
|
|
173
159
|
}
|
|
174
160
|
}
|
|
175
161
|
},
|
|
176
|
-
|
|
177
162
|
watch: {
|
|
178
163
|
fullPath(val) {
|
|
179
164
|
if (val && this.isImage && !this.isPrivate) {
|
|
180
165
|
this.previewUrl = val
|
|
181
166
|
}
|
|
182
167
|
},
|
|
183
|
-
|
|
184
168
|
previewVisible(val) {
|
|
185
169
|
if (!val) {
|
|
186
170
|
this.resetState()
|
|
187
171
|
}
|
|
188
172
|
}
|
|
189
173
|
},
|
|
190
|
-
|
|
191
174
|
mounted() {
|
|
192
|
-
// 监听全局鼠标事件用于拖拽
|
|
193
175
|
document.addEventListener('mousemove', this.handleDrag)
|
|
194
176
|
document.addEventListener('mouseup', this.stopDrag)
|
|
195
177
|
},
|
|
196
|
-
|
|
197
178
|
beforeDestroy() {
|
|
198
|
-
// 清理事件监听
|
|
199
179
|
document.removeEventListener('mousemove', this.handleDrag)
|
|
200
180
|
document.removeEventListener('mouseup', this.stopDrag)
|
|
201
|
-
|
|
202
|
-
// 清理超时
|
|
203
181
|
if (this.loadingTimeout) {
|
|
204
182
|
clearTimeout(this.loadingTimeout)
|
|
205
183
|
this.loadingTimeout = null
|
|
206
184
|
}
|
|
207
185
|
},
|
|
208
|
-
|
|
209
186
|
methods: {
|
|
210
187
|
async getFileInfo() {
|
|
211
188
|
if (!this.fullPath) return null
|
|
212
|
-
|
|
213
189
|
try {
|
|
214
190
|
const result = await $api.admin.file.getByFullPath(this.fullPath)
|
|
215
191
|
this.fileInfo = result
|
|
216
|
-
|
|
217
|
-
console.log('result', result)
|
|
218
|
-
|
|
219
192
|
return result
|
|
220
193
|
} catch (error) {
|
|
221
194
|
console.error('获取文件信息失败:', error)
|
|
222
195
|
return null
|
|
223
196
|
}
|
|
224
197
|
},
|
|
225
|
-
|
|
226
198
|
async showPreviewDialog() {
|
|
227
199
|
if (!this.isImage) return
|
|
228
|
-
|
|
229
200
|
this.resetState()
|
|
230
201
|
this.imageLoading = true
|
|
231
202
|
this.previewVisible = true
|
|
232
|
-
|
|
233
203
|
try {
|
|
234
204
|
const fileInfo = await this.getFileInfo()
|
|
235
205
|
if (!fileInfo) {
|
|
@@ -237,36 +207,29 @@ export default {
|
|
|
237
207
|
this.imageError = true
|
|
238
208
|
return
|
|
239
209
|
}
|
|
240
|
-
|
|
241
210
|
await this.setPreviewUrl(fileInfo)
|
|
242
|
-
|
|
243
|
-
// 设置一个立即检查,防止缓存图片不触发load事件
|
|
244
211
|
this.$nextTick(() => {
|
|
245
212
|
setTimeout(() => {
|
|
246
213
|
if (this.imageLoading && this.$refs.previewImage && this.$refs.previewImage.complete) {
|
|
247
214
|
this.handleImageLoad()
|
|
248
215
|
}
|
|
249
|
-
}, 300)
|
|
216
|
+
}, 300)
|
|
250
217
|
})
|
|
251
|
-
|
|
252
|
-
// 设置超时,防止图片加载事件未触发
|
|
253
218
|
this.loadingTimeout = setTimeout(() => {
|
|
254
219
|
if (this.imageLoading) {
|
|
255
220
|
console.warn('图片加载超时,强制结束加载状态')
|
|
256
221
|
this.imageLoading = false
|
|
257
|
-
// 检查图片是否已经加载完成
|
|
258
222
|
if (this.$refs.previewImage && this.$refs.previewImage.complete) {
|
|
259
223
|
this.handleImageLoad()
|
|
260
224
|
}
|
|
261
225
|
}
|
|
262
|
-
}, 2000)
|
|
226
|
+
}, 2000)
|
|
263
227
|
} catch (error) {
|
|
264
228
|
console.error('打开预览对话框失败:', error)
|
|
265
229
|
this.imageLoading = false
|
|
266
230
|
this.imageError = true
|
|
267
231
|
}
|
|
268
232
|
},
|
|
269
|
-
|
|
270
233
|
async setPreviewUrl(fileInfo) {
|
|
271
234
|
let newUrl
|
|
272
235
|
if (this.isPrivate && fileInfo.url) {
|
|
@@ -278,22 +241,13 @@ export default {
|
|
|
278
241
|
} else {
|
|
279
242
|
newUrl = fileInfo.url || this.fullPath
|
|
280
243
|
}
|
|
281
|
-
|
|
282
|
-
// 先清空URL,强制重新加载(即使URL相同)
|
|
283
244
|
if (this.previewUrl) {
|
|
284
245
|
this.previewUrl = ''
|
|
285
|
-
// 等待一个tick确保DOM更新
|
|
286
246
|
await this.$nextTick()
|
|
287
247
|
}
|
|
288
|
-
|
|
289
|
-
// 设置新的URL
|
|
290
248
|
this.previewUrl = newUrl
|
|
291
|
-
|
|
292
|
-
// 等待DOM更新后检查图片是否已经加载完成
|
|
293
249
|
this.$nextTick(() => {
|
|
294
|
-
// 如果图片元素存在且已经加载完成,直接触发加载完成事件
|
|
295
250
|
if (this.$refs.previewImage && this.$refs.previewImage.complete) {
|
|
296
|
-
// 小延迟确保图片完全渲染
|
|
297
251
|
setTimeout(() => {
|
|
298
252
|
if (this.imageLoading) {
|
|
299
253
|
this.handleImageLoad()
|
|
@@ -302,26 +256,51 @@ export default {
|
|
|
302
256
|
}
|
|
303
257
|
})
|
|
304
258
|
},
|
|
305
|
-
|
|
306
259
|
async onDownload() {
|
|
307
260
|
const fileInfo = await this.getFileInfo()
|
|
308
261
|
const url = fileInfo?.url || this.fullPath
|
|
309
262
|
this.$emit('download', url, this.fileName)
|
|
310
263
|
},
|
|
311
264
|
|
|
265
|
+
// ✅ 图片加载完成:自适应铺满 + 居中
|
|
312
266
|
handleImageLoad() {
|
|
313
|
-
// 清理超时
|
|
314
267
|
if (this.loadingTimeout) {
|
|
315
268
|
clearTimeout(this.loadingTimeout)
|
|
316
269
|
this.loadingTimeout = null
|
|
317
270
|
}
|
|
318
271
|
this.imageLoading = false
|
|
319
|
-
|
|
320
|
-
|
|
272
|
+
this.$nextTick(() => {
|
|
273
|
+
const img = this.$refs.previewImage
|
|
274
|
+
const container = this.$refs.previewContainer
|
|
275
|
+
if (!img || !container) return
|
|
276
|
+
|
|
277
|
+
const cw = container.clientWidth
|
|
278
|
+
const ch = container.clientHeight
|
|
279
|
+
const iw = img.naturalWidth
|
|
280
|
+
const ih = img.naturalHeight
|
|
281
|
+
if (!iw || !ih || !cw || !ch) return
|
|
282
|
+
|
|
283
|
+
// 记录原始尺寸(后续 zoom/rotate 需要)
|
|
284
|
+
this.imgNaturalWidth = iw
|
|
285
|
+
this.imgNaturalHeight = ih
|
|
286
|
+
|
|
287
|
+
// ✅ 自适应比例:contain 铺满容器
|
|
288
|
+
this.initScale = Math.min(cw / iw, ch / ih)
|
|
289
|
+
this.scale = this.initScale
|
|
290
|
+
this.rotation = 0
|
|
291
|
+
|
|
292
|
+
// ✅ 动态缩放边界:自适应比例 × [0.1, 10],至少允许 5 倍放大
|
|
293
|
+
this.minScale = this.initScale * 0.1
|
|
294
|
+
this.maxScale = Math.max(this.initScale * 10, 5)
|
|
295
|
+
|
|
296
|
+
// ✅ 居中:transform-origin 是 center center,
|
|
297
|
+
// 图片布局盒子的中心在 (iw/2, ih/2),需要平移到 (cw/2, ch/2)
|
|
298
|
+
this.position.x = (cw - iw) / 2
|
|
299
|
+
this.position.y = (ch - ih) / 2
|
|
300
|
+
})
|
|
321
301
|
},
|
|
322
302
|
|
|
323
303
|
handleImageError() {
|
|
324
|
-
// 清理超时
|
|
325
304
|
if (this.loadingTimeout) {
|
|
326
305
|
clearTimeout(this.loadingTimeout)
|
|
327
306
|
this.loadingTimeout = null
|
|
@@ -330,36 +309,42 @@ export default {
|
|
|
330
309
|
this.imageError = true
|
|
331
310
|
},
|
|
332
311
|
|
|
333
|
-
|
|
312
|
+
/**
|
|
313
|
+
* 以 (anchorX, anchorY) 为锚点缩放。
|
|
314
|
+
* 在 transform-origin: center center 的变换模型下:
|
|
315
|
+
* p' = O + t + s·R·(p − O)
|
|
316
|
+
* 缩放前后锚点 p 在容器坐标不变,可解得:
|
|
317
|
+
* t_new = q − O − (s_new / s_old)·(q − O − t_old)
|
|
318
|
+
* 其中 O = (iw/2, ih/2) 为图片布局盒子中心。
|
|
319
|
+
*/
|
|
320
|
+
zoomTo(newScale, anchorX, anchorY) {
|
|
321
|
+
newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale))
|
|
322
|
+
if (Math.abs(this.scale - newScale) < 0.0001) return
|
|
323
|
+
|
|
324
|
+
const Ox = this.imgNaturalWidth / 2
|
|
325
|
+
const Oy = this.imgNaturalHeight / 2
|
|
326
|
+
const k = newScale / this.scale
|
|
327
|
+
|
|
328
|
+
this.position.x = anchorX - Ox - k * (anchorX - Ox - this.position.x)
|
|
329
|
+
this.position.y = anchorY - Oy - k * (anchorY - Oy - this.position.y)
|
|
330
|
+
this.scale = newScale
|
|
331
|
+
},
|
|
332
|
+
|
|
334
333
|
handleWheel(event) {
|
|
335
334
|
event.preventDefault()
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
const newScale =
|
|
339
|
-
|
|
340
|
-
// 计算缩放中心(相对于图片)
|
|
335
|
+
// ✅ 乘法缩放:手感顺滑,且不受当前 scale 大小影响
|
|
336
|
+
const factor = event.deltaY < 0 ? 1.15 : 1 / 1.15
|
|
337
|
+
const newScale = this.scale * factor
|
|
341
338
|
if (this.$refs.previewContainer) {
|
|
342
|
-
const
|
|
343
|
-
const mouseX = event.clientX -
|
|
344
|
-
const mouseY = event.clientY -
|
|
345
|
-
|
|
346
|
-
// 计算鼠标相对于图片原始坐标系的位置
|
|
347
|
-
// 注意:position.x 和 position.y 是图片相对于容器的平移
|
|
348
|
-
const relativeX = (mouseX - this.position.x) / this.scale
|
|
349
|
-
const relativeY = (mouseY - this.position.y) / this.scale
|
|
350
|
-
|
|
351
|
-
// 更新位置以保持鼠标点不变
|
|
352
|
-
this.position.x = mouseX - relativeX * newScale
|
|
353
|
-
this.position.y = mouseY - relativeY * newScale
|
|
354
|
-
this.scale = newScale
|
|
355
|
-
} else {
|
|
356
|
-
this.scale = newScale
|
|
339
|
+
const rect = this.$refs.previewContainer.getBoundingClientRect()
|
|
340
|
+
const mouseX = event.clientX - rect.left
|
|
341
|
+
const mouseY = event.clientY - rect.top
|
|
342
|
+
this.zoomTo(newScale, mouseX, mouseY)
|
|
357
343
|
}
|
|
358
344
|
},
|
|
359
345
|
|
|
360
|
-
// 开始拖拽
|
|
361
346
|
startDrag(event) {
|
|
362
|
-
if (event.button !== 0) return
|
|
347
|
+
if (event.button !== 0) return
|
|
363
348
|
this.isDragging = true
|
|
364
349
|
this.startPosition = {
|
|
365
350
|
x: event.clientX - this.position.x,
|
|
@@ -367,73 +352,87 @@ export default {
|
|
|
367
352
|
}
|
|
368
353
|
event.preventDefault()
|
|
369
354
|
},
|
|
370
|
-
|
|
371
|
-
// 处理拖拽
|
|
372
355
|
handleDrag(event) {
|
|
373
356
|
if (!this.isDragging) return
|
|
374
|
-
|
|
375
357
|
this.position.x = event.clientX - this.startPosition.x
|
|
376
358
|
this.position.y = event.clientY - this.startPosition.y
|
|
377
359
|
},
|
|
378
|
-
|
|
379
|
-
// 停止拖拽
|
|
380
360
|
stopDrag() {
|
|
381
361
|
this.isDragging = false
|
|
382
362
|
},
|
|
383
363
|
|
|
384
|
-
// 放大
|
|
385
364
|
zoomIn() {
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
365
|
+
const container = this.$refs.previewContainer
|
|
366
|
+
if (!container) return
|
|
367
|
+
const cx = container.clientWidth / 2
|
|
368
|
+
const cy = container.clientHeight / 2
|
|
369
|
+
// ✅ 乘法缩放:每次放大 20%
|
|
370
|
+
this.zoomTo(this.scale * 1.2, cx, cy)
|
|
390
371
|
},
|
|
391
|
-
|
|
392
|
-
// 缩小
|
|
393
372
|
zoomOut() {
|
|
394
|
-
const
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
373
|
+
const container = this.$refs.previewContainer
|
|
374
|
+
if (!container) return
|
|
375
|
+
const cx = container.clientWidth / 2
|
|
376
|
+
const cy = container.clientHeight / 2
|
|
377
|
+
this.zoomTo(this.scale / 1.2, cx, cy)
|
|
398
378
|
},
|
|
399
379
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
380
|
+
rotateLeft() {
|
|
381
|
+
this.enableTransition = false
|
|
382
|
+
this.rotation = (this.rotation - 90 + 360) % 360
|
|
383
|
+
this.$nextTick(() => {
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
this.enableTransition = true
|
|
386
|
+
}, 0)
|
|
387
|
+
})
|
|
388
|
+
},
|
|
389
|
+
rotateRight() {
|
|
390
|
+
this.enableTransition = false
|
|
391
|
+
this.rotation = (this.rotation + 90) % 360
|
|
392
|
+
this.$nextTick(() => {
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
this.enableTransition = true
|
|
395
|
+
}, 0)
|
|
396
|
+
})
|
|
417
397
|
},
|
|
418
398
|
|
|
419
|
-
//
|
|
399
|
+
// ✅ 重置:角度归零,scale 回到自适应比例,重新居中
|
|
420
400
|
resetImage() {
|
|
421
|
-
|
|
422
|
-
|
|
401
|
+
const img = this.$refs.previewImage
|
|
402
|
+
const container = this.$refs.previewContainer
|
|
403
|
+
if (!img || !container) return
|
|
404
|
+
|
|
405
|
+
const cw = container.clientWidth
|
|
406
|
+
const ch = container.clientHeight
|
|
407
|
+
const iw = img.naturalWidth || this.imgNaturalWidth
|
|
408
|
+
const ih = img.naturalHeight || this.imgNaturalHeight
|
|
409
|
+
if (!iw || !ih || !cw || !ch) return
|
|
410
|
+
|
|
411
|
+
this.rotation = 0
|
|
412
|
+
this.scale = this.initScale
|
|
413
|
+
|
|
414
|
+
// transform-origin: center center,居中平移量 = (cw - iw)/2, (ch - ih)/2
|
|
415
|
+
this.position.x = (cw - iw) / 2
|
|
416
|
+
this.position.y = (ch - ih) / 2
|
|
423
417
|
},
|
|
424
418
|
|
|
425
|
-
// 重置所有状态
|
|
426
419
|
resetState() {
|
|
427
|
-
// 清理超时
|
|
428
420
|
if (this.loadingTimeout) {
|
|
429
421
|
clearTimeout(this.loadingTimeout)
|
|
430
422
|
this.loadingTimeout = null
|
|
431
423
|
}
|
|
432
424
|
this.position = { x: 0, y: 0 }
|
|
433
425
|
this.scale = 1
|
|
426
|
+
this.minScale = 0.1
|
|
427
|
+
this.maxScale = 5
|
|
428
|
+
this.rotation = 0
|
|
429
|
+
this.initScale = 1
|
|
430
|
+
this.imgNaturalWidth = 0
|
|
431
|
+
this.imgNaturalHeight = 0
|
|
434
432
|
this.imageLoading = false
|
|
435
433
|
this.imageError = false
|
|
436
434
|
this.isDragging = false
|
|
435
|
+
this.enableTransition = true
|
|
437
436
|
}
|
|
438
437
|
}
|
|
439
438
|
}
|
|
@@ -442,59 +441,42 @@ export default {
|
|
|
442
441
|
<style lang="scss" scoped>
|
|
443
442
|
.file-preview {
|
|
444
443
|
display: inline-block;
|
|
445
|
-
|
|
446
444
|
.preview-link {
|
|
447
445
|
color: #409eff;
|
|
448
446
|
cursor: pointer;
|
|
449
447
|
text-decoration: none;
|
|
450
448
|
transition: color 0.2s;
|
|
451
|
-
|
|
452
449
|
&:hover {
|
|
453
450
|
color: #66b1ff;
|
|
454
451
|
text-decoration: underline;
|
|
455
452
|
}
|
|
456
453
|
}
|
|
457
454
|
}
|
|
458
|
-
|
|
459
455
|
.preview-wrapper {
|
|
460
456
|
width: 100%;
|
|
461
|
-
height
|
|
457
|
+
/* 撑满对话框内容区(高度由 nm-dialog 的 height 决定),
|
|
458
|
+
不写固定高度,避免弹窗缩放/全屏后图片区与内容区高度不一致 */
|
|
459
|
+
height: 100%;
|
|
462
460
|
background: #000;
|
|
463
461
|
position: relative;
|
|
464
462
|
overflow: hidden;
|
|
465
|
-
display: flex;
|
|
466
|
-
align-items: center;
|
|
467
|
-
justify-content: center;
|
|
468
463
|
}
|
|
469
|
-
|
|
470
464
|
.preview-container {
|
|
471
465
|
width: 100%;
|
|
472
466
|
height: 100%;
|
|
473
|
-
display: flex;
|
|
474
|
-
align-items: center;
|
|
475
|
-
justify-content: center;
|
|
476
467
|
position: relative;
|
|
477
468
|
overflow: hidden;
|
|
478
469
|
touch-action: none;
|
|
479
|
-
/* 防止移动端默认行为 */
|
|
480
470
|
}
|
|
481
|
-
|
|
482
471
|
.preview-image {
|
|
483
|
-
max-width: 100%;
|
|
484
|
-
max-height: 100%;
|
|
485
|
-
object-fit: contain;
|
|
486
|
-
/* 关键:保持比例并适应容器 */
|
|
487
|
-
transform-origin: 0 0;
|
|
488
|
-
/* 设置为左上角以匹配我们的数学计算 */
|
|
489
|
-
transition: transform 0.2s ease;
|
|
490
472
|
user-select: none;
|
|
491
473
|
-webkit-user-drag: none;
|
|
492
|
-
|
|
493
|
-
/* 平滑的拖拽和缩放效果 */
|
|
494
474
|
will-change: transform;
|
|
475
|
+
/* ✅ 绝对定位在左上角,保证 transform 计算以容器 (0,0) 为基准 */
|
|
476
|
+
position: absolute;
|
|
477
|
+
top: 0;
|
|
478
|
+
left: 0;
|
|
495
479
|
}
|
|
496
|
-
|
|
497
|
-
/* 操作工具栏 */
|
|
498
480
|
.toolbar {
|
|
499
481
|
position: absolute;
|
|
500
482
|
bottom: 20px;
|
|
@@ -507,7 +489,6 @@ export default {
|
|
|
507
489
|
border-radius: 4px;
|
|
508
490
|
z-index: 10;
|
|
509
491
|
}
|
|
510
|
-
|
|
511
492
|
.toolbar-btn {
|
|
512
493
|
display: flex;
|
|
513
494
|
align-items: center;
|
|
@@ -521,21 +502,17 @@ export default {
|
|
|
521
502
|
cursor: pointer;
|
|
522
503
|
font-size: 12px;
|
|
523
504
|
transition: all 0.2s;
|
|
524
|
-
|
|
525
505
|
&:hover {
|
|
526
506
|
background: rgba(255, 255, 255, 0.2);
|
|
527
507
|
border-color: rgba(255, 255, 255, 0.3);
|
|
528
508
|
}
|
|
529
|
-
|
|
530
509
|
&:active {
|
|
531
510
|
transform: scale(0.95);
|
|
532
511
|
}
|
|
533
|
-
|
|
534
512
|
.icon {
|
|
535
513
|
font-size: 14px;
|
|
536
514
|
font-weight: bold;
|
|
537
515
|
}
|
|
538
|
-
|
|
539
516
|
.text {
|
|
540
517
|
white-space: nowrap;
|
|
541
518
|
}
|
|
@@ -543,23 +520,38 @@ export default {
|
|
|
543
520
|
</style>
|
|
544
521
|
|
|
545
522
|
<style lang="scss">
|
|
546
|
-
/* 对话框全局样式 */
|
|
547
523
|
.image-preview-dialog {
|
|
548
524
|
.el-dialog {
|
|
549
525
|
background: #000;
|
|
550
526
|
border: 1px solid #333;
|
|
551
527
|
}
|
|
552
|
-
|
|
553
528
|
.el-dialog__header {
|
|
554
529
|
background: #000;
|
|
555
530
|
border-bottom: 1px solid #333;
|
|
556
531
|
padding: 10px 20px;
|
|
557
532
|
|
|
558
|
-
|
|
533
|
+
/* nm-dialog 的标题结构:图标 + 标题 + 工具栏(全屏/关闭按钮) */
|
|
534
|
+
.nm-dialog-title {
|
|
559
535
|
color: #fff;
|
|
560
536
|
font-size: 14px;
|
|
537
|
+
cursor: move; /* 与 draggable 配合,提示可拖动 */
|
|
561
538
|
}
|
|
562
539
|
|
|
540
|
+
.nm-dialog-icon {
|
|
541
|
+
color: #fff;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* 工具栏里的全屏/关闭按钮:深色底上要显示为白色,
|
|
545
|
+
否则继承框架默认深色图标导致看不见 */
|
|
546
|
+
.nm-dialog-toolbar .nm-button {
|
|
547
|
+
color: #fff;
|
|
548
|
+
|
|
549
|
+
&:hover {
|
|
550
|
+
color: #409eff;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* 兼容 element-ui 原生关闭按钮 */
|
|
563
555
|
.el-dialog__close {
|
|
564
556
|
color: #fff;
|
|
565
557
|
|
|
@@ -568,11 +560,10 @@ export default {
|
|
|
568
560
|
}
|
|
569
561
|
}
|
|
570
562
|
}
|
|
571
|
-
|
|
572
563
|
.el-dialog__body {
|
|
573
564
|
padding: 0 !important;
|
|
574
565
|
background: #000 !important;
|
|
575
566
|
overflow: hidden !important;
|
|
576
567
|
}
|
|
577
568
|
}
|
|
578
|
-
</style>
|
|
569
|
+
</style>
|