feat: 更新文件

This commit is contained in:
duanshuwen
2025-10-19 15:29:58 +08:00
parent 98b3651a1b
commit c9ff2021fd
19 changed files with 445 additions and 60 deletions

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.vite
README.md

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -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();
}
})();

View File

@@ -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();
})();

View File

@@ -1,7 +1,9 @@
import { exposeThemeContext } from "./theme/theme-context"; import { exposeThemeContext } from "./theme/theme-context";
import { exposeWindowContext } from "./window/window-context"; import { exposeWindowContext } from "./window/window-context";
import { exposeRecordingContext } from "./recording/recording-context";
export default function exposeContexts() { export default function exposeContexts() {
exposeWindowContext(); exposeWindowContext();
exposeThemeContext(); exposeThemeContext();
exposeRecordingContext();
} }

View File

@@ -1,8 +1,6 @@
import { BrowserWindow } from "electron"; import { BrowserWindow } from "electron";
import { addThemeEventListeners } from "./theme/theme-listeners";
import { addWindowEventListeners } from "./window/window-listeners"; import { addWindowEventListeners } from "./window/window-listeners";
export default function registerListeners(mainWindow: BrowserWindow) { export default function registerListeners(mainWindow: BrowserWindow) {
addWindowEventListeners(mainWindow); addWindowEventListeners(mainWindow);
addThemeEventListeners();
} }

View File

@@ -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";

View File

@@ -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);
}
});
},
});
}

View File

@@ -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 };
}
});
}

View File

@@ -5,9 +5,9 @@ import {
THEME_MODE_SYSTEM_CHANNEL, THEME_MODE_SYSTEM_CHANNEL,
THEME_MODE_TOGGLE_CHANNEL, THEME_MODE_TOGGLE_CHANNEL,
} from "./theme-channels"; } from "./theme-channels";
import { contextBridge, ipcRenderer } from "electron";
export function exposeThemeContext() { export function exposeThemeContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
contextBridge.exposeInMainWorld("themeMode", { contextBridge.exposeInMainWorld("themeMode", {
current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL), current: () => ipcRenderer.invoke(THEME_MODE_CURRENT_CHANNEL),

View File

@@ -1,15 +1,14 @@
import { import {
WIN_MINIMIZE_CHANNEL, WIN_MINIMIZE_CHANNEL,
WIN_MAXIMIZE_CHANNEL, WIN_MAXIMIZE_CHANNEL,
WIN_CLOSE_CHANNEL, WIN_CLOSE_CHANNEL
} from "./window-channels"; } from "./window-channels";
import { contextBridge, ipcRenderer } from "electron";
export function exposeWindowContext() { export function exposeWindowContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
contextBridge.exposeInMainWorld("electronWindow", { contextBridge.exposeInMainWorld("electronWindow", {
minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL), minimize: () => ipcRenderer.invoke(WIN_MINIMIZE_CHANNEL),
maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL), maximize: () => ipcRenderer.invoke(WIN_MAXIMIZE_CHANNEL),
close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL), close: () => ipcRenderer.invoke(WIN_CLOSE_CHANNEL)
}); });
} }

View File

