txt以表格形式预览
This commit is contained in:
parent
901a26583f
commit
64a3231f01
@ -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,
|
||||
|
||||
})
|
||||
}
|
@ -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('保存成功')
|
||||
|
323
web/src/components/textEditing/txtexl.vue
Normal file
323
web/src/components/textEditing/txtexl.vue
Normal 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>
|
26
web/src/views/testdata/datamanagement/index.vue
vendored
26
web/src/views/testdata/datamanagement/index.vue
vendored
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user