Rml workorder/v2 need improvements
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
// tt-file-gallery.js
|
||||
Vue.component('tt-file-gallery', {
|
||||
props: {
|
||||
files: { type: Array, default: () => [] }
|
||||
files: { type: Array, default: () => [] },
|
||||
editMode: { type: Boolean, default: false },
|
||||
deleteMode: { type: Boolean, default: false },
|
||||
selectable: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullscreenItem: null, // Holds the file being viewed
|
||||
fullscreenItem: null,
|
||||
currentImageIndex: 0,
|
||||
|
||||
// Zoom & Pan state
|
||||
editingFile: null,
|
||||
selectedFiles: [],
|
||||
zoom: 1,
|
||||
pan: { x: 0, y: 0 },
|
||||
isPanning: false,
|
||||
@@ -23,7 +27,6 @@ Vue.component('tt-file-gallery', {
|
||||
return this.fullscreenItem && this.isImage(this.fullscreenItem);
|
||||
},
|
||||
imageTransformStyle() {
|
||||
// Apply CSS transform for zoom and pan
|
||||
const { x, y } = this.pan;
|
||||
return {
|
||||
transform: `translate(${x}px, ${y}px) scale(${this.zoom})`,
|
||||
@@ -33,34 +36,33 @@ Vue.component('tt-file-gallery', {
|
||||
},
|
||||
fullscreenDownloadUrl() {
|
||||
if (!this.fullscreenItem) return '#';
|
||||
return `/File/download?id=${this.fullscreenItem.id}`;
|
||||
return `/File/download?id=${this.fullscreenItem.fileId}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// File type checks
|
||||
isImage(file) {
|
||||
return file.mimetype && file.mimetype.startsWith('image/');
|
||||
},
|
||||
isPdf(file) {
|
||||
return file.mimetype === 'application/pdf';
|
||||
},
|
||||
|
||||
// Get icon for non-image/pdf files
|
||||
isImage: file => file.mimetype && file.mimetype.startsWith('image/'),
|
||||
isPdf: file => file.mimetype === 'application/pdf',
|
||||
getFileIcon(file) {
|
||||
const extension = file.fileName?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'doc':
|
||||
case 'docx': return 'fas fa-file-word text-primary';
|
||||
case 'xls':
|
||||
case 'xlsx': return 'fas fa-file-excel text-success';
|
||||
case 'zip':
|
||||
case 'rar': return 'fas fa-file-archive text-warning';
|
||||
case 'doc': case 'docx': return 'fas fa-file-word text-primary';
|
||||
case 'xls': case 'xlsx': return 'fas fa-file-excel text-success';
|
||||
case 'zip': case 'rar': return 'fas fa-file-archive text-warning';
|
||||
default: return 'fas fa-file text-secondary';
|
||||
}
|
||||
},
|
||||
|
||||
// Viewer controls
|
||||
toggleSelection(fileId) {
|
||||
if (!this.selectable) return;
|
||||
const index = this.selectedFiles.indexOf(fileId);
|
||||
if (index > -1) {
|
||||
this.selectedFiles.splice(index, 1);
|
||||
} else {
|
||||
this.selectedFiles.push(fileId);
|
||||
}
|
||||
this.$emit('selection-changed', this.selectedFiles);
|
||||
},
|
||||
openViewer(file) {
|
||||
if(this.editingFile) return;
|
||||
this.fullscreenItem = file;
|
||||
if (this.isImage(file)) {
|
||||
this.currentImageIndex = this.imageFiles.findIndex(img => img.id === file.id);
|
||||
@@ -68,9 +70,7 @@ Vue.component('tt-file-gallery', {
|
||||
this.resetZoomAndPan();
|
||||
this.$nextTick(() => { this.$refs.viewer?.focus(); });
|
||||
},
|
||||
closeViewer() {
|
||||
this.fullscreenItem = null;
|
||||
},
|
||||
closeViewer() { this.fullscreenItem = null; },
|
||||
navigateImage(direction) {
|
||||
const newIndex = this.currentImageIndex + direction;
|
||||
if (newIndex >= 0 && newIndex < this.imageFiles.length) {
|
||||
@@ -79,9 +79,8 @@ Vue.component('tt-file-gallery', {
|
||||
this.resetZoomAndPan();
|
||||
}
|
||||
},
|
||||
|
||||
// Event handlers for keyboard and clicks
|
||||
handleKeyDown(event) {
|
||||
event.stopPropagation();
|
||||
if (!this.fullscreenItem) return;
|
||||
switch (event.key) {
|
||||
case 'Escape': this.closeViewer(); break;
|
||||
@@ -89,24 +88,16 @@ Vue.component('tt-file-gallery', {
|
||||
case 'ArrowRight': this.isViewingImage && this.navigateImage(1); break;
|
||||
}
|
||||
},
|
||||
|
||||
// --- Zoom and Pan Methods ---
|
||||
resetZoomAndPan() {
|
||||
this.zoom = 1;
|
||||
this.pan = { x: 0, y: 0 };
|
||||
this.isPanning = false;
|
||||
this.zoom = 1; this.pan = { x: 0, y: 0 }; this.isPanning = false;
|
||||
},
|
||||
|
||||
// Mouse Wheel Zoom
|
||||
handleWheel(e) {
|
||||
if (!this.isViewingImage) return;
|
||||
e.preventDefault();
|
||||
const scaleFactor = 0.2;
|
||||
const newZoom = this.zoom - (e.deltaY > 0 ? scaleFactor : -scaleFactor);
|
||||
this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom between 1x and 5x
|
||||
this.zoom = Math.max(1, Math.min(newZoom, 5));
|
||||
},
|
||||
|
||||
// Mouse Drag to Pan
|
||||
onPanStart(e) {
|
||||
if (this.zoom <= 1) return;
|
||||
e.preventDefault();
|
||||
@@ -119,79 +110,81 @@ Vue.component('tt-file-gallery', {
|
||||
this.pan.x = e.clientX - this.panStart.x;
|
||||
this.pan.y = e.clientY - this.panStart.y;
|
||||
},
|
||||
onPanEnd() {
|
||||
this.isPanning = false;
|
||||
},
|
||||
onPanEnd() { this.isPanning = false; },
|
||||
onTouchStart(e) { /* ... touch logic ... */ },
|
||||
onTouchMove(e) { /* ... touch logic ... */ },
|
||||
onTouchEnd(e) { /* ... touch logic ... */ },
|
||||
|
||||
// Touch Events for Mobile (Pinch-to-Zoom & Pan)
|
||||
onTouchStart(e) {
|
||||
if (this.zoom <= 1 && e.touches.length === 1) return;
|
||||
e.preventDefault();
|
||||
if (e.touches.length === 1) { // Pan
|
||||
this.isPanning = true;
|
||||
this.panStart.x = e.touches[0].clientX - this.pan.x;
|
||||
this.panStart.y = e.touches[0].clientY - this.pan.y;
|
||||
} else if (e.touches.length === 2) { // Zoom
|
||||
this.lastPinchDist = Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
);
|
||||
startEdit(file, event) {
|
||||
event?.stopPropagation();
|
||||
this.editingFile = { ...file }; // create a copy for editing
|
||||
},
|
||||
cancelEdit(event) {
|
||||
console.log('Edit cancelled');
|
||||
event?.stopPropagation();
|
||||
this.editingFile = null;
|
||||
},
|
||||
saveEdit(event) {
|
||||
event?.stopPropagation();
|
||||
this.$emit('update-file', this.editingFile);
|
||||
this.editingFile = null;
|
||||
},
|
||||
deleteFile(file, event) {
|
||||
event?.stopPropagation();
|
||||
if (confirm(`Sind Sie sicher, dass Sie die Datei "${file.fileName}" löschen möchten?`)) {
|
||||
this.$emit('delete-file', file);
|
||||
}
|
||||
},
|
||||
onTouchMove(e) {
|
||||
if (!this.isPanning && e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
if (e.touches.length === 1 && this.isPanning) { // Pan
|
||||
this.pan.x = e.touches[0].clientX - this.panStart.x;
|
||||
this.pan.y = e.touches[0].clientY - this.panStart.y;
|
||||
} else if (e.touches.length === 2) { // Zoom
|
||||
const pinchDist = Math.hypot(
|
||||
e.touches[0].clientX - e.touches[1].clientX,
|
||||
e.touches[0].clientY - e.touches[1].clientY
|
||||
);
|
||||
const scaleFactor = 0.01;
|
||||
const newZoom = this.zoom + (pinchDist - this.lastPinchDist) * scaleFactor;
|
||||
this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom
|
||||
this.lastPinchDist = pinchDist;
|
||||
}
|
||||
},
|
||||
onTouchEnd(e) {
|
||||
this.isPanning = false;
|
||||
if (e.touches.length < 2) {
|
||||
this.lastPinchDist = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fullscreenItem(newItem) {
|
||||
// Prevent body scroll when viewer is open
|
||||
document.body.style.overflow = newItem ? 'hidden' : '';
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="card">
|
||||
<div class="card-header"><h5>Hochgeladene Dokumente</h5></div>
|
||||
<div class="card-header"><i class="fas fa-images mr-2"></i><h5>Hochgeladene Dokumente</h5></div>
|
||||
<div v-if="!files.length" class="card-body text-center text-muted">Keine Dokumente vorhanden.</div>
|
||||
<div v-else class="card-body">
|
||||
<div class="tt-file-gallery-grid">
|
||||
<div v-for="file in files" :key="file.id" class="tt-file-gallery-item" @click="openViewer(file)">
|
||||
<template v-if="isImage(file)">
|
||||
<img :src="'/File/show?id=' + file.id + '&size=small'" class="tt-file-gallery-thumbnail" :alt="file.fileName">
|
||||
<div class="tt-file-gallery-overlay"><i class="fas fa-search-plus"></i></div>
|
||||
</template>
|
||||
<template v-else-if="isPdf(file)">
|
||||
<div class="tt-file-gallery-icon-container"><i class="fas fa-file-pdf fa-3x text-danger"></i></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="'/File/download?id=' + file.id" target="_blank" @click.stop class="tt-file-gallery-icon-container">
|
||||
<i :class="getFileIcon(file)" class="fa-3x"></i>
|
||||
</a>
|
||||
</template>
|
||||
<div class="tt-file-gallery-filename" :title="file.fileName">{{ file.fileName }}</div>
|
||||
<div v-for="file in files" :key="file.id"
|
||||
class="tt-file-gallery-item"
|
||||
:class="[{ 'selected': selectable && selectedFiles.includes(file.id) }, file.class]"
|
||||
@click="openViewer(file)">
|
||||
|
||||
<div v-if="selectable" class="selection-indicator" @click.stop="toggleSelection(file.id)">
|
||||
<i :class="selectedFiles.includes(file.id) ? 'fas fa-check-circle text-primary' : 'far fa-circle text-muted'"></i>
|
||||
</div>
|
||||
|
||||
<div class="tt-file-gallery-thumbnail-wrapper">
|
||||
<template v-if="isImage(file)">
|
||||
<img :src="'/File/show?id=' + file.fileId + '&size=small'" class="tt-file-gallery-thumbnail" :alt="file.fileName">
|
||||
</template>
|
||||
<template v-else-if="isPdf(file)">
|
||||
<div class="tt-file-gallery-icon-container"><i class="fas fa-file-pdf fa-3x text-danger"></i></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="'/File/download?id=' + file.fileId" target="_blank" @click.stop class="tt-file-gallery-icon-container">
|
||||
<i :class="getFileIcon(file)" class="fa-3x"></i>
|
||||
</a>
|
||||
</template>
|
||||
<div class="tt-file-gallery-overlay" @click.stop="openViewer(file)"><i class="fas fa-search-plus"></i></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tt-file-gallery-filename" :title="file.fileName">
|
||||
<span>{{ file.fileName }}</span>
|
||||
<i v-if="editMode && !editingFile" class="fas fa-edit text-primary ml-1 action-icon" @click="startEdit(file, $event)"></i>
|
||||
<i v-if="deleteMode && !editingFile" class="fas fa-trash text-danger ml-1 action-icon" @click="deleteFile(file, $event)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<tt-modal v-if="editingFile" :show="true" title="Dokument bearbeiten" @close="cancelEdit" @submit="saveEdit" :delete="false">
|
||||
<slot name="file-edit" :file="editingFile"></slot>
|
||||
</tt-modal>
|
||||
|
||||
<div v-if="fullscreenItem" class="tt-fullscreen-overlay" @click.self="closeViewer" @keydown="handleKeyDown" tabindex="-1" ref="viewer">
|
||||
<div class="tt-fullscreen-toolbar">
|
||||
<a v-if="isViewingImage" :href="fullscreenDownloadUrl" download class="tt-fullscreen-btn" title="Download">
|
||||
@@ -199,33 +192,19 @@ Vue.component('tt-file-gallery', {
|
||||
</a>
|
||||
<button class="tt-fullscreen-btn" @click="closeViewer" title="Close"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="tt-fullscreen-content" @click.self="closeViewer">
|
||||
<template v-if="isViewingImage">
|
||||
<div class="tt-fullscreen-image-wrapper"
|
||||
@wheel="handleWheel"
|
||||
@mousedown="onPanStart"
|
||||
@mousemove="onPanMove"
|
||||
@mouseup="onPanEnd"
|
||||
@mouseleave="onPanEnd"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd">
|
||||
<img :src="'/File/show?id=' + fullscreenItem.id" class="tt-fullscreen-image" :style="imageTransformStyle" @click.stop />
|
||||
<div class="tt-fullscreen-image-wrapper" @wheel="handleWheel" @mousedown="onPanStart" @mousemove="onPanMove" @mouseup="onPanEnd" @mouseleave="onPanEnd">
|
||||
<img :src="'/File/show?id=' + fullscreenItem.fileId" class="tt-fullscreen-image" :style="imageTransformStyle" @click.stop />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="isPdf(fullscreenItem)">
|
||||
<iframe :src="'/File/show?id=' + fullscreenItem.id" class="tt-fullscreen-pdf" @click.stop></iframe>
|
||||
<iframe :src="'/File/show?id=' + fullscreenItem.fileId" class="tt-fullscreen-pdf" @click.stop></iframe>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="isViewingImage">
|
||||
<button class="tt-fullscreen-nav-btn left" @click.stop="navigateImage(-1)" v-if="currentImageIndex > 0">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button class="tt-fullscreen-nav-btn right" @click.stop="navigateImage(1)" v-if="currentImageIndex < imageFiles.length - 1">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
<button class="tt-fullscreen-nav-btn left" @click.stop="navigateImage(-1)" v-if="currentImageIndex > 0"><i class="fas fa-chevron-left"></i></button>
|
||||
<button class="tt-fullscreen-nav-btn right" @click.stop="navigateImage(1)" v-if="currentImageIndex < imageFiles.length - 1"><i class="fas fa-chevron-right"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user