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:
+9
-1
@@ -8,5 +8,13 @@
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
"endOfLine": "lf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.json",
|
||||
"options": {
|
||||
"trailingComma": "none"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const CopyButton = ({
|
||||
return () => clearTimeout(timer);
|
||||
}, [status]);
|
||||
|
||||
const performCopy = async () => {
|
||||
const performCopy = useCallback(async () => {
|
||||
if (!text) {
|
||||
console.warn('没有提供要复制的文本');
|
||||
return false;
|
||||
@@ -38,7 +38,9 @@ const CopyButton = ({
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return false;
|
||||
}, [text]);
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
setStatus('copying');
|
||||
@@ -50,7 +52,7 @@ const CopyButton = ({
|
||||
console.error('复制时出错:', error);
|
||||
setStatus('error');
|
||||
}
|
||||
}, [text]);
|
||||
}, [performCopy]);
|
||||
|
||||
// 根据状态计算当前显示的文本
|
||||
const currentText =
|
||||
|
||||
@@ -77,6 +77,7 @@ export function DatetimeToTimestamp() {
|
||||
|
||||
setResult(finalResult.toString());
|
||||
} catch (err) {
|
||||
console.error('转换错误:', err);
|
||||
setError('转换失败,请检查输入格式');
|
||||
setResult('');
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const TIMESTAMP_UNITS = [
|
||||
|
||||
export function TimestampToDatetime() {
|
||||
/** @type {[string, function]} 输入的时间戳值 */
|
||||
const [timestampValue, setTimestampValue] = useState(Date.now());
|
||||
const [timestampValue, setTimestampValue] = useState(() => Date.now());
|
||||
|
||||
/** @type {[string, function]} 转换结果 */
|
||||
const [timestampResult, setTimestampResult] = useState('');
|
||||
@@ -87,6 +87,7 @@ export function TimestampToDatetime() {
|
||||
const result = formatWithZone(numericValue, selectedZone, unit);
|
||||
setTimestampResult(result);
|
||||
} catch (err) {
|
||||
console.error('转换时间戳出错:', err);
|
||||
setError('转换失败,请检查输入格式');
|
||||
setTimestampResult('');
|
||||
}
|
||||
|
||||
+17
-14
@@ -1,3 +1,6 @@
|
||||
import '../.wxt/types/imports.d.ts';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
|
||||
@@ -48,18 +51,18 @@ export default defineBackground(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
async function sendToActiveTab(message: {
|
||||
type: string;
|
||||
data?: any;
|
||||
activeTabId?: number;
|
||||
[key: string]: any;
|
||||
}) {
|
||||
let activeTabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
let activeTab = activeTabs[0];
|
||||
const sendTo = message.activeTabId || activeTab.id;
|
||||
await browser.tabs.sendMessage(sendTo!, message);
|
||||
}
|
||||
// async function _sendToActiveTab(message: {
|
||||
// type: string;
|
||||
// data?: unknown;
|
||||
// activeTabId?: number;
|
||||
// [key: string]: unknown;
|
||||
// }) {
|
||||
// const activeTabs = await browser.tabs.query({
|
||||
// active: true,
|
||||
// currentWindow: true,
|
||||
// });
|
||||
// const activeTab = activeTabs[0];
|
||||
// const sendTo = message.activeTabId || activeTab.id;
|
||||
// await browser.tabs.sendMessage(sendTo!, message);
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// import { useRecorder } from '../hooks/useRecorder';
|
||||
import '../.wxt/types/imports.d.ts';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
export default defineContentScript({
|
||||
// matches: ['*://*.google.com/*'],
|
||||
@@ -7,7 +8,7 @@ export default defineContentScript({
|
||||
main() {
|
||||
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') {
|
||||
// startRecord();
|
||||
console.log('Start recording command received in content script');
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import { record } from 'rrweb';
|
||||
|
||||
export const useRecorder = () => {
|
||||
// 存储停止录制函数
|
||||
@@ -14,7 +13,10 @@ export const useRecorder = () => {
|
||||
setIsRecording(true);
|
||||
|
||||
console.log('[content] rrweb started');
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.error('Failed to start recording:', error);
|
||||
setIsRecording(false);
|
||||
}
|
||||
}, [isRecording]);
|
||||
|
||||
const stopRecord = () => {
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
Generated
+3303
File diff suppressed because it is too large
Load Diff
+10
-6
@@ -4,11 +4,6 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,json,css,scss,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
@@ -18,7 +13,8 @@
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"lint": "eslint . --max-warnings=0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rrweb/all": "^2.0.0-alpha.18",
|
||||
@@ -44,11 +40,19 @@
|
||||
"@types/chrome": "^0.1.36",
|
||||
"@types/react": "^19.2.7",
|
||||
"@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",
|
||||
"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",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const RecordeReplayPage = () => {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -4,6 +4,9 @@
|
||||
/* --- 原有配置保持 --- */
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext", // 支持 import.meta
|
||||
"moduleResolution": "Bundler", // 或者用 "Node"
|
||||
|
||||
/* --- 1. 严格类型检查 (关键) --- */
|
||||
// 开启所有严格检查,包括 noImplicitAny。
|
||||
@@ -15,6 +18,8 @@
|
||||
"noUnusedLocals": true,
|
||||
// 函数参数没使用报错
|
||||
"noUnusedParameters": true,
|
||||
// 函数必须有返回值,防止遗漏 return
|
||||
"noImplicitReturns": true,
|
||||
// switch 语句没有 break 时报错
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
@@ -31,7 +36,9 @@
|
||||
// WXT 通常会自动处理,但在这里显式声明有助于 VS Code 智能提示。
|
||||
"paths": {
|
||||
"@/*": ["./entrypoints/*", "./components/*", "./utils/*", "./assets/*"]
|
||||
}
|
||||
},
|
||||
"types": ["chrome", "webextension-polyfill"],
|
||||
"noImplicitAny": false
|
||||
},
|
||||
// 确保包含你的源代码目录
|
||||
"include": [
|
||||
@@ -39,6 +46,9 @@
|
||||
"components/**/*",
|
||||
"utils/**/*",
|
||||
"assets/**/*",
|
||||
".wxt/types/**/*.ts"
|
||||
]
|
||||
"hooks/**/*",
|
||||
".wxt/types/**/*.ts",
|
||||
".wxt/types/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".wxt", "eslint.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import rrwebPlayer from 'rrweb-player';
|
||||
import { eventWithTime } from 'rrweb';
|
||||
|
||||
export const downloadHtml = (events) => {
|
||||
export const downloadHtml = (events: eventWithTime[]) => {
|
||||
if (events.length === 0) return;
|
||||
|
||||
// 1. 构建 HTML 模板字符串
|
||||
@@ -41,7 +41,7 @@ export const downloadHtml = (events) => {
|
||||
`;
|
||||
|
||||
// 2. 创建 Blob 并下载
|
||||
const blob = new Blob([htmlContent], {type: 'text/html'});
|
||||
const blob = new Blob([htmlContent], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
@@ -15,6 +15,7 @@ export const formatWithZone = (
|
||||
timeZone: zone,
|
||||
}).format(ms);
|
||||
} catch (e) {
|
||||
console.error('formatWithZone error:', e);
|
||||
return '格式错误';
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user