haiwei-module-admin 1.0.1 → 1.0.2

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
@@ -2,9 +2,9 @@
2
2
  "id": 0,
3
3
  "name": "haiwei-module-admin",
4
4
  "code": "admin",
5
- "version": "1.0.1",
5
+ "version": "1.0.2",
6
6
  "description": "haiwei前端Admin模块组件",
7
- "author": "Oldli",
7
+ "author": "Eric",
8
8
  "license": "ISC",
9
9
  "main": "src/index.js",
10
10
  "scripts": {
@@ -13,13 +13,13 @@
13
13
  "lint": "vue-cli-service lint",
14
14
  "cm": "rimraf node_modules",
15
15
  "cc": "rimraf node_modules/.cache",
16
- "i": "cd ./script && npm_install.ps1",
17
- "up": "cd ./script && npm_update.ps1",
18
- "pub": "cd ./script && npm_publish.ps1"
16
+ "i": "cd ./script && npm_install.bat",
17
+ "up": "cd ./script && npm_update.bat",
18
+ "pub": "cd ./script && npm_publish.bat"
19
19
  },
20
20
  "dependencies": {
21
21
  "haiwei-skins-classics": "^1.0.2",
22
- "haiwei-ui": "^1.0.2"
22
+ "haiwei-ui": "^1.0.5"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@vue/cli-plugin-babel": "^4.4.4",
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM 执行 npm 安装脚本
3
+ powershell -ExecutionPolicy Bypass -File "%~dp0npm_install.ps1"
4
+ pause
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM 执行 npm 发布脚本
3
+ powershell -ExecutionPolicy Bypass -File "%~dp0npm_publish.ps1"
4
+ pause
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ REM 执行 npm 更新脚本
3
+ powershell -ExecutionPolicy Bypass -File "%~dp0npm_update.ps1"
4
+ pause
@@ -3,6 +3,9 @@ import module from '../../module'
3
3
  export default name => {
4
4
  const root = `${module.code}/${name}/`
5
5
  const crud = $http.crud(root)
6
+
7
+ console.log('crud', crud)
8
+
6
9
  const urls = {
7
10
  upload: root + 'Upload',
8
11
  hardDelete: root + 'HardDelete'
@@ -0,0 +1,531 @@
1
+ <template>
2
+ <div class="file-preview">
3
+ <!-- 图片预览链接 -->
4
+ <a v-if="isImage" class="preview-link" @click="showPreviewDialog">
5
+ 查看
6
+ </a>
7
+
8
+ <!-- 非图片文件下载 -->
9
+ <nm-file-download v-else :url="downloadUrl" :private="isPrivate" :fileName="fileName" :text="downloadText" :icon="downloadIcon" :size="downloadSize" :type="downloadType" @click="onDownload" />
10
+
11
+ <!-- 图片预览对话框 -->
12
+ <nm-dialog
13
+ v-if="isImage"
14
+ ref="previewDialog"
15
+ :visible.sync="previewVisible"
16
+ :title="previewTitle"
17
+ width="800px"
18
+ height="800px"
19
+ :fullscreen="false"
20
+ :footer="false"
21
+ custom-class="image-preview-dialog"
22
+ @closed="resetState"
23
+ >
24
+ <div class="preview-wrapper">
25
+ <!-- 使用CSS实现图片居中,而不是JS计算 -->
26
+ <div class="preview-container" ref="previewContainer">
27
+ <img
28
+ :src="previewUrl"
29
+ :alt="fileName"
30
+ class="preview-image"
31
+ ref="previewImage"
32
+ @load="handleImageLoad"
33
+ @error="handleImageError"
34
+ :style="imageStyle"
35
+ @mousedown="startDrag"
36
+ @wheel="handleWheel"
37
+ @dblclick="resetImage"
38
+ />
39
+ </div>
40
+
41
+ <!-- 操作按钮栏 -->
42
+ <div class="toolbar">
43
+ <button class="toolbar-btn" @click="zoomIn">
44
+ <span class="icon">+</span>
45
+ <span class="text">放大</span>
46
+ </button>
47
+ <button class="toolbar-btn" @click="zoomOut">
48
+ <span class="icon">-</span>
49
+ <span class="text">缩小</span>
50
+ </button>
51
+ <button class="toolbar-btn" @click="resetImage">
52
+ <span class="icon">⟳</span>
53
+ <span class="text">重置</span>
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </nm-dialog>
58
+ </div>
59
+ </template>
60
+
61
+ <script>
62
+ export default {
63
+ name: 'FilePreview',
64
+ props: {
65
+ fullPath: { type: String, required: true },
66
+ fileName: String,
67
+ isPrivate: { type: Boolean, default: false },
68
+ ext: String,
69
+ downloadText: { type: String, default: '下载' },
70
+ downloadIcon: { type: String, default: 'download' },
71
+ downloadSize: String,
72
+ downloadType: { type: String, default: 'primary' },
73
+ previewTitle: { type: String, default: '图片预览' },
74
+ imageExtensions: {
75
+ type: Array,
76
+ default: () => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico']
77
+ }
78
+ },
79
+
80
+ data() {
81
+ return {
82
+ previewVisible: false,
83
+ previewUrl: '',
84
+ fileInfo: null,
85
+
86
+ // 图片状态
87
+ imageLoading: false,
88
+ imageError: false,
89
+ loadingTimeout: null,
90
+
91
+ // 图片位置和缩放(使用CSS transform,但让CSS处理居中)
92
+ position: { x: 0, y: 0 },
93
+ scale: 1,
94
+ minScale: 0.1,
95
+ maxScale: 5,
96
+
97
+ // 拖拽状态
98
+ isDragging: false,
99
+ startPosition: { x: 0, y: 0 }
100
+ }
101
+ },
102
+
103
+ computed: {
104
+ isImage() {
105
+ // return false;
106
+ const ext = this.ext || (this.fileName || '').split('.').pop() || ''
107
+ return this.imageExtensions.includes(ext.toLowerCase())
108
+ },
109
+
110
+ downloadUrl() {
111
+ console.log('this.fileInfo?.url ', this.fileInfo?.url)
112
+ return this.fileInfo?.url || this.fullPath
113
+ },
114
+
115
+ imageStyle() {
116
+ return {
117
+ transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.scale})`,
118
+ cursor: this.isDragging ? 'grabbing' : 'grab'
119
+ }
120
+ }
121
+ },
122
+
123
+ watch: {
124
+ fullPath(val) {
125
+ if (val && this.isImage && !this.isPrivate) {
126
+ this.previewUrl = val
127
+ }
128
+ },
129
+
130
+ previewVisible(val) {
131
+ if (!val) {
132
+ this.resetState()
133
+ }
134
+ }
135
+ },
136
+
137
+ mounted() {
138
+ // 监听全局鼠标事件用于拖拽
139
+ document.addEventListener('mousemove', this.handleDrag)
140
+ document.addEventListener('mouseup', this.stopDrag)
141
+ },
142
+
143
+ beforeDestroy() {
144
+ // 清理事件监听
145
+ document.removeEventListener('mousemove', this.handleDrag)
146
+ document.removeEventListener('mouseup', this.stopDrag)
147
+
148
+ // 清理超时
149
+ if (this.loadingTimeout) {
150
+ clearTimeout(this.loadingTimeout)
151
+ this.loadingTimeout = null
152
+ }
153
+ },
154
+
155
+ methods: {
156
+ getFileExtension() {
157
+ if (this.ext) return this.ext.toLowerCase()
158
+ const name = this.fileName || this.fullPath || ''
159
+ const dotIndex = name.lastIndexOf('.')
160
+ return dotIndex > -1 ? name.slice(dotIndex + 1).toLowerCase() : ''
161
+ },
162
+
163
+ async getFileInfo() {
164
+ if (!this.fullPath) return null
165
+
166
+ try {
167
+ const result = await $api.admin.file.getByFullPath(this.fullPath)
168
+ this.fileInfo = result
169
+
170
+ console.log('result', result)
171
+
172
+ return result
173
+ } catch (error) {
174
+ console.error('获取文件信息失败:', error)
175
+ return null
176
+ }
177
+ },
178
+
179
+ async showPreviewDialog() {
180
+ if (!this.isImage) return
181
+
182
+ this.resetState()
183
+ this.imageLoading = true
184
+ this.previewVisible = true
185
+
186
+ try {
187
+ const fileInfo = await this.getFileInfo()
188
+ if (!fileInfo) {
189
+ this.imageLoading = false
190
+ this.imageError = true
191
+ return
192
+ }
193
+
194
+ await this.setPreviewUrl(fileInfo)
195
+
196
+ // 设置一个立即检查,防止缓存图片不触发load事件
197
+ this.$nextTick(() => {
198
+ setTimeout(() => {
199
+ if (this.imageLoading && this.$refs.previewImage && this.$refs.previewImage.complete) {
200
+ this.handleImageLoad()
201
+ }
202
+ }, 300) // 300ms后检查
203
+ })
204
+
205
+ // 设置超时,防止图片加载事件未触发
206
+ this.loadingTimeout = setTimeout(() => {
207
+ if (this.imageLoading) {
208
+ console.warn('图片加载超时,强制结束加载状态')
209
+ this.imageLoading = false
210
+ // 检查图片是否已经加载完成
211
+ if (this.$refs.previewImage && this.$refs.previewImage.complete) {
212
+ this.handleImageLoad()
213
+ }
214
+ }
215
+ }, 2000) // 2秒超时
216
+ } catch (error) {
217
+ console.error('打开预览对话框失败:', error)
218
+ this.imageLoading = false
219
+ this.imageError = true
220
+ }
221
+ },
222
+
223
+ async setPreviewUrl(fileInfo) {
224
+ let newUrl
225
+ if (this.isPrivate && fileInfo.url) {
226
+ try {
227
+ newUrl = await $api.admin.file.preview(fileInfo.url)
228
+ } catch {
229
+ newUrl = fileInfo.url
230
+ }
231
+ } else {
232
+ newUrl = fileInfo.url || this.fullPath
233
+ }
234
+
235
+ // 先清空URL,强制重新加载(即使URL相同)
236
+ if (this.previewUrl) {
237
+ this.previewUrl = ''
238
+ // 等待一个tick确保DOM更新
239
+ await this.$nextTick()
240
+ }
241
+
242
+ // 设置新的URL
243
+ this.previewUrl = newUrl
244
+
245
+ // 等待DOM更新后检查图片是否已经加载完成
246
+ this.$nextTick(() => {
247
+ // 如果图片元素存在且已经加载完成,直接触发加载完成事件
248
+ if (this.$refs.previewImage && this.$refs.previewImage.complete) {
249
+ // 小延迟确保图片完全渲染
250
+ setTimeout(() => {
251
+ if (this.imageLoading) {
252
+ this.handleImageLoad()
253
+ }
254
+ }, 100)
255
+ }
256
+ })
257
+ },
258
+
259
+ async onDownload() {
260
+ const fileInfo = await this.getFileInfo()
261
+ const url = fileInfo?.url || this.fullPath
262
+ this.$emit('download', url, this.fileName)
263
+ },
264
+
265
+ handleImageLoad() {
266
+ // 清理超时
267
+ if (this.loadingTimeout) {
268
+ clearTimeout(this.loadingTimeout)
269
+ this.loadingTimeout = null
270
+ }
271
+ this.imageLoading = false
272
+ // 图片加载完成后自动居中
273
+ this.resetImage()
274
+ },
275
+
276
+ handleImageError() {
277
+ // 清理超时
278
+ if (this.loadingTimeout) {
279
+ clearTimeout(this.loadingTimeout)
280
+ this.loadingTimeout = null
281
+ }
282
+ this.imageLoading = false
283
+ this.imageError = true
284
+ },
285
+
286
+ // 鼠标滚轮缩放 - 以鼠标为中心点缩放
287
+ handleWheel(event) {
288
+ event.preventDefault()
289
+
290
+ const delta = event.deltaY < 0 ? 0.1 : -0.1
291
+ const newScale = Math.max(this.minScale, Math.min(this.maxScale, this.scale + delta))
292
+
293
+ // 计算缩放中心(相对于图片)
294
+ if (this.$refs.previewContainer) {
295
+ const containerRect = this.$refs.previewContainer.getBoundingClientRect()
296
+ const mouseX = event.clientX - containerRect.left
297
+ const mouseY = event.clientY - containerRect.top
298
+
299
+ // 计算鼠标相对于图片原始坐标系的位置
300
+ // 注意:position.x 和 position.y 是图片相对于容器的平移
301
+ const relativeX = (mouseX - this.position.x) / this.scale
302
+ const relativeY = (mouseY - this.position.y) / this.scale
303
+
304
+ // 更新位置以保持鼠标点不变
305
+ this.position.x = mouseX - relativeX * newScale
306
+ this.position.y = mouseY - relativeY * newScale
307
+ this.scale = newScale
308
+ } else {
309
+ this.scale = newScale
310
+ }
311
+ },
312
+
313
+ // 开始拖拽
314
+ startDrag(event) {
315
+ if (event.button !== 0) return // 只响应左键
316
+ this.isDragging = true
317
+ this.startPosition = {
318
+ x: event.clientX - this.position.x,
319
+ y: event.clientY - this.position.y
320
+ }
321
+ event.preventDefault()
322
+ },
323
+
324
+ // 处理拖拽
325
+ handleDrag(event) {
326
+ if (!this.isDragging) return
327
+
328
+ this.position.x = event.clientX - this.startPosition.x
329
+ this.position.y = event.clientY - this.startPosition.y
330
+ },
331
+
332
+ // 停止拖拽
333
+ stopDrag() {
334
+ this.isDragging = false
335
+ },
336
+
337
+ // 放大
338
+ zoomIn() {
339
+ const newScale = Math.min(this.maxScale, this.scale + 0.2)
340
+ if (newScale !== this.scale) {
341
+ this.centerZoom(newScale)
342
+ }
343
+ },
344
+
345
+ // 缩小
346
+ zoomOut() {
347
+ const newScale = Math.max(this.minScale, this.scale - 0.2)
348
+ if (newScale !== this.scale) {
349
+ this.centerZoom(newScale)
350
+ }
351
+ },
352
+
353
+ // 以当前视图中心点缩放(用于工具栏按钮)
354
+ centerZoom(newScale) {
355
+ if (this.$refs.previewContainer) {
356
+ const containerRect = this.$refs.previewContainer.getBoundingClientRect()
357
+ // 使用容器中心作为缩放中心点
358
+ const centerX = containerRect.width / 2
359
+ const centerY = containerRect.height / 2
360
+
361
+ // 计算中心点相对于图片原始坐标系的位置
362
+ const relativeX = (centerX - this.position.x) / this.scale
363
+ const relativeY = (centerY - this.position.y) / this.scale
364
+
365
+ // 更新位置以保持中心点不变
366
+ this.position.x = centerX - relativeX * newScale
367
+ this.position.y = centerY - relativeY * newScale
368
+ }
369
+ this.scale = newScale
370
+ },
371
+
372
+ // 重置图片位置和大小
373
+ resetImage() {
374
+ this.position = { x: 0, y: 0 }
375
+ this.scale = 1
376
+ },
377
+
378
+ // 重置所有状态
379
+ resetState() {
380
+ // 清理超时
381
+ if (this.loadingTimeout) {
382
+ clearTimeout(this.loadingTimeout)
383
+ this.loadingTimeout = null
384
+ }
385
+ this.position = { x: 0, y: 0 }
386
+ this.scale = 1
387
+ this.imageLoading = false
388
+ this.imageError = false
389
+ this.isDragging = false
390
+ }
391
+ }
392
+ }
393
+ </script>
394
+
395
+ <style lang="scss" scoped>
396
+ .file-preview {
397
+ display: inline-block;
398
+
399
+ .preview-link {
400
+ color: #409eff;
401
+ cursor: pointer;
402
+ text-decoration: none;
403
+ transition: color 0.2s;
404
+
405
+ &:hover {
406
+ color: #66b1ff;
407
+ text-decoration: underline;
408
+ }
409
+ }
410
+ }
411
+
412
+ .preview-wrapper {
413
+ width: 100%;
414
+ height: 750px;
415
+ background: #000;
416
+ position: relative;
417
+ overflow: hidden;
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: center;
421
+ }
422
+
423
+ .preview-container {
424
+ width: 100%;
425
+ height: 100%;
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ position: relative;
430
+ overflow: hidden;
431
+ touch-action: none;
432
+ /* 防止移动端默认行为 */
433
+ }
434
+
435
+ .preview-image {
436
+ max-width: 100%;
437
+ max-height: 100%;
438
+ object-fit: contain;
439
+ /* 关键:保持比例并适应容器 */
440
+ transform-origin: 0 0;
441
+ /* 设置为左上角以匹配我们的数学计算 */
442
+ transition: transform 0.2s ease;
443
+ user-select: none;
444
+ -webkit-user-drag: none;
445
+
446
+ /* 平滑的拖拽和缩放效果 */
447
+ will-change: transform;
448
+ }
449
+
450
+ /* 操作工具栏 */
451
+ .toolbar {
452
+ position: absolute;
453
+ bottom: 20px;
454
+ left: 50%;
455
+ transform: translateX(-50%);
456
+ display: flex;
457
+ gap: 10px;
458
+ background: rgba(0, 0, 0, 0.7);
459
+ padding: 10px;
460
+ border-radius: 4px;
461
+ z-index: 10;
462
+ }
463
+
464
+ .toolbar-btn {
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ gap: 5px;
469
+ background: rgba(255, 255, 255, 0.1);
470
+ border: 1px solid rgba(255, 255, 255, 0.2);
471
+ color: white;
472
+ padding: 8px 12px;
473
+ border-radius: 4px;
474
+ cursor: pointer;
475
+ font-size: 12px;
476
+ transition: all 0.2s;
477
+
478
+ &:hover {
479
+ background: rgba(255, 255, 255, 0.2);
480
+ border-color: rgba(255, 255, 255, 0.3);
481
+ }
482
+
483
+ &:active {
484
+ transform: scale(0.95);
485
+ }
486
+
487
+ .icon {
488
+ font-size: 14px;
489
+ font-weight: bold;
490
+ }
491
+
492
+ .text {
493
+ white-space: nowrap;
494
+ }
495
+ }
496
+ </style>
497
+
498
+ <style lang="scss">
499
+ /* 对话框全局样式 */
500
+ .image-preview-dialog {
501
+ .el-dialog {
502
+ background: #000;
503
+ border: 1px solid #333;
504
+ }
505
+
506
+ .el-dialog__header {
507
+ background: #000;
508
+ border-bottom: 1px solid #333;
509
+ padding: 10px 20px;
510
+
511
+ .el-dialog__title {
512
+ color: #fff;
513
+ font-size: 14px;
514
+ }
515
+
516
+ .el-dialog__close {
517
+ color: #fff;
518
+
519
+ &:hover {
520
+ color: #409eff;
521
+ }
522
+ }
523
+ }
524
+
525
+ .el-dialog__body {
526
+ padding: 0 !important;
527
+ background: #000 !important;
528
+ overflow: hidden !important;
529
+ }
530
+ }
531
+ </style>
@@ -0,0 +1,534 @@
1
+ <template>
2
+ <div class="file-preview">
3
+ <!-- 图片预览链接 -->
4
+ <a v-if="isImage" class="preview-link" @click="showPreviewDialog">
5
+ 查看
6
+ </a>
7
+
8
+ <!-- 非图片文件下载 -->
9
+ <nm-file-download v-else :url="downloadUrl" :private="isPrivate" :fileName="fileName" :text="downloadText" :icon="downloadIcon" :size="downloadSize" :type="downloadType" @click="onDownload" />
10
+
11
+ <!-- 图片预览对话框 -->
12
+ <nm-dialog
13
+ v-if="isImage"
14
+ ref="previewDialog"
15
+ :visible.sync="previewVisible"
16
+ :title="previewTitle"
17
+ width="800px"
18
+ height="800px"
19
+ :fullscreen="false"
20
+ :footer="false"
21
+ custom-class="image-preview-dialog"
22
+ @closed="resetState"
23
+ >
24
+ <div class="preview-wrapper">
25
+ <!-- 使用CSS实现图片居中,而不是JS计算 -->
26
+ <div class="preview-container" ref="previewContainer">
27
+ <img
28
+ :src="previewUrl"
29
+ :alt="fileName"
30
+ class="preview-image"
31
+ ref="previewImage"
32
+ @load="handleImageLoad"
33
+ @error="handleImageError"
34
+ :style="imageStyle"
35
+ @mousedown="startDrag"
36
+ @wheel="handleWheel"
37
+ @dblclick="resetImage"
38
+ />
39
+ </div>
40
+
41
+ <!-- 操作按钮栏 -->
42
+ <div class="toolbar">
43
+ <button class="toolbar-btn" @click="zoomIn">
44
+ <span class="icon">+</span>
45
+ <span class="text">放大</span>
46
+ </button>
47
+ <button class="toolbar-btn" @click="zoomOut">
48
+ <span class="icon">-</span>
49
+ <span class="text">缩小</span>
50
+ </button>
51
+ <button class="toolbar-btn" @click="resetImage">
52
+ <span class="icon">⟳</span>
53
+ <span class="text">重置</span>
54
+ </button>
55
+ <!-- 非图片文件下载 -->
56
+ <nm-file-download :url="downloadUrl" :private="isPrivate" :fileName="fileName" :text="downloadText" :icon="downloadIcon" :size="downloadSize" :type="downloadType" @click="onDownload" />
57
+
58
+ </div>
59
+ </div>
60
+ </nm-dialog>
61
+ </div>
62
+ </template>
63
+
64
+ <script>
65
+ export default {
66
+ name: 'FilePreview',
67
+ props: {
68
+ fullPath: { type: String, required: true },
69
+ fileName: String,
70
+ isPrivate: { type: Boolean, default: false },
71
+ ext: String,
72
+ downloadText: { type: String, default: '下载' },
73
+ downloadIcon: { type: String, default: 'download' },
74
+ downloadSize: String,
75
+ downloadType: { type: String, default: 'primary' },
76
+ previewTitle: { type: String, default: '图片预览' },
77
+ imageExtensions: {
78
+ type: Array,
79
+ default: () => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico']
80
+ }
81
+ },
82
+
83
+ data() {
84
+ return {
85
+ previewVisible: false,
86
+ previewUrl: '',
87
+ fileInfo: null,
88
+
89
+ // 图片状态
90
+ imageLoading: false,
91
+ imageError: false,
92
+ loadingTimeout: null,
93
+
94
+ // 图片位置和缩放(使用CSS transform,但让CSS处理居中)
95
+ position: { x: 0, y: 0 },
96
+ scale: 1,
97
+ minScale: 0.1,
98
+ maxScale: 5,
99
+
100
+ // 拖拽状态
101
+ isDragging: false,
102
+ startPosition: { x: 0, y: 0 }
103
+ }
104
+ },
105
+
106
+ computed: {
107
+ isImage() {
108
+ // return false;
109
+ const ext = this.ext || (this.fileName || '').split('.').pop() || ''
110
+ return this.imageExtensions.includes(ext.toLowerCase())
111
+ },
112
+
113
+ downloadUrl() {
114
+ console.log('this.fileInfo?.url ', this.fileInfo?.url)
115
+ return this.fileInfo?.url || this.fullPath
116
+ },
117
+
118
+ imageStyle() {
119
+ return {
120
+ transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.scale})`,
121
+ cursor: this.isDragging ? 'grabbing' : 'grab'
122
+ }
123
+ }
124
+ },
125
+
126
+ watch: {
127
+ fullPath(val) {
128
+ if (val && this.isImage && !this.isPrivate) {
129
+ this.previewUrl = val
130
+ }
131
+ },
132
+
133
+ previewVisible(val) {
134
+ if (!val) {
135
+ this.resetState()
136
+ }
137
+ }
138
+ },
139
+
140
+ mounted() {
141
+ // 监听全局鼠标事件用于拖拽
142
+ document.addEventListener('mousemove', this.handleDrag)
143
+ document.addEventListener('mouseup', this.stopDrag)
144
+ },
145
+
146
+ beforeDestroy() {
147
+ // 清理事件监听
148
+ document.removeEventListener('mousemove', this.handleDrag)
149
+ document.removeEventListener('mouseup', this.stopDrag)
150
+
151
+ // 清理超时
152
+ if (this.loadingTimeout) {
153
+ clearTimeout(this.loadingTimeout)
154
+ this.loadingTimeout = null
155
+ }
156
+ },
157
+
158
+ methods: {
159
+ getFileExtension() {
160
+ if (this.ext) return this.ext.toLowerCase()
161
+ const name = this.fileName || this.fullPath || ''
162
+ const dotIndex = name.lastIndexOf('.')
163
+ return dotIndex > -1 ? name.slice(dotIndex + 1).toLowerCase() : ''
164
+ },
165
+
166
+ async getFileInfo() {
167
+ if (!this.fullPath) return null
168
+
169
+ try {
170
+ const result = await $api.admin.file.getByFullPath(this.fullPath)
171
+ this.fileInfo = result
172
+
173
+ console.log('result', result)
174
+
175
+ return result
176
+ } catch (error) {
177
+ console.error('获取文件信息失败:', error)
178
+ return null
179
+ }
180
+ },
181
+
182
+ async showPreviewDialog() {
183
+ if (!this.isImage) return
184
+
185
+ this.resetState()
186
+ this.imageLoading = true
187
+ this.previewVisible = true
188
+
189
+ try {
190
+ const fileInfo = await this.getFileInfo()
191
+ if (!fileInfo) {
192
+ this.imageLoading = false
193
+ this.imageError = true
194
+ return
195
+ }
196
+
197
+ await this.setPreviewUrl(fileInfo)
198
+
199
+ // 设置一个立即检查,防止缓存图片不触发load事件
200
+ this.$nextTick(() => {
201
+ setTimeout(() => {
202
+ if (this.imageLoading && this.$refs.previewImage && this.$refs.previewImage.complete) {
203
+ this.handleImageLoad()
204
+ }
205
+ }, 300) // 300ms后检查
206
+ })
207
+
208
+ // 设置超时,防止图片加载事件未触发
209
+ this.loadingTimeout = setTimeout(() => {
210
+ if (this.imageLoading) {
211
+ console.warn('图片加载超时,强制结束加载状态')
212
+ this.imageLoading = false
213
+ // 检查图片是否已经加载完成
214
+ if (this.$refs.previewImage && this.$refs.previewImage.complete) {
215
+ this.handleImageLoad()
216
+ }
217
+ }
218
+ }, 2000) // 2秒超时
219
+ } catch (error) {
220
+ console.error('打开预览对话框失败:', error)
221
+ this.imageLoading = false
222
+ this.imageError = true
223
+ }
224
+ },
225
+
226
+ async setPreviewUrl(fileInfo) {
227
+ let newUrl
228
+ if (this.isPrivate && fileInfo.url) {
229
+ try {
230
+ newUrl = await $api.admin.file.preview(fileInfo.url)
231
+ } catch {
232
+ newUrl = fileInfo.url
233
+ }
234
+ } else {
235
+ newUrl = fileInfo.url || this.fullPath
236
+ }
237
+
238
+ // 先清空URL,强制重新加载(即使URL相同)
239
+ if (this.previewUrl) {
240
+ this.previewUrl = ''
241
+ // 等待一个tick确保DOM更新
242
+ await this.$nextTick()
243
+ }
244
+
245
+ // 设置新的URL
246
+ this.previewUrl = newUrl
247
+
248
+ // 等待DOM更新后检查图片是否已经加载完成
249
+ this.$nextTick(() => {
250
+ // 如果图片元素存在且已经加载完成,直接触发加载完成事件
251
+ if (this.$refs.previewImage && this.$refs.previewImage.complete) {
252
+ // 小延迟确保图片完全渲染
253
+ setTimeout(() => {
254
+ if (this.imageLoading) {
255
+ this.handleImageLoad()
256
+ }
257
+ }, 100)
258
+ }
259
+ })
260
+ },
261
+
262
+ async onDownload() {
263
+ const fileInfo = await this.getFileInfo()
264
+ const url = fileInfo?.url || this.fullPath
265
+ this.$emit('download', url, this.fileName)
266
+ },
267
+
268
+ handleImageLoad() {
269
+ // 清理超时
270
+ if (this.loadingTimeout) {
271
+ clearTimeout(this.loadingTimeout)
272
+ this.loadingTimeout = null
273
+ }
274
+ this.imageLoading = false
275
+ // 图片加载完成后自动居中
276
+ this.resetImage()
277
+ },
278
+
279
+ handleImageError() {
280
+ // 清理超时
281
+ if (this.loadingTimeout) {
282
+ clearTimeout(this.loadingTimeout)
283
+ this.loadingTimeout = null
284
+ }
285
+ this.imageLoading = false
286
+ this.imageError = true
287
+ },
288
+
289
+ // 鼠标滚轮缩放 - 以鼠标为中心点缩放
290
+ handleWheel(event) {
291
+ event.preventDefault()
292
+
293
+ const delta = event.deltaY < 0 ? 0.1 : -0.1
294
+ const newScale = Math.max(this.minScale, Math.min(this.maxScale, this.scale + delta))
295
+
296
+ // 计算缩放中心(相对于图片)
297
+ if (this.$refs.previewContainer) {
298
+ const containerRect = this.$refs.previewContainer.getBoundingClientRect()
299
+ const mouseX = event.clientX - containerRect.left
300
+ const mouseY = event.clientY - containerRect.top
301
+
302
+ // 计算鼠标相对于图片原始坐标系的位置
303
+ // 注意:position.x 和 position.y 是图片相对于容器的平移
304
+ const relativeX = (mouseX - this.position.x) / this.scale
305
+ const relativeY = (mouseY - this.position.y) / this.scale
306
+
307
+ // 更新位置以保持鼠标点不变
308
+ this.position.x = mouseX - relativeX * newScale
309
+ this.position.y = mouseY - relativeY * newScale
310
+ this.scale = newScale
311
+ } else {
312
+ this.scale = newScale
313
+ }
314
+ },
315
+
316
+ // 开始拖拽
317
+ startDrag(event) {
318
+ if (event.button !== 0) return // 只响应左键
319
+ this.isDragging = true
320
+ this.startPosition = {
321
+ x: event.clientX - this.position.x,
322
+ y: event.clientY - this.position.y
323
+ }
324
+ event.preventDefault()
325
+ },
326
+
327
+ // 处理拖拽
328
+ handleDrag(event) {
329
+ if (!this.isDragging) return
330
+
331
+ this.position.x = event.clientX - this.startPosition.x
332
+ this.position.y = event.clientY - this.startPosition.y
333
+ },
334
+
335
+ // 停止拖拽
336
+ stopDrag() {
337
+ this.isDragging = false
338
+ },
339
+
340
+ // 放大
341
+ zoomIn() {
342
+ const newScale = Math.min(this.maxScale, this.scale + 0.2)
343
+ if (newScale !== this.scale) {
344
+ this.centerZoom(newScale)
345
+ }
346
+ },
347
+
348
+ // 缩小
349
+ zoomOut() {
350
+ const newScale = Math.max(this.minScale, this.scale - 0.2)
351
+ if (newScale !== this.scale) {
352
+ this.centerZoom(newScale)
353
+ }
354
+ },
355
+
356
+ // 以当前视图中心点缩放(用于工具栏按钮)
357
+ centerZoom(newScale) {
358
+ if (this.$refs.previewContainer) {
359
+ const containerRect = this.$refs.previewContainer.getBoundingClientRect()
360
+ // 使用容器中心作为缩放中心点
361
+ const centerX = containerRect.width / 2
362
+ const centerY = containerRect.height / 2
363
+
364
+ // 计算中心点相对于图片原始坐标系的位置
365
+ const relativeX = (centerX - this.position.x) / this.scale
366
+ const relativeY = (centerY - this.position.y) / this.scale
367
+
368
+ // 更新位置以保持中心点不变
369
+ this.position.x = centerX - relativeX * newScale
370
+ this.position.y = centerY - relativeY * newScale
371
+ }
372
+ this.scale = newScale
373
+ },
374
+
375
+ // 重置图片位置和大小
376
+ resetImage() {
377
+ this.position = { x: 0, y: 0 }
378
+ this.scale = 1
379
+ },
380
+
381
+ // 重置所有状态
382
+ resetState() {
383
+ // 清理超时
384
+ if (this.loadingTimeout) {
385
+ clearTimeout(this.loadingTimeout)
386
+ this.loadingTimeout = null
387
+ }
388
+ this.position = { x: 0, y: 0 }
389
+ this.scale = 1
390
+ this.imageLoading = false
391
+ this.imageError = false
392
+ this.isDragging = false
393
+ }
394
+ }
395
+ }
396
+ </script>
397
+
398
+ <style lang="scss" scoped>
399
+ .file-preview {
400
+ display: inline-block;
401
+
402
+ .preview-link {
403
+ color: #409eff;
404
+ cursor: pointer;
405
+ text-decoration: none;
406
+ transition: color 0.2s;
407
+
408
+ &:hover {
409
+ color: #66b1ff;
410
+ text-decoration: underline;
411
+ }
412
+ }
413
+ }
414
+
415
+ .preview-wrapper {
416
+ width: 100%;
417
+ height: 750px;
418
+ background: #000;
419
+ position: relative;
420
+ overflow: hidden;
421
+ display: flex;
422
+ align-items: center;
423
+ justify-content: center;
424
+ }
425
+
426
+ .preview-container {
427
+ width: 100%;
428
+ height: 100%;
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ position: relative;
433
+ overflow: hidden;
434
+ touch-action: none;
435
+ /* 防止移动端默认行为 */
436
+ }
437
+
438
+ .preview-image {
439
+ max-width: 100%;
440
+ max-height: 100%;
441
+ object-fit: contain;
442
+ /* 关键:保持比例并适应容器 */
443
+ transform-origin: 0 0;
444
+ /* 设置为左上角以匹配我们的数学计算 */
445
+ transition: transform 0.2s ease;
446
+ user-select: none;
447
+ -webkit-user-drag: none;
448
+
449
+ /* 平滑的拖拽和缩放效果 */
450
+ will-change: transform;
451
+ }
452
+
453
+ /* 操作工具栏 */
454
+ .toolbar {
455
+ position: absolute;
456
+ bottom: 20px;
457
+ left: 50%;
458
+ transform: translateX(-50%);
459
+ display: flex;
460
+ gap: 10px;
461
+ background: rgba(0, 0, 0, 0.7);
462
+ padding: 10px;
463
+ border-radius: 4px;
464
+ z-index: 10;
465
+ }
466
+
467
+ .toolbar-btn {
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ gap: 5px;
472
+ background: rgba(255, 255, 255, 0.1);
473
+ border: 1px solid rgba(255, 255, 255, 0.2);
474
+ color: white;
475
+ padding: 8px 12px;
476
+ border-radius: 4px;
477
+ cursor: pointer;
478
+ font-size: 12px;
479
+ transition: all 0.2s;
480
+
481
+ &:hover {
482
+ background: rgba(255, 255, 255, 0.2);
483
+ border-color: rgba(255, 255, 255, 0.3);
484
+ }
485
+
486
+ &:active {
487
+ transform: scale(0.95);
488
+ }
489
+
490
+ .icon {
491
+ font-size: 14px;
492
+ font-weight: bold;
493
+ }
494
+
495
+ .text {
496
+ white-space: nowrap;
497
+ }
498
+ }
499
+ </style>
500
+
501
+ <style lang="scss">
502
+ /* 对话框全局样式 */
503
+ .image-preview-dialog {
504
+ .el-dialog {
505
+ background: #000;
506
+ border: 1px solid #333;
507
+ }
508
+
509
+ .el-dialog__header {
510
+ background: #000;
511
+ border-bottom: 1px solid #333;
512
+ padding: 10px 20px;
513
+
514
+ .el-dialog__title {
515
+ color: #fff;
516
+ font-size: 14px;
517
+ }
518
+
519
+ .el-dialog__close {
520
+ color: #fff;
521
+
522
+ &:hover {
523
+ color: #409eff;
524
+ }
525
+ }
526
+ }
527
+
528
+ .el-dialog__body {
529
+ padding: 0 !important;
530
+ background: #000 !important;
531
+ overflow: hidden !important;
532
+ }
533
+ }
534
+ </style>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <nm-form-dialog ref="form" v-bind="form" v-on="on" :visible.sync="visible_">
3
+ <el-row>
4
+ <el-col :span="20" :offset="1">
5
+ <el-form-item label="模块:" prop="moduleCode">
6
+ <nm-module-select v-model="form.model.moduleCode" />
7
+ </el-form-item>
8
+ <el-form-item label="分组:" prop="group">
9
+ <el-input v-model="form.model.group" />
10
+ </el-form-item>
11
+ <el-form-item label="访问方式:" prop="accessMode">
12
+ <el-select v-model="form.model.accessMode" placeholder="请选择访问方式">
13
+ <el-option label="私有" :value="0" />
14
+ <el-option label="公开" :value="1" />
15
+ <el-option label="授权" :value="2" />
16
+ </el-select>
17
+ </el-form-item>
18
+ <el-form-item v-if="form.model.accessMode === 2" label="授权账户:" prop="accounts">
19
+ <el-input v-model="form.model.accounts" placeholder="请输入账户ID,多个用逗号分隔" clearable />
20
+ </el-form-item>
21
+ <el-form-item label="文件:" prop="filePath" :rules="[{ required: true, message: '请选择要上传的文件' }]">
22
+ <nm-file-upload
23
+ v-model="form.model.filePath"
24
+ :module="form.model.moduleCode"
25
+ :group="form.model.group"
26
+ :access-mode="form.model.accessMode"
27
+ :accounts="form.model.accounts"
28
+ max-size="50MB"
29
+ @success="onUploadSuccess"
30
+ />
31
+ </el-form-item>
32
+ </el-col>
33
+ </el-row>
34
+ </nm-form-dialog>
35
+ </template>
36
+ <script>
37
+ import { mixins } from 'haiwei-ui'
38
+
39
+ export default {
40
+ mixins: [mixins.formSave],
41
+ data() {
42
+ return {
43
+ title: '文件上传',
44
+ actions: {
45
+ add: null, // 将在created中设置
46
+ edit: null, // 禁用编辑功能
47
+ update: null // 禁用更新功能
48
+ },
49
+ form: {
50
+ width: '600px',
51
+ footer: false, // 隐藏确定/取消按钮
52
+ model: {
53
+ id: '',
54
+ moduleCode: '',
55
+ group: '',
56
+ accessMode: 1, // 默认公开
57
+ accounts: '',
58
+ filePath: '' // 文件路径
59
+ },
60
+ rules: {
61
+ moduleCode: [{ required: true, message: '请选择模块' }],
62
+ group: [{ required: true, message: '请输入分组' }],
63
+ accessMode: [{ required: true, message: '请选择访问方式' }],
64
+ filePath: [{ required: true, message: '请选择要上传的文件' }]
65
+ }
66
+ }
67
+ }
68
+ },
69
+ created() {
70
+ // 由于隐藏了按钮,不需要设置actions.add
71
+ // this.actions.add = this.handleAdd
72
+ },
73
+ methods: {
74
+ // 上传成功处理(由nm-file-upload组件触发)
75
+ onUploadSuccess(data, file) {
76
+ // 文件上传成功后,nm-file-upload会自动更新form.model.filePath
77
+ // 显示成功消息
78
+ this.$message.success('文件上传成功')
79
+
80
+ // 触发success事件,通知父组件刷新列表
81
+ this.$emit('success')
82
+
83
+ // 自动关闭对话框
84
+ this.visible_ = false
85
+
86
+ // 重置表单
87
+ this.reset()
88
+
89
+ // 可以添加额外的成功处理逻辑
90
+ console.log('文件上传成功:', data, file)
91
+ },
92
+
93
+ // 重置表单
94
+ reset() {
95
+ this.form.model = {
96
+ id: '',
97
+ moduleCode: '',
98
+ group: '',
99
+ accessMode: 1,
100
+ accounts: '',
101
+ filePath: ''
102
+ }
103
+ }
104
+ }
105
+ }
106
+ </script>
@@ -11,6 +11,42 @@
11
11
  </el-form-item>
12
12
  </template>
13
13
 
14
+ <!--高级查询-->
15
+ <template v-slot:querybar-advanced>
16
+ <el-row>
17
+ <el-col :span="20" :offset="1">
18
+ <el-form-item label="分组:" prop="group">
19
+ <el-input v-model="list.model.group" clearable />
20
+ </el-form-item>
21
+ <el-form-item label="访问方式:" prop="accessMode">
22
+ <el-select v-model="list.model.accessMode" placeholder="请选择访问方式" clearable>
23
+ <el-option label="私有" :value="0" />
24
+ <el-option label="公开" :value="1" />
25
+ <el-option label="授权" :value="2" />
26
+ </el-select>
27
+ </el-form-item>
28
+ </el-col>
29
+ </el-row>
30
+ </template>
31
+
32
+ <!--按钮-->
33
+ <template v-slot:querybar-buttons>
34
+ <nm-button v-bind="buttons.add" @click="add" />
35
+ </template>
36
+
37
+ <!-- 文件预览列 -->
38
+ <template v-slot:col-fullPath="{ row }">
39
+ <nm-file-preview :fullPath="row.fullPath" :fileName="row.fileName" :ext="row.ext" :isPrivate="row.accessMode !== 1" />
40
+ <!-- <nm-file-preview :fullPath="row.fullPath"/> -->
41
+ </template>
42
+
43
+ <!--访问方式-->
44
+ <template v-slot:col-accessModeName="{ row }">
45
+ <el-tag v-if="row.accessMode === 0" type="info">私有</el-tag>
46
+ <el-tag v-else-if="row.accessMode === 1" type="success">公开</el-tag>
47
+ <el-tag v-else-if="row.accessMode === 2" type="warning">授权</el-tag>
48
+ </template>
49
+
14
50
  <!--操作列-->
15
51
  <template v-slot:col-operation="{ row }">
16
52
  <nm-file-download v-bind="buttons.export" :url="row.url" :private="row.accessMode !== 1" @success="refresh" />
@@ -18,12 +54,16 @@
18
54
  <nm-button-delete v-bind="buttons.hardDel" :id="row.id" :action="hardRemoveAction" @success="refresh" />
19
55
  </template>
20
56
  </nm-list>
57
+
58
+ <!--保存页-->
59
+ <save-page :visible.sync="dialog.save" @success="refresh" />
21
60
  </nm-container>
22
61
  </template>
23
62
  <script>
24
63
  import { mixins } from 'haiwei-ui'
25
64
  import page from './page'
26
65
  import cols from './cols'
66
+ import SavePage from '../components/save'
27
67
 
28
68
  // 接口
29
69
  const api = $api.admin.file
@@ -31,22 +71,33 @@ const api = $api.admin.file
31
71
  export default {
32
72
  name: page.name,
33
73
  mixins: [mixins.list],
74
+ components: { SavePage },
34
75
  data() {
35
76
  return {
36
77
  list: {
37
78
  title: page.title,
38
79
  cols,
39
80
  action: api.query,
40
- operationWidth: '200',
81
+ operationWidth: '270',
82
+ advanced: {
83
+ enabled: true,
84
+ width: '400px'
85
+ },
41
86
  model: {
42
87
  moduleCode: '',
43
- fileName: ''
88
+ fileName: '',
89
+ group: '',
90
+ accessMode: ''
44
91
  }
45
92
  },
46
93
  removeAction: api.remove,
47
94
  hardRemoveAction: api.hardDelete,
48
95
  buttons: page.buttons
49
96
  }
97
+ },
98
+ methods: {
99
+ // 上传(新增)- 使用混入的add方法打开对话框
100
+ // add方法已在mixins.list中定义,会设置dialog.save = true
50
101
  }
51
102
  }
52
103
  </script>
@@ -8,7 +8,15 @@ const page = new (function() {
8
8
  // 关联权限
9
9
  this.permissions = [`${this.name}_query_get`]
10
10
 
11
+ // 按钮
11
12
  this.buttons = {
13
+ add: {
14
+ text: '上传',
15
+ type: 'success',
16
+ icon: 'upload',
17
+ code: `${this.name}_add`,
18
+ permissions: [`${this.name}_add_post`]
19
+ },
12
20
  export: {
13
21
  text: '导出',
14
22
  type: 'text',
package/vue.config.js CHANGED
@@ -105,5 +105,14 @@ module.exports = {
105
105
  name: 'manifest'
106
106
  })
107
107
  })
108
+ },
109
+ css: {
110
+ loaderOptions: {
111
+ sass: {
112
+ sassOptions: {
113
+ silenceDeprecations: ['import', 'legacy-js-api', 'bogus-combinators', 'slash-div']
114
+ }
115
+ }
116
+ }
108
117
  }
109
118
  }