EDMS/components/dms/versioning/DocumentVersionManager.vue
2025-06-05 14:57:08 +08:00

468 lines
15 KiB
Vue

<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDmsStore } from '~/stores/dms';
const props = defineProps({
documentId: {
type: String,
required: true
},
showToolbar: {
type: Boolean,
default: true
},
allowRollback: {
type: Boolean,
default: true
}
});
const emit = defineEmits([
'version-created',
'version-restored',
'version-selected',
'version-compared'
]);
// Store
const dmsStore = useDmsStore();
// Local state
const selectedVersions = ref([]);
const showUploadDialog = ref(false);
const uploadFile = ref(null);
const versionNotes = ref('');
const isCreatingVersion = ref(false);
const isRestoring = ref(false);
const compareMode = ref(false);
// Get document data
const document = computed(() => {
return dmsStore.items.find(item => item.id === props.documentId);
});
// Get version history
const versions = computed(() => {
return dmsStore.getDocumentVersions(props.documentId).sort((a, b) => b.version - a.version);
});
// Get current version
const currentVersion = computed(() => {
return document.value?.version || 1;
});
// Check if versioning is enabled
const versioningEnabled = computed(() => {
return dmsStore.systemSettings.documents.versionControl.enabled;
});
// Format file size
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Format date
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString();
};
// Get time ago
const getTimeAgo = (dateString) => {
const now = new Date();
const date = new Date(dateString);
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'Just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
return `${Math.floor(diffInSeconds / 86400)} days ago`;
};
// Handle file upload for new version
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
uploadFile.value = file;
}
};
// Create new version
const createNewVersion = async () => {
if (!uploadFile.value || !versionNotes.value.trim()) {
return;
}
isCreatingVersion.value = true;
try {
const metadata = {
author: dmsStore.currentUser.name,
changes: versionNotes.value.trim()
};
const newVersion = await dmsStore.createNewVersion(
props.documentId,
uploadFile.value,
metadata
);
emit('version-created', newVersion);
// Reset form
uploadFile.value = null;
versionNotes.value = '';
showUploadDialog.value = false;
// Reset file input
const fileInput = document.querySelector('#version-file-input');
if (fileInput) fileInput.value = '';
} catch (error) {
console.error('Failed to create version:', error);
// Handle error (show toast, etc.)
} finally {
isCreatingVersion.value = false;
}
};
// Restore to specific version
const restoreToVersion = async (version) => {
if (!props.allowRollback) return;
isRestoring.value = true;
try {
// Simulate version restoration
await new Promise(resolve => setTimeout(resolve, 1500));
// Update document version
if (document.value) {
document.value.version = version.version;
document.value.modified = new Date().toLocaleDateString();
}
emit('version-restored', version);
} catch (error) {
console.error('Failed to restore version:', error);
} finally {
isRestoring.value = false;
}
};
// Toggle version selection for comparison
const toggleVersionSelection = (version) => {
const index = selectedVersions.value.findIndex(v => v.version === version.version);
if (index > -1) {
selectedVersions.value.splice(index, 1);
} else if (selectedVersions.value.length < 2) {
selectedVersions.value.push(version);
} else {
// Replace oldest selection
selectedVersions.value.shift();
selectedVersions.value.push(version);
}
emit('version-selected', selectedVersions.value);
};
// Compare selected versions
const compareVersions = () => {
if (selectedVersions.value.length === 2) {
compareMode.value = true;
emit('version-compared', selectedVersions.value);
}
};
// Clear selection
const clearSelection = () => {
selectedVersions.value = [];
compareMode.value = false;
};
// Download specific version
const downloadVersion = (version) => {
// Simulate download
console.log('Downloading version', version.version);
// In real implementation, this would trigger file download
};
// Get version status badge
const getVersionStatus = (version) => {
if (version.version === currentVersion.value) {
return { text: 'Current', color: 'green' };
} else if (version.version === Math.max(...versions.value.map(v => v.version))) {
return { text: 'Latest', color: 'blue' };
} else {
return { text: 'Previous', color: 'gray' };
}
};
// Cancel upload dialog
const cancelUpload = () => {
uploadFile.value = null;
versionNotes.value = '';
showUploadDialog.value = false;
const fileInput = document.querySelector('#version-file-input');
if (fileInput) fileInput.value = '';
};
</script>
<template>
<div class="document-version-manager">
<!-- Toolbar -->
<div v-if="showToolbar" class="toolbar mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Version History
</h3>
<div class="text-sm text-gray-500 dark:text-gray-400">
<span v-if="document">{{ document.name }}</span>
<span class="mx-2"></span>
<span>{{ versions.length }} version{{ versions.length !== 1 ? 's' : '' }}</span>
</div>
</div>
<div class="flex items-center space-x-2">
<!-- Compare button -->
<button
v-if="selectedVersions.length === 2"
@click="compareVersions"
class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
>
Compare Selected
</button>
<!-- Clear selection -->
<button
v-if="selectedVersions.length > 0"
@click="clearSelection"
class="px-3 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 text-sm"
>
Clear Selection
</button>
<!-- Upload new version -->
<button
v-if="versioningEnabled"
@click="showUploadDialog = true"
class="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 text-sm"
>
Upload New Version
</button>
</div>
</div>
<!-- Selected versions info -->
<div v-if="selectedVersions.length > 0" class="mt-3 p-2 bg-blue-50 dark:bg-blue-900/20 rounded">
<p class="text-sm text-blue-800 dark:text-blue-200">
{{ selectedVersions.length }} version{{ selectedVersions.length !== 1 ? 's' : '' }} selected
<span v-if="selectedVersions.length === 2">(Ready to compare)</span>
</p>
</div>
</div>
<!-- Version list -->
<div class="version-list space-y-4">
<div
v-for="version in versions"
:key="version.version"
class="version-item border border-gray-200 dark:border-gray-600 rounded-lg p-4"
:class="{
'border-blue-500 bg-blue-50 dark:bg-blue-900/20': selectedVersions.some(v => v.version === version.version),
'border-green-500 bg-green-50 dark:bg-green-900/20': version.version === currentVersion
}"
>
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4 flex-1">
<!-- Selection checkbox -->
<div class="flex items-center mt-1">
<input
type="checkbox"
:checked="selectedVersions.some(v => v.version === version.version)"
@change="toggleVersionSelection(version)"
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</div>
<!-- Version info -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-3 mb-2">
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Version {{ version.version }}
</h4>
<!-- Status badge -->
<span
class="px-2 py-1 text-xs font-medium rounded-full"
:class="`bg-${getVersionStatus(version).color}-100 text-${getVersionStatus(version).color}-800 dark:bg-${getVersionStatus(version).color}-900/20 dark:text-${getVersionStatus(version).color}-200`"
>
{{ getVersionStatus(version).text }}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600 dark:text-gray-400">
<div>
<p><strong>Author:</strong> {{ version.author }}</p>
<p><strong>Upload Date:</strong> {{ formatDate(version.uploadDate) }}</p>
<p><strong>Time Ago:</strong> {{ getTimeAgo(version.uploadDate) }}</p>
</div>
<div>
<p><strong>File Size:</strong> {{ formatFileSize(version.fileSize) }}</p>
<p v-if="version.changes"><strong>Changes:</strong> {{ version.changes }}</p>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center space-x-2 ml-4">
<!-- Download button -->
<button
@click="downloadVersion(version)"
class="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/20 rounded"
title="Download this version"
>
<Icon name="mdi:download" class="w-4 h-4" />
</button>
<!-- View button -->
<button
class="p-2 text-gray-400 hover:text-green-600 hover:bg-green-100 dark:hover:bg-green-900/20 rounded"
title="View this version"
>
<Icon name="mdi:eye" class="w-4 h-4" />
</button>
<!-- Restore button -->
<button
v-if="allowRollback && version.version !== currentVersion"
@click="restoreToVersion(version)"
:disabled="isRestoring"
class="p-2 text-gray-400 hover:text-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/20 rounded disabled:opacity-50"
title="Restore to this version"
>
<Icon name="mdi:restore" class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
<!-- Empty state -->
<div v-if="versions.length === 0" class="text-center py-8">
<Icon name="mdi:file-clock" class="w-16 h-16 mx-auto text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
No Version History
</h3>
<p class="text-gray-500 dark:text-gray-400">
This document doesn't have any version history yet.
</p>
</div>
<!-- Upload new version dialog -->
<rs-modal :visible="showUploadDialog" @close="cancelUpload" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Upload New Version</h3>
</template>
<template #body>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select File
</label>
<input
id="version-file-input"
type="file"
@change="handleFileUpload"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Version Notes *
</label>
<textarea
v-model="versionNotes"
rows="3"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
placeholder="Describe what changed in this version..."
required
></textarea>
</div>
<div v-if="uploadFile" class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
<div class="flex items-center space-x-3">
<Icon name="mdi:file" class="w-5 h-5 text-gray-400" />
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ uploadFile.name }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(uploadFile.size) }}
</p>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end space-x-2">
<button
@click="cancelUpload"
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
@click="createNewVersion"
:disabled="!uploadFile || !versionNotes.trim() || isCreatingVersion"
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span v-if="isCreatingVersion">Uploading...</span>
<span v-else>Create Version</span>
</button>
</div>
</template>
</rs-modal>
<!-- Restore confirmation -->
<div v-if="isRestoring" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
<div class="text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Restoring Version
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Please wait while we restore the document to the selected version...
</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.version-item {
transition: all 0.2s ease;
}
.version-item:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
</style>