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

485 lines
16 KiB
Vue

<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>
<div class="flex">
<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 text-wrap">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex">
<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">
<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 text-wrap">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex">
<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 max-h-14 mt-0.5 text-xs">
<div class="flex shrink-0">
<el-tooltip content="未满足断帧规则的数据(如:未超时),暂时实时显示在此区域。" effect="light">
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
<p>►</p>
</div>
<div class="p-0.5 border-2 rounded w-full overflow-auto 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-y-scroll">
<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:${store.TxByteCount}B/${store.TxTotalByteCount}B` }}
</el-tag>
</el-link>
<el-link type="success">
<el-tag class="font-mono font-bold" size="small" type="success">
{{ `RX:${store.RxByteCount}B/${store.RxTotalByteCount}B` }}
</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 }}/20000</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();
let lastScrollHeight = 0;
const mutationObserver = new MutationObserver(() => {
if (store.forceToBottom) {
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)
}
}
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;
scrollerElement.scrollTo(scrollerElement.scrollLeft, scrollerElement.scrollHeight);
});
}
function scrollToTop() {
nextTick(() => {
vuetifyVirtualScrollBarRef.value.scrollTop = vuetifyVirtualScrollBarRef.value.scrollHeight;
// vuetifyVirtualScrollBarRef.value.scrollTo(0, 0);
});
}
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) {
scrollToTop()
scrollToBottom();
store.filterChanged = false;
}
})
const handleScroll = (ev: Event) => {
if (store.forceToBottom) {
if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) {
store.forceToBottom = false;
}
} 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) {
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>