wireless-esp32-tools-web-host/src/views/text-data-viewer/textDataViewer.vue

497 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="flex min-h-7 overflow-auto">
<el-popover
placement="bottom"
trigger="click"
:hide-after="0"
transition="none"
width="300"
>
<div v-if="!store.configPanelShow" class="h-[40vh] overflow-auto">
<text-data-config></text-data-config>
</div>
<template #reference>
<el-link v-show="!store.configPanelShow" type="primary">
<InlineSvg name="arrow_drop_down" class="h-6 mb-1 px-2"></InlineSvg>
</el-link>
</template>
</el-popover>
<div class="flex">
<el-checkbox size="small" v-model="store.forceToBottom" label="自动滚动至底部" border/>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>仅清除显示区域,可用刷新恢复</p>
</template>
<el-button size="small" @click="store.clearFilteredBuff">
<InlineSvg class="h-5" name="trash"></InlineSvg>
清屏 ⇩
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>与缓存同步+过滤</p>
</template>
<el-button size="small" @click="store.refreshFilteredBuff">
刷新
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>仅停止刷新显示区,后台继续接收数据</p>
</template>
<el-checkbox size="small" border v-model="store.dataFilterAutoUpdate">
自动刷新
</el-checkbox>
</el-tooltip>
</div>
<!-- <div class="flex">-->
<!-- <el-button size="small" @click="addItem(1)">add1</el-button>-->
<!-- <el-button size="small" @click="addItem(10)">add10</el-button>-->
<!-- <el-button size="small" @click="addItem(100)">add100</el-button>-->
<!-- <el-button size="small" @click="addItem(1000)">add1000</el-button>-->
<!-- <el-button size="small" @click="scrollToBottom">scrollToBottom</el-button>-->
<!-- <el-button @click="toggleAutoBottom">autoBot: {{ forceToBottom }}</el-button>-->
<!-- <el-checkbox size="small" v-model="store.forceToBottom" :label="'autoBot:' + store.forceToBottom" border></el-checkbox>-->
<!-- <el-button>{{ count }}, {{ items.length }}, {{ vuetifyVirtualScrollBarRef.scrollTop }},-->
<!-- {{ vuetifyVirtualScrollBarRef.clientHeight }}, {{ vuetifyVirtualScrollBarRef.scrollHeight }}-->
<!-- </el-button>-->
<!-- <el-button @click="updateScroll">{{ scrollTop }}, {{ clientHeight }}, {{ scrollHeight }}</el-button>-->
<!-- </div>-->
<!-- <div>-->
<!-- <el-popover-->
<!-- placement="bottom"-->
<!-- trigger="click"-->
<!-- :hide-after="0"-->
<!-- transition="none"-->
<!-- width="300"-->
<!-- >-->
<!-- <div id="quick-access-panel-tp" class="bg-amber-200"></div>-->
<!-- <template #reference>-->
<!-- <el-link class="content-center" v-show="!store.quickAccessPanelShow">-->
<!-- <InlineSvg name="arrow_drop_down" class="h-7"></InlineSvg>-->
<!-- 666-->
<!-- </el-link>-->
<!-- </template>-->
<!-- </el-popover>-->
<!-- </div>-->
</div>
<div class="flex flex-grow overflow-hidden border-2 rounded scroll-m-2">
<v-virtual-scroll
v-if="store.showVirtualScroll"
:items="store.dataFiltered"
id="myScrollerID"
ref="vuetifyVirtualScrollRef"
class="font-mono break-all text-sm"
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
>
<template v-slot:default="{ item, }">
<div class="">
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
<span>{{ item.time }}</span>◄-RX|</p>
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-►|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>未发送►|</p>
<p v-show="store.showText"
v-html="item.str"></p>
</div>
<div class="flex">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex whitespace-pre">
<p v-show="store.showHexdump"
class="text-nowrap"
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
v-html="item.hexdump"
></p>
</div>
</div>
</template>
</v-virtual-scroll>
<v-virtual-scroll
v-else
:items="store.dataFiltered"
id="myScrollerID"
ref="vuetifyVirtualScrollRef2"
class="font-mono break-all text-sm"
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
>
<template v-slot:default="{ item, }">
<div>
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
<span>{{ item.time }}</span>◄-RX|</p>
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-►|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>未发送►|</p>
<p v-show="store.showText"
v-html="item.str"></p>
</div>
<div class="flex">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex whitespace-pre">
<p v-show="store.showHexdump"
class="text-nowrap"
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
v-html="item.hexdump"
></p>
</div>
</div>
</template>
</v-virtual-scroll>
</div>
<div class="shrink-0 flex h-8 mt-0.5 text-xs">
<div class="flex shrink-0">
<el-tooltip content="未满足断帧规则的数据未超时暂时实时显示在此区域。超过8192字节自动断帧" effect="light">
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
<p>►</p>
</div>
<div ref="RxHexDumpRef" class="p-0.5 border-2 rounded w-full overflow-y-scroll font-mono text-nowrap">
<p v-html="store.RxRemainHexdump"></p>
</div>
</div>
<div class="shrink-0 min-h-6 flex gap-2 justify-between overflow-auto">
<div class="flex gap-2">
<el-link @click="clearSendInput">
<el-tag class="font-mono" size="small">
<div class="flex ">
<InlineSvg class="h-5" name="trash"></InlineSvg>
<span class="content-end text-xs">⇩</span>
</div>
</el-tag>
</el-link>
<el-tooltip content="实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’" placement="right" effect="light" :show-after="1000">
<div class="flex align-center">
<el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
循环发送(ms)
</el-checkbox>
<el-input-number
v-model="loopSendFreq"
class="h-5"
size="small"
:step="10"
:min="1"
>
</el-input-number>
</div>
</el-tooltip>
<el-link @click="isSendTextFormat = !isSendTextFormat">
<el-tag class="font-mono font-bold" size="small">发送格式:{{ isSendTextFormat ? "文本" : "HEX" }}</el-tag>
</el-link>
</div>
<div class="flex gap-2">
<el-link>
<el-tag class="font-mono font-bold" size="small">
{{ `TX(B):${store.TxByteCount}/ ${store.TxTotalByteCount}` }}
</el-tag>
</el-link>
<el-link type="success">
<el-tag class="font-mono font-bold" size="small" type="success">
{{ `RX(B):${store.RxByteCount}/ ${store.RxTotalByteCount}` }}
</el-tag>
</el-link>
<div class="flex align-center">
<el-tag class="font-mono font-bold" size="small" type="info">
<el-link class="flex" @click="store.clearDataBuff" type="warning">
<InlineSvg class="h-5" name="trash"></InlineSvg>
</el-link>
<span class="align-text-bottom">缓存帧数: {{ store.dataBufLength }}/30000</span>
</el-tag>
</div>
</div>
</div>
<div class="flex flex-row font-mono">
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="uartInputTextBox" clearable
:placeholder="isSendTextFormat ?
'输入文本,支持\\n\\x转义' :
'输入HEX格式'"
@keydown="handleTextboxKeydown"
></el-input>
<el-tooltip content="Ctrl+回车" placement="top" :auto-close="500">
<el-button type="primary"
@click="onSendClick">
{{ (isSendTextFormat || isHexStringValid) ? "发送" : "格式化" }}
</el-button>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import InlineSvg from "@/components/InlineSvg.vue";
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
import {debouncedWatch} from "@vueuse/core";
import {globalNotify} from "@/composables/notification";
const count = ref(0);
const vuetifyVirtualScrollBarRef = ref(document.body);
const vuetifyVirtualScrollContainerRef = ref(document.body);
const enableLoopSend = ref(false);
const loopSendFreq = ref(1000);
let loopSendIntervalID: number = -1;
const isSendTextFormat = ref(true)
const isHexStringValid = ref(false);
const uartInputTextBox = ref("")
const store = useDataViewerStore();
const RxHexDumpRef = ref(document.body);
let lastScrollHeight = 0;
const mutationObserver = new MutationObserver(() => {
if (store.forceToBottom) {
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
scrollToBottom();
}
});
function attachScroll() {
const parent = document.getElementById('myScrollerID') || document.body;
// used to scroll to bottom
vuetifyVirtualScrollBarRef.value = parent || document.body;
// used to monitor height changes, so that one new Items are rendered, the height change -> scroll to bottom
vuetifyVirtualScrollContainerRef.value = parent.querySelector('.v-virtual-scroll__container') || document.body;
vuetifyVirtualScrollBarRef.value.onscroll = handleScroll;
if (vuetifyVirtualScrollContainerRef.value) {
const config = {childList: true, subtree: true, attributes: true};
mutationObserver.observe(vuetifyVirtualScrollBarRef.value, config)
}
if (store.forceToBottom) {
scrollToBottom();
}
}
onMounted(() => {
attachScroll();
})
onUnmounted(() => {
mutationObserver.disconnect();
});
debouncedWatch(() => store.showVirtualScroll, () => {
lastScrollHeight = 0;
mutationObserver.disconnect();
attachScroll();
}, {debounce: 80});
function addItem(nr: number) {
let rawText = "";
let maxcount = count.value + nr;
for (; count.value < maxcount; count.value++) {
let text = "";
if ((count.value & 3) === 3) {
text = `<p class="border-4">${count.value}inputasdf<br/>${count.value}asdf <br/>${count.value}asdfasdf <br/>${count.value}asdf </p>`;
text += text;
} else if ((count.value & 2) === 2) {
text = `<p class="border-4">${count.value}inputas df2<br/>${count.value} asdf asd <br/>${count.value}fasdf asdf </p>`;
text += text;
} else if ((count.value & 1) === 1) {
text = `<p class="border-4">${count.value}inputas df2asdf asd <br/>${count.value}fasdf asdf ${count.value} </p>`;
text += text;
} else {
text = `<p class="border-4">${count.value}inputasa<br/>jdhfklasjdhfklasdhflasidfhilasdfhlasdiufhlasdkfhuasnlfcyerhfcibnkuaweghnfctiklaweuyrchnlaweirtucgnawertkcgyawertcnawelcrvnawgervcawencrgf${count.value} </p>`;
}
rawText = count.value + "<p class=\"border-4\"> 666666666b\n6666 666\x1b[33m6666666666666666666666666</p>b\n"
const encoder = new TextEncoder();
const arr = encoder.encode(rawText);
store.addItem(arr, false, false, 1);
}
}
function scrollToBottom() {
nextTick(() => {
const scrollerElement = vuetifyVirtualScrollBarRef.value; // Adjust according to your setup
scrollerElement.scrollTop = scrollerElement.scrollHeight;
});
}
function formatHexInput(input: string) {
// Split the input string on spaces to process each segment separately
let str;
// Remove any "0x" prefix and handle uppercase conversion
str = input.replace(/^0x/i, ' ').toUpperCase();
// Remove any non-hexadecimal characters
str = str.replace(/[^0-9A-F]/gi, ' ');
let segments = str.split(/\s+/);
let output: string[] = [];
segments.forEach(segment => {
// Check if segment length is odd and needs padding
if (segment.length % 2 !== 0) {
segment = '0' + segment; // Prepend '0' to make the length even
}
// Split segment into array of two-character chunks
let chunked = [];
for (let i = 0; i < segment.length; i += 2) {
chunked.push(segment.substring(i, i + 2));
}
// Concatenate chunked segments and add to output
output.push(chunked.join(' '));
});
// Join all processed segments with a space and return
return output.join(' ');
}
function checkHexTextValid() {
isHexStringValid.value = uartInputTextBox.value.toUpperCase() === formatHexInput(uartInputTextBox.value);
}
watch(isSendTextFormat, (value) => {
if (!value) {
checkHexTextValid()
}
});
watch(() => uartInputTextBox.value, () => {
if (!isSendTextFormat.value) {
checkHexTextValid()
}
})
watch(enableLoopSend, (newValue) => {
if (newValue) {
if (loopSendIntervalID !== -1) {
clearInterval(loopSendIntervalID);
}
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
} else {
clearInterval(loopSendIntervalID);
loopSendIntervalID = -1;
}
});
watch(loopSendFreq, (value) => {
if (enableLoopSend.value && value) {
/* update interval with new value */
if (loopSendIntervalID !== -1) {
clearInterval(loopSendIntervalID);
}
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
}
})
/* patch scroll container does not update clear filter */
watch(() => store.filterChanged, (value) => {
if (value && store.forceToBottom) {
scrollToBottom();
}
store.filterChanged = false;
})
watch(() => store.RxRemainHexdump, value => {
if (value) {
RxHexDumpRef.value.scrollTop = RxHexDumpRef.value.scrollHeight;
}
})
watch(() => store.showVirtualScroll, () => {
if (store.forceToBottom) {
scrollToBottom();
}
});
const handleScroll = () => {
if (store.forceToBottom) {
if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) {
store.forceToBottom = false;
} else {
scrollToBottom();
}
} else if ((vuetifyVirtualScrollBarRef.value.scrollHeight -
vuetifyVirtualScrollBarRef.value.scrollTop) <= vuetifyVirtualScrollBarRef.value.clientHeight) {
store.forceToBottom = true;
}
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
};
watch(() => store.forceToBottom, value => {
if (value) {
setTimeout(scrollToBottom, 0);
}
});
function clearSendInput() {
uartInputTextBox.value = ""
}
function handleTextboxKeydown(ev: KeyboardEvent) {
if (ev.ctrlKey && ev.key === 'Enter') {
onSendClick();
}
}
function onSendClick() {
if (!uartInputTextBox.value && !store.hasAddedText) {
globalNotify("无帧头帧尾、发送框无数据发送")
return;
}
if (store.acceptIncomingData) {
if (isSendTextFormat.value) {
store.addString(uartInputTextBox.value, false, true);
} else if (!isHexStringValid.value) {
uartInputTextBox.value = formatHexInput(uartInputTextBox.value);
} else {
store.addHexString(uartInputTextBox.value, false, true);
}
} else {
if (isSendTextFormat.value) {
store.addString(uartInputTextBox.value, false, true, 1);
} else if (!isHexStringValid.value) {
uartInputTextBox.value = formatHexInput(uartInputTextBox.value);
} else {
store.addHexString(uartInputTextBox.value, false, true, 1);
}
}
}
</script>