@@ -2,13 +2,14 @@ import { BrowserWindow, ipcMain } from "electron";
import { import {
WIN_CLOSE_CHANNEL, WIN_CLOSE_CHANNEL,
WIN_MAXIMIZE_CHANNEL, WIN_MAXIMIZE_CHANNEL,
WIN_MINIMIZE_CHANNEL, WIN_MINIMIZE_CHANNEL
} from "./window-channels"; } from "./window-channels";
export function addWindowEventListeners(mainWindow: BrowserWindow) { export function addWindowEventListeners(mainWindow: BrowserWindow) {
ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => { ipcMain.handle(WIN_MINIMIZE_CHANNEL, () => {
mainWindow.minimize(); mainWindow.minimize();
}); });
ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => { ipcMain.handle(WIN_MAXIMIZE_CHANNEL, () => {
if (mainWindow.isMaximized()) { if (mainWindow.isMaximized()) {
mainWindow.unmaximize(); mainWindow.unmaximize();
@@ -16,6 +17,7 @@ export function addWindowEventListeners(mainWindow: BrowserWindow) {
mainWindow.maximize(); mainWindow.maximize();
} }
}); });
ipcMain.handle(WIN_CLOSE_CHANNEL, () => { ipcMain.handle(WIN_CLOSE_CHANNEL, () => {
mainWindow.close(); mainWindow.close();
}); });

View File

@@ -37,13 +37,8 @@ export async function setTheme(newTheme: ThemeMode) {
localStorage.setItem(THEME_KEY, newTheme); 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() { export async function syncThemeWithLocal() {
const { local } = await getCurrentTheme(); const { local } = await getCurrentTheme();

View File

@@ -3,10 +3,6 @@
@tailwind utilities; @tailwind utilities;
body { body {
font-family: margin: 0;
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, padding: 0;
sans-serif;
margin: auto;
max-width: 38rem;
padding: 2rem;
} }

View File

@@ -8,6 +8,7 @@ import path from "node:path";
// app.quit(); // app.quit();
// } // }
const inDevelopment = process.env.NODE_ENV === "development";
const createWindow = () => { const createWindow = () => {
// Create the browser window. // Create the browser window.
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
@@ -18,10 +19,14 @@ const createWindow = () => {
maximizable: false, // 禁止最大化 maximizable: false, // 禁止最大化
minimizable: true, // 允许最小化 minimizable: true, // 允许最小化
webPreferences: { webPreferences: {
devTools: inDevelopment,
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
}, },
}); });
// 注册 IPC 事件监听器(包括录制功能)
registerListeners(mainWindow);
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
@@ -32,7 +37,7 @@ const createWindow = () => {
} }
// Open the DevTools. // Open the DevTools.
// mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
}; };
// This method will be called when Electron has finished // This method will be called when Electron has finished

View File

@@ -0,0 +1,3 @@
import exposeContexts from "./helpers/ipc/context-exposer";
exposeContexts();

View File

@@ -1,13 +1,13 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
const routes = [ const routes = [
// {
// path: "/",
// name: "Login",
// component: () => import("@/views/login/index.vue"),
// },
{ {
path: "/", path: "/",
name: "Login",
component: () => import("@/views/login/index.vue"),
},
{
path: "/home",
name: "Home", name: "Home",
component: () => import("@/views/home/index.vue"), component: () => import("@/views/home/index.vue"),
}, },

9
src/types.d.ts vendored
View File

@@ -1,8 +1,6 @@
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite // 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 // 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). // 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 // Preload types
interface ThemeModeContext { interface ThemeModeContext {
@@ -19,7 +17,14 @@ interface ElectronWindow {
close: () => Promise<void>; close: () => Promise<void>;
} }
interface RecordingContext {
runRecordingScript: () => Promise<unknown>;
onRecordingSave: (callback: (payload: unknown) => void) => void;
onRecordingEdit: (callback: (payload: unknown) => void) => void;
}
declare interface Window { declare interface Window {
themeMode: ThemeModeContext; themeMode: ThemeModeContext;
electronWindow: ElectronWindow; electronWindow: ElectronWindow;
recording: RecordingContext;
} }

View File

@@ -1,45 +1,16 @@
<template> <template>
<div class="home"> <div class="home">
<h2 class="text-2xl font-bold mb-4">欢迎使用智念科技 AI</h2> <h2 class="text-2xl font-bold mb-4">欢迎使用智念科技 AI</h2>
<div class="bg-white p-6 rounded-lg shadow-md"> <div class="bg-white p-6">
<h3 class="text-lg font-semibold mb-4">计数器示例</h3>
<div class="flex items-center space-x-4 mb-4">
<button <button
@click="counterStore.decrement" class="bg-blue-300 hover:bg-blue-500 text-white px-4 py-2 rounded mr-2"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded"
> >
- 打开百度
</button>
<span class="text-xl font-bold">{{ counterStore.count }}</span>
<button
@click="counterStore.increment"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
>
+
</button>
</div>
<div class="mb-4">
<p>双倍值: {{ counterStore.doubleCount }}</p>
</div>
<button
@click="counterStore.reset"
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded"
>
重置
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCounterStore } from "@/stores/counter";
const counterStore = useCounterStore();
</script> </script>
<style scoped>
.home {
max-width: 600px;
margin: 0 auto;
}
</style>