diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d80e99a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Add files here to ignore them from prettier formatting +/dist +/coverage +/.vite +README.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ab4c216 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": false, + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/src/automation/baidu-automation.js b/src/automation/baidu-automation.js new file mode 100644 index 0000000..90016a8 --- /dev/null +++ b/src/automation/baidu-automation.js @@ -0,0 +1,28 @@ +const { chromium } = require("playwright"); + +(async () => { + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + // 打开百度 + await page.goto("https://www.baidu.com"); + + // 输入搜索关键词并回车 + await page.fill("textarea#chat-textarea", "Playwright 自动化"); + await page.keyboard.press("Enter"); + + // 等待搜索结果加载 + await page.waitForSelector("#content_left"); + + console.log("百度搜索执行成功"); + } catch (e) { + console.error("自动化执行失败:", e); + process.exitCode = 1; + } finally { + // 保持几秒以方便观察,再关闭 + await page.waitForTimeout(2000); + await browser.close(); + } +})(); diff --git a/src/automation/recording-automation.js b/src/automation/recording-automation.js new file mode 100644 index 0000000..234ee72 --- /dev/null +++ b/src/automation/recording-automation.js @@ -0,0 +1,247 @@ +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +// 录制配置 +const recordingConfig = { + outputFile: 'recorded-steps.json', + headless: false, // 显示浏览器窗口 + slowMo: 100, // 减慢操作速度,便于观察 + viewport: { width: 1280, height: 720 } +}; + +// 存储录制的步骤 +let recordedSteps = []; +let recordingStartTime = Date.now(); +let isRecording = true; + +// 生成时间戳 +function getTimestamp() { + return Date.now() - recordingStartTime; +} + +// 记录步骤 +function recordStep(step) { + const stepWithTimestamp = { + ...step, + timestamp: getTimestamp() + }; + recordedSteps.push(stepWithTimestamp); + console.log(`📹 录制步骤: ${step.type} - ${step.description}`); +} + +// 保存录制的步骤到文件 +function saveRecordedSteps() { + const outputPath = path.join(__dirname, recordingConfig.outputFile); + const data = { + steps: recordedSteps, + metadata: { + totalSteps: recordedSteps.length, + duration: getTimestamp(), + recordedAt: new Date().toISOString() + } + }; + + fs.writeFileSync(outputPath, JSON.stringify(data, null, 2)); + console.log(`\n✅ 录制完成!共录制 ${recordedSteps.length} 个步骤`); + console.log(`📁 录制文件已保存到: ${outputPath}`); +} + +// 监听页面事件 +async function setupPageListeners(page) { + // 监听点击事件 + await page.exposeFunction('__recordClick', (selector, coordinates) => { + if (!isRecording) return; + + recordStep({ + type: 'click', + description: `点击元素: ${selector || '未知元素'}`, + selector: selector || '', + coordinates: coordinates || {} + }); + }); + + // 监听输入事件 + await page.exposeFunction('__recordInput', (selector, text) => { + if (!isRecording) return; + + recordStep({ + type: 'type', + description: `输入文本: "${text || ''}"`, + selector: selector || '', + text: text || '' + }); + }); + + // 注入脚本监听用户操作 + await page.addInitScript(() => { + // 监听点击事件 + document.addEventListener('click', (event) => { + const target = event.target; + const selector = generateSelector(target); + const coordinates = { x: event.clientX, y: event.clientY }; + + if (window.__recordClick) { + window.__recordClick(selector, coordinates); + } + }, true); + + // 监听输入事件 + document.addEventListener('input', (event) => { + const target = event.target; + const selector = generateSelector(target); + const text = target.value || target.textContent || ''; + + if (window.__recordInput) { + window.__recordInput(selector, text); + } + }, true); + + // 生成元素选择器 + function generateSelector(element) { + if (!element) return ''; + + // 优先使用 ID + if (element.id) { + return `#${element.id}`; + } + + // 使用 class 和标签名 + const tagName = element.tagName.toLowerCase(); + if (element.className) { + const classes = element.className.split(' ').filter(c => c.trim()); + if (classes.length > 0) { + return `${tagName}.${classes.join('.')}`; + } + } + + // 使用属性 + const attributes = ['name', 'placeholder', 'type']; + for (const attr of attributes) { + if (element.getAttribute(attr)) { + return `${tagName}[${attr}="${element.getAttribute(attr)}"]`; + } + } + + // 使用路径 + return getElementPath(element); + } + + function getElementPath(element) { + const path = []; + while (element && element.nodeType === Node.ELEMENT_NODE) { + let selector = element.nodeName.toLowerCase(); + if (element.id) { + selector += `#${element.id}`; + path.unshift(selector); + break; + } else { + let sibling = element; + let nth = 1; + while (sibling = sibling.previousElementSibling) { + if (sibling.nodeName.toLowerCase() === selector) nth++; + } + if (nth !== 1) selector += `:nth-of-type(${nth})`; + } + path.unshift(selector); + element = element.parentNode; + } + return path.join(' > '); + } + }); +} + +// 主录制函数 +async function startRecording() { + console.log('🎬 开始自动化录制...'); + console.log('请在浏览器中进行操作,系统会自动记录您的操作步骤'); + console.log('按 Ctrl+C 停止录制并保存结果\n'); + + let browser; + let page; + + try { + // 启动浏览器 + browser = await chromium.launch({ + headless: recordingConfig.headless, + slowMo: recordingConfig.slowMo + }); + + // 创建新页面 + page = await browser.newPage({ + viewport: recordingConfig.viewport + }); + + // 设置页面监听器 + await setupPageListeners(page); + + // 监听页面导航 + page.on('framenavigated', async (frame) => { + if (frame === page.mainFrame() && isRecording) { + recordStep({ + type: 'navigate', + description: `导航到: ${frame.url()}`, + url: frame.url() + }); + + // 重新设置监听器 + await setupPageListeners(page); + } + }); + + // 导航到百度首页作为起始页面 + console.log('🌐 正在打开百度首页...'); + await page.goto('https://www.baidu.com'); + + recordStep({ + type: 'navigate', + description: '打开百度首页', + url: 'https://www.baidu.com' + }); + + console.log('✅ 录制已启动!请在浏览器中进行操作...'); + console.log('💡 您可以:'); + console.log(' - 点击页面元素'); + console.log(' - 在输入框中输入文本'); + console.log(' - 导航到其他页面'); + console.log(' - 按回车键或其他键盘操作'); + console.log(''); + console.log('⚠️ 按 Ctrl+C 停止录制并保存结果'); + + // 保持浏览器打开,直到用户手动停止 + await new Promise((resolve) => { + process.on('SIGINT', () => { + console.log('\n🛑 用户停止录制'); + isRecording = false; + resolve(); + }); + }); + + } catch (error) { + console.error('❌ 录制过程中发生错误:', error); + } finally { + if (browser) { + // 保存录制的步骤 + saveRecordedSteps(); + await browser.close(); + } + } +} + +// 处理程序终止 +process.on('SIGINT', () => { + console.log('\n🛑 用户停止录制'); + saveRecordedSteps(); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\n🛑 程序终止'); + saveRecordedSteps(); + process.exit(0); +}); + +// 启动录制 +(async () => { + await startRecording(); +})(); \ No newline at end of file diff --git a/src/helpers/ipc/context-exposer.ts b/src/helpers/ipc/context-exposer.ts index 14aecd9..28dcc10 100644 --- a/src/helpers/ipc/context-exposer.ts +++ b/src/helpers/ipc/context-exposer.ts @@ -1,7 +1,9 @@ import { exposeThemeContext } from "./theme/theme-context"; import { exposeWindowContext } from "./window/window-context"; +import { exposeRecordingContext } from "./recording/recording-context"; export default function exposeContexts() { exposeWindowContext(); exposeThemeContext(); + exposeRecordingContext(); } diff --git a/src/helpers/ipc/listeners-register.ts b/src/helpers/ipc/listeners-register.ts index 8c86fe4..b24d9ca 100644 --- a/src/helpers/ipc/listeners-register.ts +++ b/src/helpers/ipc/listeners-register.ts @@ -1,8 +1,6 @@ import { BrowserWindow } from "electron"; -import { addThemeEventListeners } from "./theme/theme-listeners"; import { addWindowEventListeners } from "./window/window-listeners"; export default function registerListeners(mainWindow: BrowserWindow) { addWindowEventListeners(mainWindow); - addThemeEventListeners(); } diff --git a/src/helpers/ipc/recording/recording-channels.ts b/src/helpers/ipc/recording/recording-channels.ts new file mode 100644 index 0000000..6b11e43 --- /dev/null +++ b/src/helpers/ipc/recording/recording-channels.ts @@ -0,0 +1,5 @@ +// 自动化录制相关的 IPC 通道 + +export const RUN_RECORDING_SCRIPT_CHANNEL = "run-recording-script"; +export const RUN_RECORDING_SCRIPT_SAVE_CHANNEL = "run-recording-script-save"; +export const RUN_RECORDING_SCRIPT_EDIT_CHANNEL = "run-recording-script-edit"; diff --git a/src/helpers/ipc/recording/recording-context.ts b/src/helpers/ipc/recording/recording-context.ts new file mode 100644 index 0000000..5493d43 --- /dev/null +++ b/src/helpers/ipc/recording/recording-context.ts @@ -0,0 +1,35 @@ +import { + RUN_RECORDING_SCRIPT_CHANNEL, + RUN_RECORDING_SCRIPT_SAVE_CHANNEL, + RUN_RECORDING_SCRIPT_EDIT_CHANNEL, +} from "./recording-channels"; +import { contextBridge, ipcRenderer } from "electron"; + +export function exposeRecordingContext() { + contextBridge.exposeInMainWorld("recording", { + // 运行录制脚本 + runRecordingScript: () => ipcRenderer.invoke(RUN_RECORDING_SCRIPT_CHANNEL), + + // 监听录制脚本输出(保存) + onRecordingSave: (callback: (payload: unknown) => void) => { + ipcRenderer.on(RUN_RECORDING_SCRIPT_SAVE_CHANNEL, (_event, payload) => { + try { + callback(payload); + } catch (err) { + console.error("onRecordingSave callback error:", err); + } + }); + }, + + // 监听录制脚本输出(编辑) + onRecordingEdit: (callback: (payload: unknown) => void) => { + ipcRenderer.on(RUN_RECORDING_SCRIPT_EDIT_CHANNEL, (_event, payload) => { + try { + callback(payload); + } catch (err) { + console.error("onRecordingEdit callback error:", err); + } + }); + }, + }); +} diff --git a/src/helpers/ipc/recording/recording-listeners.ts b/src/helpers/ipc/recording/recording-listeners.ts new file mode 100644 index 0000000..1f506e6 --- /dev/null +++ b/src/helpers/ipc/recording/recording-listeners.ts @@ -0,0 +1,81 @@ +import { BrowserWindow, ipcMain, app } from "electron"; +import { spawn } from "child_process"; +import path from "path"; +import { RUN_RECORDING_SCRIPT_CHANNEL } from "./recording-channels"; + +export function addRecordingEventListeners(mainWindow: BrowserWindow) { + // 监听运行录制脚本的请求 + ipcMain.handle(RUN_RECORDING_SCRIPT_CHANNEL, async () => { + try { + console.log('🎬 主进程:开始执行录制脚本'); + + const basePath = app.isPackaged ? process.resourcesPath : app.getAppPath(); + const scriptPath = app.isPackaged + ? path.join(basePath, 'automation', 'recording-automation.js') + : path.join(basePath, 'src', 'automation', 'recording-automation.js'); + + // 启动录制脚本 + const recordingProcess = spawn('node', [scriptPath], { + stdio: ['pipe', 'pipe', 'pipe'], + detached: false + }); + + let stdout = ''; + let stderr = ''; + + // 监听标准输出 + recordingProcess.stdout.on('data', (data) => { + const message = data.toString(); + stdout += message; + console.log('录制脚本输出:', message); + + // 发送输出到渲染进程 + mainWindow.webContents.send(RECORDING_CHANNELS.RECORDING_SCRIPT_RESULT, { + type: 'stdout', + message: message + }); + }); + + // 监听错误输出 + recordingProcess.stderr.on('data', (data) => { + const message = data.toString(); + stderr += message; + console.error('录制脚本错误:', message); + + // 发送错误到渲染进程 + mainWindow.webContents.send(RECORDING_CHANNELS.RECORDING_SCRIPT_ERROR, { + type: 'stderr', + message: message + }); + }); + + // 监听进程退出 + recordingProcess.on('close', (code) => { + console.log(`录制脚本退出,退出码: ${code}`); + + mainWindow.webContents.send(RECORDING_CHANNELS.RECORDING_SCRIPT_RESULT, { + type: 'exit', + code: code, + stdout: stdout, + stderr: stderr + }); + }); + + // 监听进程错误 + recordingProcess.on('error', (error) => { + console.error('录制脚本启动失败:', error); + + mainWindow.webContents.send(RECORDING_CHANNELS.RECORDING_SCRIPT_ERROR, { + type: 'error', + message: error.message + }); + }); + + return { success: true, message: '录制脚本已启动' }; + + } catch (error) { + console.error('执行录制脚本失败:', error); + return { success: false, error: (error as Error).message }; + } + }); +} \ No newline at end of file diff --git a/src/helpers/ipc/theme/theme-context.ts b/src/helpers/ipc/theme/theme-context.ts index 1d493bd..ec0c7e9 100644 --- a/src/helpers/ipc/theme/theme-context.ts +++ b/src/helpers/ipc/theme/theme-context.ts @@ -5,9 +5,9 @@ import { THEME_MODE_SYSTEM_CHANNEL, THEME_MODE_TOGGLE_CHANNEL, } from "./theme-channels"; +import { contextBridge, ipcRenderer } from "electron"; export function exposeThemeContext() { - const { contextBridge, ipcRenderer } = window.require("electron"); contextBridge.exposeInMainWorld("themeMode", { current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL), diff --git a/src/helpers/ipc/window/window-context.ts b/src/helpers/ipc/window/window-context.ts index f066884..c1cdba3 100644 --- a/src/helpers/ipc/window/window-context.ts +++ b/src/helpers/ipc/window/window-context.ts @@ -1,15 +1,14 @@ import { WIN_MINIMIZE_CHANNEL, WIN_MAXIMIZE_CHANNEL, - WIN_CLOSE_CHANNEL, + WIN_CLOSE_CHANNEL } from "./window-channels"; +import { contextBridge, ipcRenderer } from "electron"; export function exposeWindowContext() { - const { contextBridge, ipcRenderer } = window.require("electron"); - contextBridge.exposeInMainWorld("electronWindow", { minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL), maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL), - close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL), + close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL) }); } diff --git a/src/helpers/ipc/window/window-listeners.ts b/src/helpers/ipc/window/window-listeners.ts index 5964f89..76086b3 100644 --- a/src/helpers/ipc/window/window-listeners.ts +++ b/src/helpers/ipc/window/window-listeners.ts @@ -2,13 +2,14 @@ import { BrowserWindow, ipcMain } from "electron"; import { WIN_CLOSE_CHANNEL, WIN_MAXIMIZE_CHANNEL, - WIN_MINIMIZE_CHANNEL, + WIN_MINIMIZE_CHANNEL } from "./window-channels"; export function addWindowEventListeners(mainWindow: BrowserWindow) { ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => { mainWindow.minimize(); }); + ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); @@ -16,6 +17,7 @@ export function addWindowEventListeners(mainWindow: BrowserWindow) { mainWindow.maximize(); } }); + ipcMain.handle(WIN_CLOSE_CHANNEL, () => { mainWindow.close(); }); diff --git a/src/helpers/theme_helpers.ts b/src/helpers/theme_helpers.ts index 7af078c..afbe325 100644 --- a/src/helpers/theme_helpers.ts +++ b/src/helpers/theme_helpers.ts @@ -37,13 +37,8 @@ export async function setTheme(newTheme: ThemeMode) { localStorage.setItem(THEME_KEY, newTheme); } -export async function toggleTheme() { - const isDarkMode = await window.themeMode.toggle(); - const newTheme = isDarkMode ? "dark" : "light"; - updateDocumentTheme(isDarkMode); - localStorage.setItem(THEME_KEY, newTheme); -} + export async function syncThemeWithLocal() { const { local } = await getCurrentTheme(); diff --git a/src/index.css b/src/index.css index 4e4bbec..5308f18 100644 --- a/src/index.css +++ b/src/index.css @@ -3,10 +3,6 @@ @tailwind utilities; body { - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, - sans-serif; - margin: auto; - max-width: 38rem; - padding: 2rem; + margin: 0; + padding: 0; } diff --git a/src/main.ts b/src/main.ts index d0a7e7d..3d896de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import path from "node:path"; // app.quit(); // } +const inDevelopment = process.env.NODE_ENV === "development"; const createWindow = () => { // Create the browser window. const mainWindow = new BrowserWindow({ @@ -18,10 +19,14 @@ const createWindow = () => { maximizable: false, // 禁止最大化 minimizable: true, // 允许最小化 webPreferences: { + devTools: inDevelopment, preload: path.join(__dirname, "preload.js"), }, }); + // 注册 IPC 事件监听器(包括录制功能) + registerListeners(mainWindow); + // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); @@ -32,7 +37,7 @@ const createWindow = () => { } // Open the DevTools. - // mainWindow.webContents.openDevTools(); + mainWindow.webContents.openDevTools(); }; // This method will be called when Electron has finished diff --git a/src/preload.ts b/src/preload.ts index e69de29..78d0e3c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -0,0 +1,3 @@ +import exposeContexts from "./helpers/ipc/context-exposer"; + +exposeContexts(); diff --git a/src/router/index.ts b/src/router/index.ts index 352d62b..6c2909d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,13 +1,13 @@ import { createRouter, createWebHistory } from "vue-router"; const routes = [ + // { + // path: "/", + // name: "Login", + // component: () => import("@/views/login/index.vue"), + // }, { path: "/", - name: "Login", - component: () => import("@/views/login/index.vue"), - }, - { - path: "/home", name: "Home", component: () => import("@/views/home/index.vue"), }, diff --git a/src/types.d.ts b/src/types.d.ts index 391d991..cdd3d9d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,8 +1,6 @@ // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite // plugin that tells the Electron app where to look for the Vite-bundled app code (depending on // whether you're running in development or production). -declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; -declare const MAIN_WINDOW_VITE_NAME: string; // Preload types interface ThemeModeContext { @@ -19,7 +17,14 @@ interface ElectronWindow { close: () => Promise; } +interface RecordingContext { + runRecordingScript: () => Promise; + onRecordingSave: (callback: (payload: unknown) => void) => void; + onRecordingEdit: (callback: (payload: unknown) => void) => void; +} + declare interface Window { themeMode: ThemeModeContext; electronWindow: ElectronWindow; + recording: RecordingContext; } diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 4342cad..49d1e18 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -1,45 +1,16 @@ -