feat: 实现串口信息显示
- 以chat-bubble形式显示串口信息
This commit is contained in:
92
src/lib/components/RecordPanel/RecordPanel.svelte
Normal file
92
src/lib/components/RecordPanel/RecordPanel.svelte
Normal 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>
|
||||||
103
src/lib/components/RecordPanel/components/ChatBubble.svelte
Normal file
103
src/lib/components/RecordPanel/components/ChatBubble.svelte
Normal 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>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const POSITION_MAP = {
|
||||||
|
read: 'chat-start',
|
||||||
|
write: 'chat-end',
|
||||||
|
system: 'chat-start',
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user