feat: 实现record全局store
- 工具函数:datacode用于处理编码,time用于格式化时间
This commit is contained in:
113
src/lib/stores/datacode.ts
Normal file
113
src/lib/stores/datacode.ts
Normal 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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll(' ', ' ')
|
||||
.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);
|
||||
}
|
||||
133
src/lib/stores/record/record.ts
Normal file
133
src/lib/stores/record/record.ts
Normal 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);
|
||||
}
|
||||
15
src/lib/stores/utils/time.ts
Normal file
15
src/lib/stores/utils/time.ts
Normal 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)}`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user