txt以表格形式预览

This commit is contained in:
wangxk 2025-03-26 18:31:28 +08:00
parent 901a26583f
commit 64a3231f01
4 changed files with 366 additions and 15 deletions

View File

@ -185,4 +185,13 @@ export function saveContent(params:any){
// 'Content-Type': 'application/json' // 明确指定内容类型
// }
})
}
//excel编辑保存
export function batchModify(params:any){
return request ({
url:'/experimentalData/ts-files/batchModify',
method:'post',
data:params,
})
}

View File

@ -52,31 +52,28 @@ const txtloading = ref(false)
const loadContent = () => {
txtloading.value = true
apicontent({ id: props.rowId }).then((res) => {
// debugger
editor.value.commands.setContent(convertNewlinesToParagraphs(res.data))
txtloading.value = false
// statusMessage.value = ''
// setTimeout(() => statusMessage.value = '', 2000)
})
}
function convertNewlinesToParagraphs(text) {
//
const paragraphs = text.split('\n').filter(line => line.trim() !== '');
// <p>
return paragraphs.map(line => `<p>${line}</p>`).join('');
return text.split('\n').map(line => `<p>${line}</p>`).join('');
}
function convertParagraphsToNewlines(html) {
// <p>
return html.replace(/<\/?p>/g, '\n')
//
.replace(/\n+/g, '\n')
//
.trim();
return html
.replace(/<\/p>/g, '\n') //
.replace(/<p>/g, '') //
.trim(); //
}
//
const saveCcontent = () => {
// debugger
console.log(editor.value.getHTML())
const content = convertParagraphsToNewlines(editor.value.getHTML())
console.log(content)
saveContent({ id: props.rowId, content: content }).then((res) => {
loadContent()
ElMessage.success('保存成功')

View File

@ -0,0 +1,323 @@
<template>
<div class="text-viewer" :class="{ 'full-screen': isFullscreen }">
<!-- 在顶部添加控制按钮 -->
<div class="controls">
<!-- 在controls区块添加 -->
<button @click="saveChanges" style="margin-right: 8px;">
保存修改
</button>
<button @click="toggleFullscreen">
{{ isFullscreen ? '退出全屏' : '全屏' }}
</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-else>
<div v-if="isLoading" class="loading">加载进度{{ loadedPercent }}%</div>
<div class="scroll-container" ref="scrollContainer" @scroll="handleScroll"
:style="{ height: containerHeight + 'px' }">
<div class="phantom" :style="{ height: totalHeight + 'px' }">
<div v-for="row in visibleRows" :key="row.index" class="row" :style="row.style">
<div v-for="(cell, i) in row.data" :key="i" class="cell">
<input :value="rawData[row.index][i]" @blur="(e) => handleEdit(row.index, i, e)"
class="edit-input">
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { apicontent, batchModify } from "@/api/datamanagement";
import { ElMessage } from "element-plus";
const props = defineProps({
fileUrl: {
type: String,
required: true
},
rowId: {
type: String,
default: false
},
});
//
const CHUNK_SIZE = 1024 * 1024; // 1MB
const ROW_HEIGHT = 40; //
const VISIBLE_BUFFER = 200; //
//
const isLoading = ref(false);
const error = ref(null);
const rawData = reactive([]); //
const scrollTop = ref(0);
const totalRows = ref(0);
const reader = ref(null); // reader
const loadedPercent = ref(0);
//
const isFullscreen = ref(false);
//
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value;
// 使API
// if (isFullscreen.value && document.documentElement.requestFullscreen) {
// document.documentElement.requestFullscreen();
// } else if (document.exitFullscreen) {
// document.exitFullscreen();
// }
};
//
const containerHeight = computed(() => {
return isFullscreen.value ? window.innerHeight : window.innerHeight * 0.8;
});
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - VISIBLE_BUFFER)
);
const endIndex = computed(() =>
Math.min(totalRows.value, startIndex.value + Math.ceil(containerHeight.value / ROW_HEIGHT) + VISIBLE_BUFFER * 2)
);
const visibleRows = computed(() => {
// console.log(rawData)
return rawData.slice(startIndex.value, endIndex.value).map((row, index) => ({
index: startIndex.value + index,
data: row,
style: {
transform: `translateY(${(startIndex.value + index) * ROW_HEIGHT}px)`
}
}));
});
const totalHeight = computed(() => totalRows.value * ROW_HEIGHT);
//
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop;
};
const abortLoading = () => {
if (reader.value) {
reader.value.cancel();
reader.value = null;
}
};
const processChunk = (chunk, isFinal) => {
const lines = chunk.split('\n');
// const lines = chunk.split('\n').map(line => line.replace(/\t/g, ' ')); //
if (!isFinal) {
//
const lastLine = lines.pop() || '';
rawData.push(...lines.filter(l => l).map(line => line.split('\t')));
return lastLine;
}
rawData.push(...lines.filter(l => l).map(line => line.split('\t')));
return '';
};
const loadStream = async () => {
// debugger
try {
isLoading.value = true;
error.value = null;
rawData.length = 0;
totalRows.value = 0;
const response = await fetch(props.fileUrl);
if (!response.body) throw new Error('不支持流式读取');
const contentLength = response.headers.get('content-length');
let receivedLength = 0;
let leftover = '';
reader.value = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.value.read();
if (done) break;
receivedLength += value.length;
if (contentLength) {
loadedPercent.value = Math.round((receivedLength / contentLength) * 100);
}
const chunk = decoder.decode(value, { stream: true });
// debugger
leftover = processChunk(leftover + chunk, false);
}
//
if (leftover) processChunk(leftover, true);
totalRows.value = rawData.length;
} catch (err) {
if (err.name !== 'AbortError') {
error.value = `加载失败:${err.message}`;
}
} finally {
isLoading.value = false;
reader.value = null;
}
};
// URL
watch(() => props.fileUrl, (newVal) => {
if (newVal) {
abortLoading();
loadStream();
}
});
//
onBeforeUnmount(() => {
abortLoading();
});
//
onMounted(() => {
if (props.fileUrl) loadStream();
});
const tabledata = ref([])
//
const handleEdit = (rowIndex, colIndex, event) => {
const oldValue = rawData[rowIndex][colIndex];
const newValue1 = event.target.value.trim();
if (newValue1 !== oldValue) {
console.log(`修改了第${rowIndex + 1}行第${colIndex + 1}`,`旧值: ${oldValue} => 新值: ${newValue1}`);
rawData[rowIndex][colIndex] = newValue1; //
tabledata.value.push({lineNum:rowIndex + 1,colNum:colIndex + 1,newValue:newValue1})
// debugger
}
};
// script
const saveChanges = () => {
// 使\t
// const csvData = rawData.map(row => row.join('\t')).join('\n');
batchModify({ id: props.rowId, modifications: tabledata.value }).then((res) => {
ElMessage.success('保存成功')
if (props.fileUrl) {
abortLoading();
loadStream()
tabledata.value.length = 0
}
})
};
</script>
<style>
.text-viewer {
width: 100%;
/* max-width: 1200px; */
margin: 0 auto;
.edit-input {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 12px;
background: transparent;
text-align: center;
}
.edit-input:focus {
background: #f0f9ff;
box-shadow: 0 0 0 1px #409eff inset;
}
.scroll-container {
position: relative;
overflow: auto;
border: 1px solid #ddd;
}
.phantom {
position: relative;
}
.row {
position: absolute;
min-width: 100%;
/* 保证最小宽度与容器一致 */
height: v-bind(ROW_HEIGHT + 'px');
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
border-right: 1px solid #eee;
box-sizing: border-box;
}
.cell {
flex: 1 0 auto;
/* 允许单元格根据内容扩展 */
min-width: 160px;
/* 设置最小宽度防止挤压 */
padding: 0 12px;
white-space: nowrap;
overflow: visible;
/* 显示完整内容 */
position: relative;
text-align: center;
}
.cell+.cell {
border-left: 1px solid #f0f0f0;
}
.scroll-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.scroll-container::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 4px;
}
.loading {
padding: 10px;
background: #f5f5f5;
text-align: center;
}
.error {
color: #ff4444;
padding: 20px;
text-align: center;
}
}
/* 添加全屏样式 */
.text-viewer.full-screen {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
background: white;
width: 100vw;
height: 100vh;
}
.controls {
position: absolute;
bottom: 0px;
right: 0px;
z-index: 10000;
}
button {
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>

View File

@ -20,6 +20,7 @@ import useHeaderStorageList from "@/components/header/useHeaderStorageList";
import useFileData from "@/components/file/file/useFileData";
//text
import textEdit from '@/components/textEditing/index.vue'
import txtexl from '@/components/textEditing/txtexl.vue'
//
import MapChart from '@/components/trajectory/index.vue';
import Echart from '@/components/trajectory/echarts.vue';
@ -601,8 +602,12 @@ function openPreview(row: any) {
isViewfile.value = true
fileType.value = getFileExtension(row.fileName)
} else if (getFileExtension(row.fileName) == 'txt') {
} else if (getFileExtension(row.fileName) == 'txt' && !row.fileName.includes('ins_img') ) {
testClick(row)
// testexcelClick(row)
} else if (getFileExtension(row.fileName) == 'txt' && row.fileName.includes('ins_img')) {
// testClick(row)
testexcelClick(row)
} else {
row.fileType = getFileType(row.fileName)
filePreview.value = row
@ -1509,6 +1514,18 @@ function testClick(row: any) {
function textClose() {
textedit.value = false
}
//
const textedit1 = ref(false)
const fileUrl = ref('')
function testexcelClick(row: any) {
rowId.value = row.id
textedit1.value = true
title.value = row.fileName
fileUrl.value = row.url
}
function texexceltClose() {
textedit1.value = false
}
</script>
<template>
@ -1943,7 +1960,12 @@ function textClose() {
<el-dialog :title="title" v-model="textedit" :before-close="textClose" top="30px" draggable width="60%"
destroy-on-close>
<textEdit :rowId="rowId" />
<!-- </el-scrollbar> -->
<!-- <txtexl :file-url="fileUrl" /> -->
</el-dialog>
<el-dialog :title="title" v-model="textedit1" :before-close="texexceltClose" top="30px" draggable width="60%"
destroy-on-close>
<txtexl :file-url="fileUrl" :rowId="rowId" />
</el-dialog>
</div>