new version of rmlworkorder
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
.tt-file-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tt-file-gallery-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tt-file-gallery-thumbnail {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #dee2e6;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.tt-file-gallery-item:hover .tt-file-gallery-thumbnail {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.tt-file-gallery-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 40px; /* Adjust to not cover filename */
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.tt-file-gallery-item:hover .tt-file-gallery-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tt-file-gallery-icon-container {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background-color: #f8f9fa;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.tt-file-gallery-filename {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* --- Fullscreen Viewer Styles --- */
|
||||
|
||||
.tt-fullscreen-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tt-fullscreen-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.tt-fullscreen-btn {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tt-fullscreen-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.tt-fullscreen-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tt-fullscreen-image-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden; /* Important for panning */
|
||||
}
|
||||
|
||||
.tt-fullscreen-image {
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
object-fit: contain;
|
||||
will-change: transform; /* Performance hint for browser */
|
||||
}
|
||||
|
||||
.tt-fullscreen-pdf {
|
||||
width: calc(100vw - 40px);
|
||||
height: calc(100vh - 40px);
|
||||
max-width: 1600px; /* Optional: max width for very large screens */
|
||||
border: none;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.tt-fullscreen-nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tt-fullscreen-nav-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.tt-fullscreen-nav-btn.left {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.tt-fullscreen-nav-btn.right {
|
||||
right: 15px;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
Vue.component('tt-file-gallery', {
|
||||
props: {
|
||||
files: { type: Array, default: () => [] }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullscreenItem: null, // Holds the file being viewed
|
||||
currentImageIndex: 0,
|
||||
|
||||
// Zoom & Pan state
|
||||
zoom: 1,
|
||||
pan: { x: 0, y: 0 },
|
||||
isPanning: false,
|
||||
panStart: { x: 0, y: 0 },
|
||||
lastPinchDist: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageFiles() {
|
||||
return this.files.filter(this.isImage);
|
||||
},
|
||||
isViewingImage() {
|
||||
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})`,
|
||||
cursor: this.isPanning ? 'grabbing' : 'grab',
|
||||
transition: this.isPanning ? 'none' : 'transform 0.2s',
|
||||
};
|
||||
},
|
||||
fullscreenDownloadUrl() {
|
||||
if (!this.fullscreenItem) return '#';
|
||||
return `/File/download?id=${this.fullscreenItem.id}`;
|
||||
}
|
||||
},
|
||||
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
|
||||
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';
|
||||
default: return 'fas fa-file text-secondary';
|
||||
}
|
||||
},
|
||||
|
||||
// Viewer controls
|
||||
openViewer(file) {
|
||||
this.fullscreenItem = file;
|
||||
if (this.isImage(file)) {
|
||||
this.currentImageIndex = this.imageFiles.findIndex(img => img.id === file.id);
|
||||
}
|
||||
this.resetZoomAndPan();
|
||||
this.$nextTick(() => { this.$refs.viewer?.focus(); });
|
||||
},
|
||||
closeViewer() {
|
||||
this.fullscreenItem = null;
|
||||
},
|
||||
navigateImage(direction) {
|
||||
const newIndex = this.currentImageIndex + direction;
|
||||
if (newIndex >= 0 && newIndex < this.imageFiles.length) {
|
||||
this.currentImageIndex = newIndex;
|
||||
this.fullscreenItem = this.imageFiles[newIndex];
|
||||
this.resetZoomAndPan();
|
||||
}
|
||||
},
|
||||
|
||||
// Event handlers for keyboard and clicks
|
||||
handleKeyDown(event) {
|
||||
if (!this.fullscreenItem) return;
|
||||
switch (event.key) {
|
||||
case 'Escape': this.closeViewer(); break;
|
||||
case 'ArrowLeft': this.isViewingImage && this.navigateImage(-1); break;
|
||||
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;
|
||||
},
|
||||
|
||||
// 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
|
||||
},
|
||||
|
||||
// Mouse Drag to Pan
|
||||
onPanStart(e) {
|
||||
if (this.zoom <= 1) return;
|
||||
e.preventDefault();
|
||||
this.isPanning = true;
|
||||
this.panStart.x = e.clientX - this.pan.x;
|
||||
this.panStart.y = e.clientY - this.pan.y;
|
||||
},
|
||||
onPanMove(e) {
|
||||
if (!this.isPanning) return;
|
||||
this.pan.x = e.clientX - this.panStart.x;
|
||||
this.pan.y = e.clientY - this.panStart.y;
|
||||
},
|
||||
onPanEnd() {
|
||||
this.isPanning = false;
|
||||
},
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
},
|
||||
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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i class="fas fa-download"></i>
|
||||
</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>
|
||||
</template>
|
||||
<template v-else-if="isPdf(fullscreenItem)">
|
||||
<iframe :src="'/File/show?id=' + fullscreenItem.id" 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>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
@@ -181,7 +181,7 @@ Vue.component('tt-table-crud', {
|
||||
key: this.crudConfig.key,
|
||||
tableHeader: this.crudConfig.tableHeader,
|
||||
headers: this.crudConfig.columns.filter(column => column.table !== false).map(column => {
|
||||
return {text: column.text, key: column.key, ...column.table, filterOptions: column?.modal?.items, priority: column.priority}
|
||||
return {text: column.text, key: column.key, ...column.table, filterOptions: column?.table?.filterOptions ?? column?.modal?.items ?? [], priority: column.priority}
|
||||
})
|
||||
}
|
||||
}, modalConfig() {
|
||||
|
||||
@@ -201,9 +201,9 @@ Vue.component('tt-table', {
|
||||
.isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key])
|
||||
.format('DD.MM.YYYY HH:mm')) : ''
|
||||
}}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<!-- <i v-else-if="column.filter === 'iconSelect'"-->
|
||||
<!-- :title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"-->
|
||||
<!-- :class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>-->
|
||||
<span v-else-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}
|
||||
|
||||
@@ -236,9 +236,9 @@ Vue.component('tt-table', {
|
||||
}}</span>
|
||||
<span v-else-if="column.filter === 'select'">{{ columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text }}</span>
|
||||
<span v-else-if="key === 'create'">{{ window.moment(row[key] * 1000).format('DD.MM.YYYY HH:mm:ss') }}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<!-- <i v-else-if="column.filter === 'iconSelect'"-->
|
||||
<!-- :title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"-->
|
||||
<!-- :class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>-->
|
||||
<span v-else-if="column.filter === 'autocomplete'">
|
||||
{{ autoCompleteData[key] && autoCompleteData[key][row[key]] ? autoCompleteData[key][row[key]]['title'] : row[key] }}</span>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user