feat: 实现record全局store

- 工具函数:datacode用于处理编码,time用于格式化时间
This commit is contained in:
2025-12-26 13:34:17 +08:00
parent ea408ba924
commit e1d57a4816
3 changed files with 261 additions and 0 deletions

113
src/lib/stores/datacode.ts Normal file
View File

@ -0,0 +1,113 @@
import { writable, get } from 'svelte/store';
import { TextEncoder, TextDecoder } from '@kayahr/text-encoding';
/**
* 编码状态
*/
export type DataCode = 'UTF-8' | 'GBK';
const STORAGE_KEY = 'data-code';
const initialCode =
(typeof localStorage !== 'undefined'
? (localStorage.getItem(STORAGE_KEY) as DataCode)
: null) ?? 'UTF-8';
export const dataCode = writable<DataCode>(initialCode);
dataCode.subscribe((code) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(STORAGE_KEY, code);
}
});
/**
* Encoder / Decoder
*/
const gbkDecoder = new TextDecoder('gbk');
const utf8Decoder = new TextDecoder();
const gbkEncoder = new TextEncoder('gbk');
const utf8Encoder = new TextEncoder();
export function hexStringToHexFormat(str: string) {
return `0x${
str
.match(/.{1,2}/g)
?.map((i) => i.toUpperCase())
.join(', 0x') ?? ''
}`;
}
export function hexStringToBuffer(str: string) {
return Uint8Array.from(
str.match(/.{1,2}/g)?.map((b) => Number.parseInt(b, 16)) ?? []
);
}
export function bufferToHexString(buffer: Uint8Array) {
return Array.from(buffer)
.map((i) => i.toString(16).padStart(2, '0').toUpperCase())
.join('');
}
export function bufferToHexFormat(buffer: Uint8Array) {
return Array.from(buffer)
.map((i) => `0x${i.toString(16).padStart(2, '0').toUpperCase()}`)
.join(', ');
}
export function bufferToString(buffer: Uint8Array) {
return get(dataCode) === 'UTF-8'
? utf8Decoder.decode(buffer)
: gbkDecoder.decode(buffer);
}
export function stringToBuffer(str: string) {
return get(dataCode) === 'UTF-8'
? utf8Encoder.encode(str)
: gbkEncoder.encode(str);
}
export function stringToHexFormat(str: string) {
return bufferToHexFormat(stringToBuffer(str));
}
export function stringToHexString(str: string) {
return bufferToHexString(stringToBuffer(str));
}
export function decStringToBuffer(str: string) {
return hexStringToBuffer(Number.parseInt(str, 10).toString(16));
}
export function bufferToDecString(buffer: Uint8Array) {
return Number.parseInt(bufferToHexString(buffer), 16).toString();
}
/**
* HTML Safety
*/
export function stringToSafeHtml(str: string) {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll(' ', '&nbsp;')
.replaceAll('\r\n', '<br/>')
.replaceAll('\n', '<br/>')
.replaceAll('\r', '<br/>');
}
export function stringToText(str: string) {
return str;
}
/*
* 工具函数
*/
export function isHexString(str: string) {
return /^[0-9a-f]+$/i.test(str);
}

View File

@ -0,0 +1,133 @@
import { writable, get } from 'svelte/store';
import { toast } from 'svelte-sonner';
import {
bufferToDecString,
bufferToHexFormat,
bufferToString,
} from '../datacode';
import { formatTimestamp } from '$lib/stores/utils/time';
/**
* Record定义
*/
export type RecordItem = {
readonly type: 'read' | 'write' | 'system';
data: Uint8Array;
timestamp?: number;
display: 'hex' | 'ascii';
};
/* ----------------------------
* 基础状态writable
* ---------------------------- */
export const records = writable<RecordItem[]>([]);
export const readingRecord = writable<RecordItem | undefined>(undefined);
export const pinBottom = writable(true);
export const scrollToRecordIndex = writable(-1);
const rxCount = writable(0);
const txCount = writable(0);
/* ----------------------------
* 核心操作
* ---------------------------- */
export function addRecord(record: RecordItem) {
records.update((list) => {
const next = [...list, record];
if (record.type === 'read') {
rxCount.update((v) => v + record.data.length);
}
if (record.type === 'write') {
txCount.update((v) => v + record.data.length);
}
return next;
});
}
export function clearRecords() {
records.set([]);
rxCount.set(0);
txCount.set(0);
}
/**
* 导出 / 工具方法
*/
export function exportRecords(list: RecordItem[] = get(records)) {
if (!list.length) {
toast.error('记录为空,导出失败');
return;
}
const exportData = list.map((record) => {
const timestamp = record.timestamp ?? null;
const time = timestamp ? formatTimestamp(timestamp) : null;
const data =
record.display === 'hex'
? bufferToHexFormat(record.data)
: bufferToString(record.data);
return {
type: record.type,
data,
timestamp,
time,
display: record.display,
};
});
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const now = Date.now();
const d = new Date(now);
const pad = (n: number) => String(n).padStart(2, '0');
const fileName =
`records-${d.getFullYear()}-` +
`${pad(d.getMonth() + 1)}-` +
`${pad(d.getDate())}-` +
`${pad(d.getHours())}-` +
`${pad(d.getMinutes())}-` +
`${pad(d.getSeconds())}.json`;
const a = document.createElement('a');
a.href = url;
a.download = fileName;
URL.revokeObjectURL(url);
a.remove();
}
export async function copyRecordContent(record: RecordItem) {
let content = '';
if (record.display === 'hex') {
content = bufferToHexFormat(record.data);
} else if (record.display === 'ascii') {
content = bufferToString(record.data);
} else {
content = bufferToDecString(record.data);
}
try {
await navigator.clipboard.writeText(content);
toast.success('复制消息内容成功');
} catch (err) {
console.error('复制失败:', err);
toast.error('复制消息内容失败');
}
}
export function scrollToRecord(index: number) {
scrollToRecordIndex.set(index);
setTimeout(() => {
scrollToRecordIndex.set(-1);
}, 100);
}

View File

@ -0,0 +1,15 @@
export function formatTimestamp(ts: number) {
const d = new Date(ts);
const pad = (n: number, l = 2) => String(n).padStart(l, '0');
return (
`${d.getFullYear()}-` +
`${pad(d.getMonth() + 1)}-` +
`${pad(d.getDate())} ` +
`${pad(d.getHours())}:` +
`${pad(d.getMinutes())}:` +
`${pad(d.getSeconds())}:` +
`${pad(d.getMilliseconds(), 3)}`
);
}