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

283 lines
7.9 KiB
Vue

<template>
<div class="spreadsheet-viewer w-full h-full bg-white dark:bg-gray-900 relative">
<!-- Toolbar -->
<div v-if="mode === 'edit'" class="border-b border-gray-200 dark:border-gray-700 p-2 bg-gray-50 dark:bg-gray-800">
<div class="flex items-center space-x-2">
<button
@click="addRow"
class="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
>
Add Row
</button>
<button
@click="addColumn"
class="px-3 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700"
>
Add Column
</button>
<button
@click="deleteRow"
class="px-3 py-1.5 text-sm bg-red-600 text-white rounded hover:bg-red-700"
:disabled="selectedRow === -1"
>
Delete Row
</button>
<button
@click="deleteColumn"
class="px-3 py-1.5 text-sm bg-red-600 text-white rounded hover:bg-red-700"
:disabled="selectedColumn === -1"
>
Delete Column
</button>
</div>
</div>
<!-- Spreadsheet Grid -->
<div class="flex-1 overflow-auto">
<table class="w-full border-collapse">
<!-- Header Row -->
<thead>
<tr>
<th class="w-12 h-8 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600"></th>
<th
v-for="(col, colIndex) in columnHeaders"
:key="colIndex"
@click="selectColumn(colIndex)"
:class="[
'h-8 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-center text-sm font-medium cursor-pointer',
selectedColumn === colIndex ? 'bg-blue-200 dark:bg-blue-800' : ''
]"
>
{{ col }}
</th>
</tr>
</thead>
<!-- Data Rows -->
<tbody>
<tr
v-for="(row, rowIndex) in spreadsheetData"
:key="rowIndex"
>
<!-- Row Header -->
<td
@click="selectRow(rowIndex)"
:class="[
'w-12 h-8 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-center text-sm font-medium cursor-pointer',
selectedRow === rowIndex ? 'bg-blue-200 dark:bg-blue-800' : ''
]"
>
{{ rowIndex + 1 }}
</td>
<!-- Data Cells -->
<td
v-for="(cell, colIndex) in row"
:key="colIndex"
@click="selectCell(rowIndex, colIndex)"
:class="[
'h-8 border border-gray-300 dark:border-gray-600 p-1',
selectedCell.row === rowIndex && selectedCell.col === colIndex
? 'bg-blue-100 dark:bg-blue-900 ring-2 ring-blue-500'
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
]"
>
<input
v-if="mode === 'edit' && selectedCell.row === rowIndex && selectedCell.col === colIndex"
v-model="editingValue"
@blur="finishEdit"
@keydown.enter="finishEdit"
@keydown.escape="cancelEdit"
class="w-full h-full border-none outline-none bg-transparent text-sm"
ref="cellInput"
/>
<span v-else class="text-sm">{{ cell }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Cell Info Bar -->
<div class="border-t border-gray-200 dark:border-gray-700 p-2 bg-gray-50 dark:bg-gray-800">
<div class="flex items-center space-x-4 text-sm">
<span class="font-medium">
Cell: {{ getCellReference(selectedCell.row, selectedCell.col) }}
</span>
<span v-if="selectedCell.row !== -1 && selectedCell.col !== -1">
Value: {{ getCurrentCellValue() }}
</span>
</div>
</div>
<!-- Save Button (Edit Mode) -->
<div v-if="mode === 'edit'" class="absolute bottom-4 right-4">
<button
@click="saveSpreadsheet"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center space-x-2"
>
<Icon name="mdi:content-save" class="w-4 h-4" />
<span>Save</span>
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
const props = defineProps({
document: {
type: Object,
required: true
},
mode: {
type: String,
default: 'view' // 'view' | 'edit'
},
data: {
type: Array,
default: () => []
}
});
const emit = defineEmits([
'data-changed',
'save-requested'
]);
// State
const spreadsheetData = ref([]);
const selectedCell = ref({ row: -1, col: -1 });
const selectedRow = ref(-1);
const selectedColumn = ref(-1);
const editingValue = ref('');
const cellInput = ref(null);
// Computed
const columnHeaders = computed(() => {
const headers = [];
const maxCols = Math.max(...spreadsheetData.value.map(row => row.length), 0);
for (let i = 0; i < maxCols; i++) {
headers.push(String.fromCharCode(65 + i)); // A, B, C, etc.
}
return headers;
});
// Methods
const selectCell = (row, col) => {
if (mode.value === 'edit') {
selectedCell.value = { row, col };
selectedRow.value = -1;
selectedColumn.value = -1;
editingValue.value = spreadsheetData.value[row][col] || '';
nextTick(() => {
if (cellInput.value) {
cellInput.value.focus();
cellInput.value.select();
}
});
}
};
const selectRow = (row) => {
selectedRow.value = row;
selectedCell.value = { row: -1, col: -1 };
selectedColumn.value = -1;
};
const selectColumn = (col) => {
selectedColumn.value = col;
selectedCell.value = { row: -1, col: -1 };
selectedRow.value = -1;
};
const finishEdit = () => {
if (selectedCell.value.row !== -1 && selectedCell.value.col !== -1) {
const newData = [...spreadsheetData.value];
if (!newData[selectedCell.value.row]) {
newData[selectedCell.value.row] = [];
}
newData[selectedCell.value.row][selectedCell.value.col] = editingValue.value;
spreadsheetData.value = newData;
emit('data-changed', newData);
}
selectedCell.value = { row: -1, col: -1 };
};
const cancelEdit = () => {
selectedCell.value = { row: -1, col: -1 };
editingValue.value = '';
};
const addRow = () => {
const maxCols = Math.max(...spreadsheetData.value.map(row => row.length), 0);
const newRow = new Array(maxCols).fill('');
spreadsheetData.value.push(newRow);
emit('data-changed', spreadsheetData.value);
};
const addColumn = () => {
spreadsheetData.value.forEach(row => {
row.push('');
});
emit('data-changed', spreadsheetData.value);
};
const deleteRow = () => {
if (selectedRow.value !== -1) {
spreadsheetData.value.splice(selectedRow.value, 1);
selectedRow.value = -1;
emit('data-changed', spreadsheetData.value);
}
};
const deleteColumn = () => {
if (selectedColumn.value !== -1) {
spreadsheetData.value.forEach(row => {
row.splice(selectedColumn.value, 1);
});
selectedColumn.value = -1;
emit('data-changed', spreadsheetData.value);
}
};
const getCellReference = (row, col) => {
if (row === -1 || col === -1) return '';
return `${String.fromCharCode(65 + col)}${row + 1}`;
};
const getCurrentCellValue = () => {
if (selectedCell.value.row === -1 || selectedCell.value.col === -1) return '';
return spreadsheetData.value[selectedCell.value.row]?.[selectedCell.value.col] || '';
};
const saveSpreadsheet = () => {
emit('save-requested');
};
// Watch for data changes
watch(() => props.data, (newData) => {
spreadsheetData.value = newData || [];
}, { immediate: true });
</script>
<style scoped>
.spreadsheet-viewer {
font-family: 'Arial', sans-serif;
}
table {
table-layout: fixed;
}
td, th {
min-width: 100px;
max-width: 200px;
}
input {
font-family: inherit;
}
</style>