feat: 实现串口信息显示

- 以chat-bubble形式显示串口信息
This commit is contained in:
2025-12-26 13:33:08 +08:00
parent 955dc99119
commit 6e6d2da3ce
3 changed files with 200 additions and 0 deletions

View File

@ -0,0 +1,92 @@
<script lang="ts">
import { tick } from 'svelte';
import ChatBubble from './components/ChatBubble.svelte';
import {
records,
readingRecord,
pinBottom,
scrollToRecordIndex,
} from '$lib/stores/record/record';
let rootEl: HTMLDivElement | null = null;
let showFullDate = $state(false);
async function scrollToBottom() {
if (!rootEl) return;
rootEl.scrollTop = rootEl.scrollHeight + 2000;
}
let lastLength = 0;
$effect(() => {
const len = $records.length;
const pinned = $pinBottom;
if (pinned && len !== lastLength) {
lastLength = len;
(async () => {
await tick();
scrollToBottom();
})();
}
});
$effect(() => {
const index = $scrollToRecordIndex;
if (index >= 0 && rootEl) {
const els = rootEl.querySelectorAll('.chat');
const el = els[index];
if (el) {
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
scrollToRecordIndex.set(-1);
}
});
</script>
<div
bind:this={rootEl}
class="record-panel relative w-full overflow-y-auto scroll-smooth p-2 pb-10"
>
{#each $records as record (record.timestamp)}
<ChatBubble {record} bind:fullDate={showFullDate} />
{/each}
{#if $readingRecord}
<ChatBubble
bind:record={$readingRecord}
reading={true}
bind:fullDate={showFullDate}
/>
{/if}
</div>
<style>
::-webkit-scrollbar {
width: 4px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
::-webkit-scrollbar-track {
border-radius: 4px;
margin: 15px;
}
::-webkit-scrollbar-button {
width: 0;
height: 0;
}
</style>

View File

@ -0,0 +1,103 @@
<script lang="ts">
import {
bufferToHexFormat,
bufferToDecString,
bufferToString,
} from '$lib/stores/datacode';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '$lib/components/ui/tooltip';
import { Button } from '$lib/components/ui/button';
import Copy from 'phosphor-svelte/lib/Copy';
import { formatTimestamp } from '$lib/stores/utils/time';
import type { RecordItem } from '$lib/stores/record/record';
import { POSITION_MAP } from './ChatBubble.variant';
import { copyRecordContent } from '$lib/stores/record/record';
type Props = {
record: RecordItem;
reading?: boolean;
fullDate?: boolean;
};
let {
record = $bindable(),
fullDate = $bindable(),
reading = false,
}: Props = $props();
type DisplayType = 'hex' | 'ascii';
const types = ['hex', 'ascii'] satisfies DisplayType[];
const positionClass = $derived(POSITION_MAP[record.type]);
let currentDisplay: DisplayType = $state('ascii');
function toggleDisplay() {
const idx = types.indexOf(currentDisplay);
const next = types[(idx + 1) % types.length];
currentDisplay = next;
}
function toggleTimeFormat() {
if (fullDate === undefined) return;
fullDate = !fullDate;
}
function formatTime(ts?: number) {
if (!ts) return '';
const full = formatTimestamp(ts);
return fullDate ? full : full.slice(11); // HH:mm:ss:SSS
}
</script>
<div class={`group chat ${positionClass}`}>
<div class="chat-header mx-2 flex">
<button
class="cursor-pointer text-sm opacity-70 hover:opacity-100"
onclick={toggleTimeFormat}
>
{formatTime(record.timestamp)}
</button>
<div class="w-4"></div>
{#if !reading}
<button
class="cursor-pointer text-sm font-medium opacity-70 hover:opacity-100"
onclick={toggleDisplay}
>
{currentDisplay.toUpperCase()}
</button>
{/if}
</div>
<div class="chat-bubble max-w-[50%] text-sm wrap-break-word">
{#if currentDisplay === 'hex'}
{bufferToHexFormat(record.data)}
{:else if currentDisplay === 'ascii'}
{bufferToString(record.data)}
{:else}
{bufferToDecString(record.data)}
{/if}
</div>
<div class="chat-footer mt-1 opacity-0 transition group-hover:opacity-100">
<Tooltip>
<TooltipTrigger>
<Button
circle
size="xs"
variant="ghost"
onclick={() => copyRecordContent(record)}
>
<Copy size={16} />
</Button>
</TooltipTrigger>
<TooltipContent>复制</TooltipContent>
</Tooltip>
</div>
</div>

View File

@ -0,0 +1,5 @@
export const POSITION_MAP = {
read: 'chat-start',
write: 'chat-end',
system: 'chat-start',
};