feat: update package.json to remove lint-staged, add eslint configuration and dependencies

feat: add state management for recording in RecordeReplayPage component

refactor: remove bridge and storage services as they are no longer needed

chore: update tsconfig.json for improved type checking and module resolution

fix: update downloadHtml function to use eventWithTime type and improve formatting

fix: add error logging in formatWithZone function for better debugging
This commit is contained in:
雨霖铃
2026-01-31 18:00:01 +08:00
parent 88a612f9cf
commit 4213e70697
18 changed files with 3437 additions and 135 deletions
+1 -1
View File
@@ -1 +1 @@
npx lint-staged npx lint-staged
+9 -1
View File
@@ -8,5 +8,13 @@
"trailingComma": "all", "trailingComma": "all",
"bracketSpacing": true, "bracketSpacing": true,
"arrowParens": "always", "arrowParens": "always",
"endOfLine": "lf" "endOfLine": "lf",
"overrides": [
{
"files": "*.json",
"options": {
"trailingComma": "none"
}
}
]
} }
+5 -3
View File
@@ -21,7 +21,7 @@ const CopyButton = ({
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [status]); }, [status]);
const performCopy = async () => { const performCopy = useCallback(async () => {
if (!text) { if (!text) {
console.warn('没有提供要复制的文本'); console.warn('没有提供要复制的文本');
return false; return false;
@@ -38,7 +38,9 @@ const CopyButton = ({
return false; return false;
} }
} }
};
return false;
}, [text]);
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
setStatus('copying'); setStatus('copying');
@@ -50,7 +52,7 @@ const CopyButton = ({
console.error('复制时出错:', error); console.error('复制时出错:', error);
setStatus('error'); setStatus('error');
} }
}, [text]); }, [performCopy]);
// 根据状态计算当前显示的文本 // 根据状态计算当前显示的文本
const currentText = const currentText =
+1
View File
@@ -77,6 +77,7 @@ export function DatetimeToTimestamp() {
setResult(finalResult.toString()); setResult(finalResult.toString());
} catch (err) { } catch (err) {
console.error('转换错误:', err);
setError('转换失败,请检查输入格式'); setError('转换失败,请检查输入格式');
setResult(''); setResult('');
} }
+2 -1
View File
@@ -49,7 +49,7 @@ const TIMESTAMP_UNITS = [
export function TimestampToDatetime() { export function TimestampToDatetime() {
/** @type {[string, function]} 输入的时间戳值 */ /** @type {[string, function]} 输入的时间戳值 */
const [timestampValue, setTimestampValue] = useState(Date.now()); const [timestampValue, setTimestampValue] = useState(() => Date.now());
/** @type {[string, function]} 转换结果 */ /** @type {[string, function]} 转换结果 */
const [timestampResult, setTimestampResult] = useState(''); const [timestampResult, setTimestampResult] = useState('');
@@ -87,6 +87,7 @@ export function TimestampToDatetime() {
const result = formatWithZone(numericValue, selectedZone, unit); const result = formatWithZone(numericValue, selectedZone, unit);
setTimestampResult(result); setTimestampResult(result);
} catch (err) { } catch (err) {
console.error('转换时间戳出错:', err);
setError('转换失败,请检查输入格式'); setError('转换失败,请检查输入格式');
setTimestampResult(''); setTimestampResult('');
} }
+17 -14
View File
@@ -1,3 +1,6 @@
import '../.wxt/types/imports.d.ts';
import { browser } from 'wxt/browser';
export default defineBackground(() => { export default defineBackground(() => {
console.log('Hello background!', { id: browser.runtime.id }); console.log('Hello background!', { id: browser.runtime.id });
@@ -48,18 +51,18 @@ export default defineBackground(() => {
return true; return true;
}); });
async function sendToActiveTab(message: { // async function _sendToActiveTab(message: {
type: string; // type: string;
data?: any; // data?: unknown;
activeTabId?: number; // activeTabId?: number;
[key: string]: any; // [key: string]: unknown;
}) { // }) {
let activeTabs = await browser.tabs.query({ // const activeTabs = await browser.tabs.query({
active: true, // active: true,
currentWindow: true, // currentWindow: true,
}); // });
let activeTab = activeTabs[0]; // const activeTab = activeTabs[0];
const sendTo = message.activeTabId || activeTab.id; // const sendTo = message.activeTabId || activeTab.id;
await browser.tabs.sendMessage(sendTo!, message); // await browser.tabs.sendMessage(sendTo!, message);
} // }
}); });
+3 -2
View File
@@ -1,4 +1,5 @@
// import { useRecorder } from '../hooks/useRecorder'; import '../.wxt/types/imports.d.ts';
import { browser } from 'wxt/browser';
export default defineContentScript({ export default defineContentScript({
// matches: ['*://*.google.com/*'], // matches: ['*://*.google.com/*'],
@@ -7,7 +8,7 @@ export default defineContentScript({
main() { main() {
console.log('Content script loaded successfully', { id: browser.runtime.id }); console.log('Content script loaded successfully', { id: browser.runtime.id });
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'START_RECORD') { if (message.type === 'START_RECORD') {
// startRecord(); // startRecord();
console.log('Start recording command received in content script'); console.log('Start recording command received in content script');
+45
View File
@@ -0,0 +1,45 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import * as reactHooks from 'eslint-plugin-react-hooks';
import * as reactPlugin from 'eslint-plugin-react';
import globals from 'globals';
export default tseslint.config(
{ ignores: ['dist', '.wxt', 'node_modules', 'eslint.config.ts'] },
{
files: [
'hooks/**/*.{ts,tsx}',
'entrypoints/**/*.{ts,tsx}',
'pages/**/*.{ts,tsx}',
'utils/**/*.{ts,tsx}',
'components/**/*.{ts,tsx}',
'services/**/*.{ts,tsx}',
],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
react: reactPlugin as any, // 现在这里就算写 TS 语法也没事了,因为文件被忽略了
'react-hooks': reactHooks as any,
},
rules: {
...reactHooks.configs.recommended.rules,
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'react/react-in-jsx-scope': 'off',
},
},
);
+4 -2
View File
@@ -1,5 +1,4 @@
import { useRef, useState, useCallback } from 'react'; import { useRef, useState, useCallback } from 'react';
import { record } from 'rrweb';
export const useRecorder = () => { export const useRecorder = () => {
// 存储停止录制函数 // 存储停止录制函数
@@ -14,7 +13,10 @@ export const useRecorder = () => {
setIsRecording(true); setIsRecording(true);
console.log('[content] rrweb started'); console.log('[content] rrweb started');
} catch (error) {} } catch (error) {
console.error('Failed to start recording:', error);
setIsRecording(false);
}
}, [isRecording]); }, [isRecording]);
const stopRecord = () => { const stopRecord = () => {
+15
View File
@@ -0,0 +1,15 @@
export default {
// 对于代码文件:
'*.{ts,tsx,js,jsx}': [
// 1. ESLint: 依然检查具体文件,拦截未使用变量
'eslint --fix --max-warnings=0 --no-warn-ignored',
// 2. TypeScript: 使用函数形式
// 关键!这就告诉 lint-staged:“不要把文件名传给 tsc,直接运行这个命令就好”
// 这样 tsc 就会去读取 tsconfig.json,并正确排除 eslint.config.ts
() => 'tsc --noEmit --skipLibCheck',
],
// 对于其他文件:
'*.{json,css,scss,md}': ['prettier --write'],
};
+3303
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -4,11 +4,6 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
"dev:firefox": "wxt -b firefox", "dev:firefox": "wxt -b firefox",
@@ -18,7 +13,8 @@
"zip:firefox": "wxt zip -b firefox", "zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit", "compile": "tsc --noEmit",
"postinstall": "wxt prepare", "postinstall": "wxt prepare",
"prepare": "husky" "prepare": "husky",
"lint": "eslint . --max-warnings=0"
}, },
"dependencies": { "dependencies": {
"@rrweb/all": "^2.0.0-alpha.18", "@rrweb/all": "^2.0.0-alpha.18",
@@ -44,11 +40,19 @@
"@types/chrome": "^0.1.36", "@types/chrome": "^0.1.36",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/webextension-polyfill": "^0.12.4",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"@wxt-dev/module-react": "^1.1.5", "@wxt-dev/module-react": "^1.1.5",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^17.2.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"wxt": "^0.20.6" "wxt": "^0.20.6"
} }
} }
+2
View File
@@ -1,3 +1,5 @@
import { useEffect, useState } from 'react';
const RecordeReplayPage = () => { const RecordeReplayPage = () => {
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
-56
View File
@@ -1,56 +0,0 @@
// src/services/bridge.ts
import {record} from 'rrweb';
import {isExtension} from '../utils/env';
let stopFn = null;
let events = [];
/**
* 发送指令给录制器(Content Script 或 当前页面逻辑)
* @param action 'START_RECORD' | 'STOP_RECORD'
* @returns {Promise<unknown>}
*/
export const sendRecordCommand = async (action) => {
if (isExtension()) {
// === 插件环境 ===
try {
const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
if (tab?.id) {
// 使用 Promise 包装 sendMessage 以便统一 async/await 风格
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(tab.id, {action}, (response) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(response);
}
});
});
}
} catch (e) {
console.error("Extension communication error:", e);
}
} else {
// === 网页环境 (Web Demo / Localhost 调试) ===
console.log(`[Web Mode] Simulation: Sending ${action}`);
if (action === 'START_RECORD') {
stopFn = record({
emit(event) {
if (events.length > 1000) {
console.log('events length > 1000');
stopFn();
}
events.push(event);
}
});
} else if (action === 'STOP_RECORD') {
if (stopFn != null) {
stopFn();
}
console.log('[Web Mode] Simulation: Stopping recorder');
console.log('[Web Mode] Simulation: Events:', events);
console.log('[Web Mode] Simulation: Events:', events.length);
}
}
};
-40
View File
@@ -1,40 +0,0 @@
// src/services/storage.ts
import {isExtension} from '../utils/env';
export const storage = {
set: async (items) => {
if (isExtension()) {
return chrome.storage.local.set(items);
} else {
// Web 兼容:写入 localStorage
Object.keys(items).forEach(key => {
localStorage.setItem(key, JSON.stringify(items[key]));
});
return Promise.resolve();
}
},
get: async (keys) => {
if (isExtension()) {
return chrome.storage.local.get(keys);
} else {
// Web 兼容:读取 localStorage
const result = {};
const keyList = Array.isArray(keys) ? keys : [keys];
keyList.forEach(key => {
const val = localStorage.getItem(key);
if (val) result[key] = JSON.parse(val);
});
return Promise.resolve(result);
}
},
remove: async (key) => {
if (isExtension()) {
return chrome.storage.local.remove(key);
} else {
localStorage.removeItem(key);
return Promise.resolve();
}
}
};
+13 -3
View File
@@ -4,6 +4,9 @@
/* --- --- */ /* --- --- */
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"esModuleInterop": true,
"module": "ESNext", // 支持 import.meta
"moduleResolution": "Bundler", // 或者用 "Node"
/* --- 1. () --- */ /* --- 1. () --- */
// 开启所有严格检查,包括 noImplicitAny。 // 开启所有严格检查,包括 noImplicitAny。
@@ -15,6 +18,8 @@
"noUnusedLocals": true, "noUnusedLocals": true,
// 函数参数没使用报错 // 函数参数没使用报错
"noUnusedParameters": true, "noUnusedParameters": true,
// 函数必须有返回值,防止遗漏 return
"noImplicitReturns": true,
// switch 语句没有 break 时报错 // switch 语句没有 break 时报错
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
@@ -31,7 +36,9 @@
// WXT 通常会自动处理,但在这里显式声明有助于 VS Code 智能提示。 // WXT 通常会自动处理,但在这里显式声明有助于 VS Code 智能提示。
"paths": { "paths": {
"@/*": ["./entrypoints/*", "./components/*", "./utils/*", "./assets/*"] "@/*": ["./entrypoints/*", "./components/*", "./utils/*", "./assets/*"]
} },
"types": ["chrome", "webextension-polyfill"],
"noImplicitAny": false
}, },
// 确保包含你的源代码目录 // 确保包含你的源代码目录
"include": [ "include": [
@@ -39,6 +46,9 @@
"components/**/*", "components/**/*",
"utils/**/*", "utils/**/*",
"assets/**/*", "assets/**/*",
".wxt/types/**/*.ts" "hooks/**/*",
] ".wxt/types/**/*.ts",
".wxt/types/*.d.ts"
],
"exclude": ["node_modules", ".wxt", "eslint.config.ts"]
} }
+6 -6
View File
@@ -1,8 +1,8 @@
// import rrwebPlayer from 'rrweb-player'; import { eventWithTime } from 'rrweb';
export const downloadHtml = (events) => { export const downloadHtml = (events: eventWithTime[]) => {
if (events.length === 0) return; if (events.length === 0) return;
// 1. 构建 HTML 模板字符串 // 1. 构建 HTML 模板字符串
// 我们将 events 数据直接注入到 <script> 标签中 // 我们将 events 数据直接注入到 <script> 标签中
const htmlContent = ` const htmlContent = `
@@ -39,9 +39,9 @@ export const downloadHtml = (events) => {
</body> </body>
</html> </html>
`; `;
// 2. 创建 Blob 并下载 // 2. 创建 Blob 并下载
const blob = new Blob([htmlContent], {type: 'text/html'}); const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@@ -50,4 +50,4 @@ export const downloadHtml = (events) => {
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
+1
View File
@@ -15,6 +15,7 @@ export const formatWithZone = (
timeZone: zone, timeZone: zone,
}).format(ms); }).format(ms);
} catch (e) { } catch (e) {
console.error('formatWithZone error:', e);
return '格式错误'; return '格式错误';
} }
}; };