这个提交包含在:
xuqm 2025-10-24 17:31:14 +08:00
当前提交 66623aa663
共有 16 个文件被更改,包括 2588 次插入0 次删除

11
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,11 @@
/tmp
/out-tsc
/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.pnp
.pnp.js
.vscode/*

8
.idea/.gitignore 自动生成的 vendored 普通文件
查看文件

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

51
electron/logger.js 普通文件
查看文件

@ -0,0 +1,51 @@
const log = require('electron-log');
const { app } = require('electron');
const path = require('path');
const fs = require('fs');
class Logger {
constructor() {
this.initializeLogger();
}
initializeLogger() {
log.transports.file.resolvePath = () => {
const logsPath = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logsPath)) {
fs.mkdirSync(logsPath, { recursive: true });
}
return path.join(logsPath, 'app.log');
};
if (process.env.NODE_ENV === 'development') {
log.transports.console.level = 'info';
} else {
log.transports.console.level = false;
}
log.transports.file.level = 'info';
log.transports.file.maxSize = 10 * 1024 * 1024;
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
this.logger = log;
this.info('Logger initialized');
}
info(message, ...args) {
this.logger.info(message, ...args);
}
warn(message, ...args) {
this.logger.warn(message, ...args);
}
error(message, ...args) {
this.logger.error(message, ...args);
}
debug(message, ...args) {
this.logger.debug(message, ...args);
}
}
module.exports = Logger;

273
electron/main.js 普通文件
查看文件

@ -0,0 +1,273 @@
const { app, BrowserWindow, Tray, Menu, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const { wpsHelper } = require('./winaxHelper.js');
const Logger = require('./logger.js');
const isDev = process.env.NODE_ENV === 'development';
const logger = new Logger();
let mainWindow;
let loginWindow;
let settingsWindow;
let tray;
let appConfig = {
showFloatingWindow: true,
autoConnectWPS: true,
logLevel: 'info'
};
let isLoggedIn = false;
function createMainWindow() {
const { width, height } = require('electron').screen.getPrimaryDisplay().workAreaSize;
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
x: width - 1100,
y: 50,
show: false,
frame: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
title: 'AI Electron Client'
});
if (isDev) {
mainWindow.loadURL('http://localhost:5173/#/main');
} else {
mainWindow.loadFile('dist/index.html', { hash: 'main' });
}
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});
logger.info('Main window created');
}
function createLoginWindow() {
loginWindow = new BrowserWindow({
width: 350,
height: 450,
show: false,
modal: true,
frame: false,
resizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
if (isDev) {
loginWindow.loadURL('http://localhost:5173/#/login');
} else {
loginWindow.loadFile('dist/index.html', { hash: 'login' });
}
loginWindow.on('closed', () => {
loginWindow = null;
});
}
function createTray() {
const iconPath = path.join(__dirname, '../assets/tray.png');
tray = new Tray(iconPath || require('electron').nativeImage.createEmpty());
const contextMenu = Menu.buildFromTemplate([
{
label: '打开主界面',
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
}
},
{
label: '退出',
click: () => {
app.isQuitting = true;
app.quit();
}
}
]);
tray.setContextMenu(contextMenu);
tray.setToolTip('AI Electron Client');
}
// IPC 处理
ipcMain.handle('login', async (event, credentials) => {
try {
logger.info('Login attempt for user:', credentials.username);
await new Promise(resolve => setTimeout(resolve, 1000));
if (credentials.username && credentials.password) {
isLoggedIn = true;
wpsHelper.connectToWPS();
if (loginWindow) loginWindow.hide();
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
return { success: true, user: { name: credentials.username } };
} else {
return { success: false, message: '用户名和密码不能为空' };
}
} catch (error) {
logger.error('Login error:', error);
return { success: false, message: '登录失败' };
}
});
ipcMain.handle('logout', () => {
isLoggedIn = false;
if (mainWindow) mainWindow.hide();
wpsHelper.cleanup();
showLoginWindow();
});
ipcMain.handle('window-minimize', () => {
if (mainWindow) mainWindow.minimize();
});
ipcMain.handle('window-close', () => {
if (mainWindow) mainWindow.hide();
});
ipcMain.handle('show-settings', () => {
if (!settingsWindow) {
settingsWindow = new BrowserWindow({
width: 600,
height: 500,
modal: true,
parent: mainWindow,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
if (isDev) {
settingsWindow.loadURL('http://localhost:5173/#/settings');
} else {
settingsWindow.loadFile('dist/index.html', { hash: 'settings' });
}
}
settingsWindow.show();
});
ipcMain.handle('open-file', async (event, filePath) => {
try {
if (!filePath) {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Word Documents', extensions: ['doc', 'docx'] }
]
});
if (result.canceled) return { success: false };
filePath = result.filePaths[0];
}
const success = wpsHelper.openDocument(filePath);
return { success, message: success ? '文件已打开' : '打开失败' };
} catch (error) {
logger.error('Error opening file:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-wps-status', () => {
return wpsHelper.getStatus();
});
ipcMain.handle('navigate-paragraph', (event, direction) => {
return wpsHelper.navigateParagraph(direction);
});
ipcMain.handle('get-full-paragraph-content', () => {
return wpsHelper.getFullParagraphContent();
});
ipcMain.handle('update-paragraph-with-revisions', (event, content) => {
return wpsHelper.updateParagraphWithRevisions(content);
});
ipcMain.handle('handle-revision', (event, action, revisionIndex) => {
return wpsHelper.handleRevision(action, revisionIndex);
});
ipcMain.handle('add-comment', (event, commentText) => {
return wpsHelper.addComment(commentText);
});
ipcMain.handle('set-track-revisions', (event, track) => {
return wpsHelper.setTrackRevisions(track);
});
ipcMain.handle('switch-document', (event, filePath) => {
return wpsHelper.switchToDocument(filePath);
});
function showLoginWindow() {
if (!loginWindow) {
createLoginWindow();
}
loginWindow.show();
loginWindow.focus();
}
app.whenReady().then(() => {
logger.info('App is ready');
createLoginWindow();
createMainWindow();
createTray();
setTimeout(() => {
if (loginWindow) {
loginWindow.show();
loginWindow.focus();
}
}, 100);
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
app.isQuitting = true;
wpsHelper.cleanup();
});
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
}

35
electron/preload.js 普通文件
查看文件

@ -0,0 +1,35 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口操作
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
closeWindow: () => ipcRenderer.invoke('window-close'),
// 登录相关
login: (credentials) => ipcRenderer.invoke('login', credentials),
logout: () => ipcRenderer.invoke('logout'),
// 窗口管理
showSettings: () => ipcRenderer.invoke('show-settings'),
// 文件操作
openFile: (filePath) => ipcRenderer.invoke('open-file', filePath),
// WPS操作
getWPSStatus: () => ipcRenderer.invoke('get-wps-status'),
navigateParagraph: (direction) => ipcRenderer.invoke('navigate-paragraph', direction),
getFullParagraphContent: () => ipcRenderer.invoke('get-full-paragraph-content'),
updateParagraphWithRevisions: (content) => ipcRenderer.invoke('update-paragraph-with-revisions', content),
handleRevision: (action, revisionIndex) => ipcRenderer.invoke('handle-revision', action, revisionIndex),
addComment: (commentText) => ipcRenderer.invoke('add-comment', commentText),
setTrackRevisions: (track) => ipcRenderer.invoke('set-track-revisions', track),
switchDocument: (filePath) => ipcRenderer.invoke('switch-document', filePath),
// 事件监听
onWPSStatusChange: (callback) => ipcRenderer.on('wps-status-changed', callback),
onDocumentsListChange: (callback) => ipcRenderer.on('documents-list-changed', callback),
onActiveDocumentChange: (callback) => ipcRenderer.on('active-document-changed', callback),
onFullParagraphContentChange: (callback) => ipcRenderer.on('full-paragraph-content-changed', callback),
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
});

572
electron/winaxHelper.js 普通文件
查看文件

@ -0,0 +1,572 @@
const { ipcMain, BrowserWindow } = require('electron');
const path = require('path');
const fs = require('fs');
let winax;
try {
winax = require('winax');
console.log('✅ Winax loaded successfully');
} catch (error) {
console.warn('❌ Winax not available:', error.message);
}
class WPSHelper {
constructor() {
this.wpsApp = null;
this.activeDocument = null;
this.isConnected = false;
this.documents = new Map();
this.checkInterval = null;
this.lastParagraphContent = '';
this.startMonitoring();
console.log('🔄 WPSHelper initialized');
}
connectToWPS() {
if (!winax) {
console.warn('⚠️ Winax not available, using fallback mode');
this.setupFallbackMode();
return false;
}
try {
this.wpsApp = new winax.Object('KWPS.Application');
this.wpsApp.Visible = true;
this.isConnected = true;
console.log('✅ Connected to WPS');
this.setupEventListeners();
this.startMonitoring();
this.sendWPSStatus();
return true;
} catch (error) {
console.error('❌ Failed to connect to WPS:', error);
this.isConnected = false;
this.setupFallbackMode();
return false;
}
}
setupEventListeners() {
this.lastActiveDocument = null;
this.lastDocumentCount = 0;
}
startMonitoring() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
this.checkInterval = setInterval(() => {
this.checkWPSStatus();
this.updateDocumentsList();
}, 2000);
}
checkWPSStatus() {
try {
if (this.wpsApp && this.isConnected) {
try {
const name = this.wpsApp.Name;
this.isConnected = true;
} catch (error) {
this.isConnected = false;
this.wpsApp = null;
}
} else {
this.isConnected = false;
}
} catch (error) {
this.isConnected = false;
this.wpsApp = null;
}
}
updateDocumentsList() {
if (!this.isConnected || !this.wpsApp) {
if (!this.isConnected) {
this.simulateDocumentsList();
}
return;
}
try {
const newDocs = new Map();
let documents;
try {
documents = this.wpsApp.Documents;
} catch (error) {
return;
}
for (let i = 1; i <= documents.Count; i++) {
try {
const doc = documents.Item(i);
const fullName = doc.FullName;
const name = doc.Name;
newDocs.set(fullName, {
name: name,
path: fullName,
document: doc,
isActive: doc === this.activeDocument
});
} catch (error) {
console.warn('Error accessing document:', error);
}
}
this.documents = newDocs;
this.sendDocumentsList();
} catch (error) {
console.error('Error updating documents list:', error);
}
}
getFullParagraphContent() {
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
return this.getFallbackParagraphContent();
}
try {
const selection = this.wpsApp.Selection;
if (!selection) {
return this.getFallbackParagraphContent();
}
const originalStart = selection.Start;
const originalEnd = selection.End;
try {
selection.Expand(5); // wdParagraph
const text = selection.Text;
const revisions = this.getRevisionsInSelection(selection);
const comments = this.getCommentsInSelection(selection);
selection.SetRange(originalStart, originalEnd);
return {
text: text.trim(),
html: text.trim(),
revisions: revisions,
comments: comments,
hasRevisions: revisions.length > 0,
hasComments: comments.length > 0
};
} catch (error) {
selection.SetRange(originalStart, originalEnd);
return this.getFallbackParagraphContent();
}
} catch (error) {
return this.getFallbackParagraphContent();
}
}
getRevisionsInSelection(selection) {
const revisions = [];
if (!selection || !selection.Revisions) return revisions;
try {
const revisionsCount = selection.Revisions.Count;
for (let i = 1; i <= revisionsCount; i++) {
try {
const revision = selection.Revisions.Item(i);
revisions.push({
type: 'insert',
author: revision.Author || '未知作者',
date: revision.Date || new Date(),
text: revision.Range.Text || '',
index: i
});
} catch (error) {
console.warn('Error getting revision:', error);
}
}
} catch (error) {
console.warn('Could not access revisions:', error.message);
}
return revisions;
}
getCommentsInSelection(selection) {
const comments = [];
if (!selection || !selection.Comments) return comments;
try {
const commentsCount = selection.Comments.Count;
for (let i = 1; i <= commentsCount; i++) {
try {
const comment = selection.Comments.Item(i);
comments.push({
author: comment.Author || '未知作者',
date: comment.Date || new Date(),
text: comment.Range.Text || '',
index: i
});
} catch (error) {
console.warn('Error getting comment:', error);
}
}
} catch (error) {
console.warn('Could not access comments:', error.message);
}
return comments;
}
getFallbackParagraphContent() {
return {
text: this.lastParagraphContent || '',
html: this.lastParagraphContent || '',
revisions: [],
comments: [],
hasRevisions: false,
hasComments: false
};
}
updateParagraphWithRevisions(newContent) {
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
return { success: false, error: 'WPS not connected' };
}
try {
const selection = this.wpsApp.Selection;
if (!selection) {
return { success: false, error: 'No selection available' };
}
const originalStart = selection.Start;
const originalEnd = selection.End;
try {
selection.Expand(5); // wdParagraph
const originalText = selection.Text.trim();
if (originalText === newContent.trim()) {
return { success: true, unchanged: true };
}
selection.Text = newContent;
this.lastParagraphContent = newContent;
return {
success: true,
originalContent: originalText,
newContent: newContent,
hasChanges: true
};
} catch (error) {
return { success: false, error: error.message };
} finally {
try {
selection.SetRange(originalStart, originalStart);
} catch (e) {}
}
} catch (error) {
return { success: false, error: error.message };
}
}
navigateParagraph(direction) {
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
return false;
}
try {
const selection = this.wpsApp.Selection;
if (!selection) {
return false;
}
if (direction === 'prev') {
selection.MoveUp(5, 1); // wdParagraph
} else if (direction === 'next') {
selection.MoveDown(5, 1); // wdParagraph
}
return true;
} catch (error) {
console.error('Error navigating paragraphs:', error);
return false;
}
}
openDocument(filePath) {
if (!this.isConnected || !this.wpsApp) {
console.log('WPS not connected, using fallback');
return false;
}
try {
if (!fs.existsSync(filePath)) {
throw new Error('File does not exist: ' + filePath);
}
const doc = this.wpsApp.Documents.Open(filePath);
this.activeDocument = doc;
this.documents.set(filePath, {
name: doc.Name,
path: filePath,
document: doc,
isActive: true
});
this.sendDocumentsList();
this.sendActiveDocumentChange();
console.log('Document opened successfully:', path.basename(filePath));
return true;
} catch (error) {
console.error('Error opening document:', error);
return false;
}
}
switchToDocument(filePath) {
if (!this.isConnected || !this.wpsApp) {
return false;
}
const docInfo = this.documents.get(filePath);
if (docInfo && docInfo.document) {
try {
docInfo.document.Activate();
this.activeDocument = docInfo.document;
this.sendActiveDocumentChange();
return true;
} catch (error) {
console.error('Error switching document:', error);
}
}
return false;
}
handleRevision(action, revisionIndex) {
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
return { success: false, error: 'WPS not connected' };
}
try {
if (revisionIndex !== null) {
const revision = this.activeDocument.Revisions.Item(revisionIndex);
if (action === 'accept') {
revision.Accept();
} else {
revision.Reject();
}
} else {
const revisions = this.activeDocument.Revisions;
if (action === 'accept') {
revisions.AcceptAll();
} else {
revisions.RejectAll();
}
}
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
addComment(commentText) {
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
return { success: false, error: 'WPS not connected' };
}
try {
const selection = this.wpsApp.Selection;
if (!selection) {
return { success: false, error: 'No selection available' };
}
const comment = selection.Comments.Add(selection.Range);
comment.Range.Text = commentText;
return { success: true, comment: commentText };
} catch (error) {
return { success: false, error: error.message };
}
}
setTrackRevisions(track) {
if (!this.isConnected || !this.wpsApp) return false;
try {
this.wpsApp.ActiveDocument.TrackRevisions = track;
return true;
} catch (error) {
console.warn('Could not set track revisions:', error.message);
return false;
}
}
setupFallbackMode() {
console.log('Setting up fallback mode');
this.isConnected = false;
this.startMonitoring();
setTimeout(() => {
this.simulateDocumentsList();
}, 2000);
}
simulateDocumentsList() {
const mockDocuments = [
{
name: '示例文档.docx',
path: 'C:\\Documents\\示例文档.docx',
isActive: true
}
];
this.sendDocumentsList(mockDocuments);
const sampleParagraph = `这是一个示例段落内容。在回退模式下,您可以查看界面功能,但实际的WPS文档操作需要安装WPS并确保Winax正常工作。
当前功能包括
文档列表显示
段落内容显示
基本的导航操作
文件拖拽处理
要启用完整的WPS集成功能请确保
1. 已安装WPS Office
2. Winax模块正确安装
3. 应用程序有足够的权限访问COM组件`;
this.sendFullParagraphContent({
text: sampleParagraph,
html: sampleParagraph,
revisions: [],
comments: [],
hasRevisions: false,
hasComments: false
});
}
sendWPSStatus() {
if (ipcMain) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
if (window && !window.isDestroyed()) {
try {
window.webContents.send('wps-status-changed', {
connected: this.isConnected,
hasActiveDocument: !!this.activeDocument,
documentCount: this.documents.size,
timestamp: new Date().toISOString()
});
} catch (error) {
console.warn('Error sending WPS status:', error);
}
}
});
}
}
sendDocumentsList(documents = null) {
if (ipcMain) {
const docs = documents || Array.from(this.documents.values()).map(doc => ({
name: doc.name,
path: doc.path,
isActive: doc.document === this.activeDocument
}));
const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
if (window && !window.isDestroyed()) {
try {
window.webContents.send('documents-list-changed', docs);
} catch (error) {
console.warn('Error sending documents list:', error);
}
}
});
}
}
sendActiveDocumentChange() {
if (ipcMain && this.activeDocument) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
if (window && !window.isDestroyed()) {
try {
window.webContents.send('active-document-changed', {
name: this.activeDocument.Name,
path: this.activeDocument.FullName
});
} catch (error) {
console.warn('Error sending active document change:', error);
}
}
});
}
}
sendFullParagraphContent(content) {
if (ipcMain) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(window => {
if (window && !window.isDestroyed()) {
try {
window.webContents.send('full-paragraph-content-changed', content);
} catch (error) {
console.warn('Error sending full paragraph content:', error);
}
}
});
}
}
getStatus() {
if (!this.isConnected) this.connectToWPS();
return {
connected: this.isConnected,
activeDocument: this.activeDocument ? {
name: this.activeDocument.Name,
path: this.activeDocument.FullName
} : null,
documents: Array.from(this.documents.values()).map(doc => ({
name: doc.name,
path: doc.path,
isActive: doc.document === this.activeDocument
})),
currentParagraph: this.lastParagraphContent,
timestamp: new Date().toISOString()
};
}
cleanup() {
console.log('Cleaning up WPSHelper resources...');
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.wpsApp) {
try {
console.log('WPS application reference cleaned');
} catch (error) {
console.warn('Error cleaning WPS application:', error);
}
this.wpsApp = null;
}
this.activeDocument = null;
this.isConnected = false;
this.documents.clear();
console.log('WPSHelper cleanup completed');
}
}
const wpsHelper = new WPSHelper();
module.exports = { wpsHelper };

12
index.html 普通文件
查看文件

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Electron Client</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

58
package.json 普通文件
查看文件

@ -0,0 +1,58 @@
{
"name": "ai-electron-client",
"private": true,
"version": "1.0.0",
"main": "electron/main.js",
"description": "AI Electron Client",
"author": "Your Name",
"license": "MIT",
"scripts": {
"dev": "concurrently \"yarn serve\" \"yarn electron-dev\"",
"serve": "vite",
"electron-dev": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
"build": "vue-tsc -b && vite build",
"dist": "yarn build && electron-builder",
"postinstall": "electron-builder install-app-deps"
},
"dependencies": {
"vue": "3.5.22",
"vue-router": "4.5.0",
"pinia": "2.1.7",
"winax": "3.6.2",
"electron-updater": "6.3.0-alpha.6",
"electron-log": "5.1.2",
"chokidar": "4.0.3"
},
"devDependencies": {
"@types/node": "24.6.0",
"@vitejs/plugin-vue": "6.0.1",
"concurrently": "9.2.1",
"cross-env": "10.1.0",
"electron": "38.2.2",
"electron-builder": "26.1.0",
"rimraf": "6.0.1",
"typescript": "5.9.3",
"vite": "npm:rolldown-vite@7.1.14",
"vue-tsc": "3.1.0",
"wait-on": "9.0.1"
},
"build": {
"appId": "com.example.ai-electron",
"productName": "AI Electron Client",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"electron/**/*"
],
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}

25
src/App.vue 普通文件
查看文件

@ -0,0 +1,25 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
}
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>

26
src/main.ts 普通文件
查看文件

@ -0,0 +1,26 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHashHistory } from 'vue-router'
import App from './App.vue'
import LoginWindow from './views/LoginWindow.vue'
import MainWindow from './views/MainWindow.vue'
import SettingsWindow from './views/SettingsWindow.vue'
const routes = [
{ path: '/login', component: LoginWindow },
{ path: '/main', component: MainWindow },
{ path: '/settings', component: SettingsWindow },
{ path: '/', redirect: '/login' }
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')

97
src/stores/wpsStore.ts 普通文件
查看文件

@ -0,0 +1,97 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useWPSStore = defineStore('wps', () => {
const connectionStatus = ref<'connected' | 'disconnected'>('disconnected');
const activeDocument = ref<any>(null);
const documents = ref<any[]>([]);
const paragraphContent = ref<any>({
text: '',
html: '',
revisions: [],
comments: [],
hasRevisions: false,
hasComments: false
});
const hasUnsavedChanges = ref(false);
const autoSync = ref(true);
const isConnected = computed(() => connectionStatus.value === 'connected');
const hasRevisions = computed(() => paragraphContent.value.revisions.length > 0);
const hasComments = computed(() => paragraphContent.value.comments.length > 0);
const canEdit = computed(() => isConnected.value && !!activeDocument.value);
const setupEventListeners = () => {
window.electronAPI.onWPSStatusChange((_event, status) => {
connectionStatus.value = status.connected ? 'connected' : 'disconnected';
documents.value = status.documents || [];
activeDocument.value = status.activeDocument || null;
});
window.electronAPI.onDocumentsListChange((_event, docs) => {
documents.value = docs;
});
window.electronAPI.onActiveDocumentChange((_event, doc) => {
activeDocument.value = doc;
hasUnsavedChanges.value = false;
});
window.electronAPI.onFullParagraphContentChange((_event, content) => {
paragraphContent.value = content;
hasUnsavedChanges.value = false;
});
};
const refreshParagraph = async (): Promise<boolean> => {
if (!isConnected.value) return false;
try {
const content = await window.electronAPI.getFullParagraphContent();
paragraphContent.value = content;
hasUnsavedChanges.value = false;
return true;
} catch (error) {
console.error('Failed to refresh paragraph:', error);
return false;
}
};
const updateParagraph = async (content: string) => {
if (!isConnected.value) return { success: false, error: 'WPS not connected' };
try {
const result = await window.electronAPI.updateParagraphWithRevisions(content);
if (result.success) {
hasUnsavedChanges.value = false;
await refreshParagraph();
}
return result;
} catch (error: any) {
return { success: false, error: error.message };
}
};
const navigateParagraph = async (direction: 'prev' | 'next') => {
if (!isConnected.value) return false;
return await window.electronAPI.navigateParagraph(direction);
};
return {
connectionStatus,
activeDocument,
documents,
paragraphContent,
hasUnsavedChanges,
autoSync,
isConnected,
hasRevisions,
hasComments,
canEdit,
setupEventListeners,
refreshParagraph,
updateParagraph,
navigateParagraph
};
});

29
src/types/electron.d.ts vendored 普通文件
查看文件

@ -0,0 +1,29 @@
interface ElectronAPI {
minimizeWindow: () => Promise<void>;
closeWindow: () => Promise<void>;
login: (credentials: any) => Promise<any>;
logout: () => Promise<void>;
showSettings: () => Promise<void>;
openFile: (filePath?: string) => Promise<any>;
getWPSStatus: () => Promise<any>;
navigateParagraph: (direction: string) => Promise<boolean>;
getFullParagraphContent: () => Promise<any>;
updateParagraphWithRevisions: (content: string) => Promise<any>;
handleRevision: (action: string, revisionIndex: number | null) => Promise<any>;
addComment: (commentText: string) => Promise<any>;
setTrackRevisions: (track: boolean) => Promise<boolean>;
switchDocument: (filePath: string) => Promise<boolean>;
onWPSStatusChange: (callback: (event: any, status: any) => void) => void;
onDocumentsListChange: (callback: (event: any, documents: any[]) => void) => void;
onActiveDocumentChange: (callback: (event: any, document: any) => void) => void;
onFullParagraphContentChange: (callback: (event: any, content: any) => void) => void;
removeAllListeners: (channel: string) => void;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
export {};

227
src/views/LoginWindow.vue 普通文件
查看文件

@ -0,0 +1,227 @@
<template>
<div class="login-window">
<div class="login-container">
<div class="login-header">
<h2>AI Electron Client</h2>
<p>请登录您的账户</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="credentials.username"
type="text"
required
placeholder="请输入用户名"
:disabled="loading"
>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="credentials.password"
type="password"
required
placeholder="请输入密码"
:disabled="loading"
>
</div>
<button
type="submit"
class="login-btn"
:disabled="loading || !credentials.username || !credentials.password"
>
<span v-if="loading" class="loading-spinner"></span>
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="demo-info">
<p>演示账号: 任意用户名和密码</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const loading = ref(false);
const error = ref('');
const credentials = reactive({
username: '',
password: ''
});
const handleLogin = async () => {
if (!credentials.username.trim() || !credentials.password.trim()) {
error.value = '请输入用户名和密码';
return;
}
loading.value = true;
error.value = '';
try {
const result = await window.electronAPI.login(credentials);
if (result && result.success) {
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('userInfo', JSON.stringify(result.user));
router.push('/main');
} else {
error.value = result?.message || '登录失败';
}
} catch (err) {
error.value = '登录过程中发生错误';
} finally {
loading.value = false;
}
};
onMounted(() => {
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (isLoggedIn === 'true') {
router.push('/main');
}
});
</script>
<style scoped>
.login-window {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 350px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #2c3e50;
margin-bottom: 10px;
}
.login-header p {
color: #7f8c8d;
font-size: 14px;
}
.login-form {
display: flex;
flex-direction: column;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #2c3e50;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
}
.login-btn {
background: #3498db;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.login-btn:hover:not(:disabled) {
background: #2980b9;
}
.login-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
margin-top: 15px;
padding: 10px;
background: #e74c3c;
color: white;
border-radius: 4px;
font-size: 14px;
text-align: center;
}
.demo-info {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #3498db;
}
.demo-info p {
margin: 0;
font-size: 12px;
color: #6c757d;
text-align: center;
}
</style>

882
src/views/MainWindow.vue 普通文件
查看文件

@ -0,0 +1,882 @@
<template>
<div class="main-window">
<div class="title-bar">
<div class="title">AI Electron Client - WPS文档助手</div>
<div class="window-controls">
<button class="control-btn minimize" @click="minimize"></button>
<button class="control-btn close" @click="close">×</button>
</div>
</div>
<div class="content">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<h3>已打开文档</h3>
<div class="header-actions">
<button @click="refreshDocuments" class="action-btn" title="刷新">
🔄
</button>
<button @click="openFile" class="action-btn" title="打开文件">
📁
</button>
</div>
</div>
<div class="document-list">
<div
v-for="doc in documents"
:key="doc.path"
class="doc-item"
:class="{ active: doc.isActive }"
@click="switchDocument(doc.path)"
>
<div class="doc-icon">📄</div>
<div class="doc-info">
<div class="doc-name">{{ doc.name }}</div>
<div class="doc-path">{{ getShortPath(doc.path) }}</div>
</div>
<div v-if="doc.isActive" class="active-indicator"></div>
</div>
<div v-if="documents.length === 0" class="empty-state">
<div class="empty-icon">📝</div>
<p>暂无打开的文档</p>
<button class="open-file-btn" @click="openFile">打开文件</button>
</div>
</div>
<div class="wps-status" :class="{ connected: isConnected }">
<div class="status-icon"></div>
<span class="status-text">{{ isConnected ? 'WPS 已连接' : 'WPS 未连接' }}</span>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="status-bar" :class="{ connected: isConnected }">
<div class="status-info">
<div class="status-indicator"></div>
<span class="status-text">{{ statusText }}</span>
</div>
<div class="status-details">
<span v-if="activeDocument" class="active-doc">当前文档: {{ activeDocument.name }}</span>
<span class="document-count">文档数: {{ documents.length }}</span>
<span class="revision-count" v-if="hasRevisions">{{ paragraphContent.revisions.length }} 个修订</span>
</div>
</div>
<div class="editor-area">
<!-- 导航 -->
<div class="navigation">
<button @click="navigateParagraph('prev')" :disabled="!canNavigate"> 上一段</button>
<div class="navigation-info">
<span v-if="paragraphContent.text && isConnected" class="has-content">
当前段落 ({{ paragraphContent.text.length }} 字符)
</span>
<span v-else-if="isConnected" class="no-content">未选择段落</span>
<span v-else class="disconnected">WPS 未连接</span>
</div>
<button @click="navigateParagraph('next')" :disabled="!canNavigate">下一段 </button>
</div>
<!-- 修订面板 -->
<div v-if="hasRevisions" class="revisions-panel">
<h4>修订内容 ({{ paragraphContent.revisions.length }})</h4>
<div class="revisions-list">
<div v-for="revision in paragraphContent.revisions" :key="revision.index" class="revision-item">
<div class="revision-header">
<span class="revision-type">{{ revision.type }}</span>
<span class="revision-author">{{ revision.author }}</span>
</div>
<div class="revision-content">{{ revision.text }}</div>
<div class="revision-actions">
<button @click="handleRevision('accept', revision.index)" class="action-btn accept">接受</button>
<button @click="handleRevision('reject', revision.index)" class="action-btn reject">拒绝</button>
</div>
</div>
</div>
</div>
<!-- 编辑器 -->
<div class="editor-container">
<div class="editor-header">
<h4>段落内容编辑</h4>
<div class="editor-actions">
<button @click="updateParagraphWithRevisions" :disabled="!hasUnsavedChanges" class="save-btn">
💾 保存
</button>
<button @click="refreshParagraphContent" class="refresh-btn">
🔄 刷新
</button>
</div>
</div>
<textarea
v-model="editableContent"
@input="onParagraphInput"
:disabled="!canEdit"
class="paragraph-editor"
:class="{ 'has-unsaved': hasUnsavedChanges }"
placeholder="段落内容将显示在这里..."
rows="12"
></textarea>
<div class="editor-footer">
<label class="auto-sync-toggle">
<input type="checkbox" v-model="autoSync"> 自动保存
</label>
<div class="editor-stats">
<span class="char-count">{{ editableContent.length }} 字符</span>
<span v-if="hasUnsavedChanges" class="unsaved-badge">未保存</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button @click="openFile" :disabled="!isConnected" class="btn primary">
📁 打开文件
</button>
<button @click="refreshDocuments" class="btn secondary">
🔄 刷新列表
</button>
<button @click="openSettings" class="btn secondary">
设置
</button>
<button @click="logout" class="btn danger">
🚪 退出
</button>
</div>
</div>
<!-- 连接提示 -->
<div v-if="!isConnected" class="connection-prompt">
<div class="prompt-content">
<div class="prompt-icon"></div>
<h3>WPS 未连接</h3>
<p>无法与 WPS Office 建立连接</p>
<button @click="refreshDocuments" class="btn primary">
重新连接
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useWPSStore } from '../stores/wpsStore';
const router = useRouter();
const wpsStore = useWPSStore();
const {
connectionStatus: isConnected,
activeDocument,
documents,
paragraphContent,
hasUnsavedChanges,
autoSync,
hasRevisions,
canEdit,
setupEventListeners,
refreshParagraph,
updateParagraph,
navigateParagraph
} = wpsStore;
const editableContent = ref('');
const editTimer = ref<any>(null);
const statusText = computed(() => {
if (!isConnected.value) return 'WPS 未连接';
if (!activeDocument.value) return 'WPS 已连接 - 无活动文档';
return `WPS 已连接 - 正在编辑: ${activeDocument.value.name}`;
});
const minimize = () => window.electronAPI.minimizeWindow();
const close = () => window.electronAPI.closeWindow();
const openFile = async () => {
try {
await window.electronAPI.openFile();
} catch (error) {
console.error('Failed to open file:', error);
}
};
const refreshDocuments = async () => {
try {
await window.electronAPI.getWPSStatus();
} catch (error) {
console.error('Failed to refresh documents:', error);
}
};
const refreshParagraphContent = () => {
refreshParagraph().then(() => {
editableContent.value = paragraphContent.value.text;
});
};
const updateParagraphWithRevisions = async () => {
const result = await updateParagraph(editableContent.value);
if (result.success) {
console.log('段落已更新');
}
};
const handleRevision = async (action: string, revisionIndex: number) => {
await window.electronAPI.handleRevision(action, revisionIndex);
refreshParagraphContent();
};
const switchDocument = async (filePath: string) => {
await window.electronAPI.switchDocument(filePath);
};
const openSettings = () => {
window.electronAPI.showSettings();
};
const logout = async () => {
try {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('userInfo');
await window.electronAPI.logout();
} catch (error) {
console.error('Logout error:', error);
router.push('/login');
}
};
const getShortPath = (fullPath: string) => {
if (!fullPath) return '';
const parts = fullPath.split('\\');
return parts.length > 2 ? `...\\${parts.slice(-2).join('\\')}` : fullPath;
};
const onParagraphInput = () => {
hasUnsavedChanges.value = true;
if (autoSync.value) {
clearTimeout(editTimer.value);
editTimer.value = setTimeout(updateParagraphWithRevisions, 1000);
}
};
onMounted(() => {
setupEventListeners();
refreshParagraphContent();
const interval = setInterval(() => {
if (isConnected.value) {
refreshParagraphContent();
}
}, 3000);
onUnmounted(() => {
clearInterval(interval);
if (editTimer.value) clearTimeout(editTimer.value);
window.electronAPI.removeAllListeners('wps-status-changed');
window.electronAPI.removeAllListeners('documents-list-changed');
window.electronAPI.removeAllListeners('active-document-changed');
window.electronAPI.removeAllListeners('full-paragraph-content-changed');
});
});
</script>
<style scoped>
.main-window {
width: 100%;
height: 100vh;
background: #f8f9fa;
display: flex;
flex-direction: column;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.title-bar {
height: 32px;
background: #2c3e50;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
-webkit-app-region: drag;
}
.title {
font-size: 14px;
font-weight: 500;
}
.window-controls {
display: flex;
-webkit-app-region: no-drag;
}
.control-btn {
width: 32px;
height: 20px;
border: none;
background: transparent;
color: white;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.control-btn.close:hover {
background: #e74c3c;
}
.control-btn.minimize:hover {
background: rgba(255, 255, 255, 0.1);
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 280px;
background: white;
border-right: 1px solid #e1e5e9;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h3 {
margin: 0;
color: #2c3e50;
font-size: 14px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 4px;
}
.action-btn {
background: none;
border: none;
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: background-color 0.2s;
}
.action-btn:hover {
background: #f8f9fa;
}
.document-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.doc-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 4px;
position: relative;
}
.doc-item:hover {
background: #f8f9fa;
}
.doc-item.active {
background: #e3f2fd;
border: 1px solid #2196f3;
}
.doc-icon {
font-size: 16px;
margin-right: 12px;
opacity: 0.8;
}
.doc-info {
flex: 1;
min-width: 0;
}
.doc-name {
font-weight: 500;
color: #2c3e50;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-path {
font-size: 11px;
color: #7f8c8d;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.active-indicator {
color: #2196f3;
font-size: 12px;
margin-left: 8px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #7f8c8d;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state p {
margin: 0 0 16px 0;
font-size: 14px;
}
.open-file-btn {
background: #3498db;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.wps-status {
padding: 12px 16px;
border-top: 1px solid #e1e5e9;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 500;
}
.wps-status.connected {
background: rgba(39, 174, 96, 0.1);
color: #27ae60;
}
.wps-status:not(.connected) {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
.status-icon {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.wps-status.connected .status-icon {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.status-bar {
padding: 8px 16px;
background: #e74c3c;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.status-bar.connected {
background: #27ae60;
}
.status-info {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-details {
display: flex;
gap: 16px;
font-size: 12px;
opacity: 0.9;
}
.active-doc {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.revision-count {
padding: 2px 6px;
border-radius: 10px;
background: rgba(243, 156, 18, 0.2);
color: #f39c12;
font-size: 11px;
font-weight: 500;
}
.editor-area {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
}
.navigation {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 0 8px;
}
.navigation button {
background: #3498db;
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.navigation button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.navigation-info {
color: #7f8c8d;
font-size: 14px;
font-weight: 500;
}
.has-content {
color: #27ae60;
}
.no-content {
color: #e67e22;
}
.disconnected {
color: #e74c3c;
}
.revisions-panel {
background: #f8f9fa;
border: 1px solid #e1e5e9;
border-radius: 6px;
margin-bottom: 16px;
padding: 16px;
}
.revisions-panel h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: #2c3e50;
}
.revision-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
}
.revision-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.revision-type {
background: #f39c12;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.revision-author {
font-size: 11px;
color: #6c757d;
}
.revision-content {
font-size: 12px;
line-height: 1.4;
margin-bottom: 8px;
}
.revision-actions {
display: flex;
gap: 8px;
}
.revision-actions .action-btn {
padding: 4px 8px;
border: none;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
}
.revision-actions .accept {
background: #28a745;
color: white;
}
.revision-actions .reject {
background: #dc3545;
color: white;
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
background: white;
border: 1px solid #e1e5e9;
border-radius: 8px;
overflow: hidden;
}
.editor-header {
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-header h4 {
margin: 0;
color: #2c3e50;
font-size: 14px;
font-weight: 600;
}
.save-btn, .refresh-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-left: 8px;
}
.save-btn {
background: #27ae60;
color: white;
}
.save-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.refresh-btn {
background: #3498db;
color: white;
}
.paragraph-editor {
flex: 1;
padding: 16px;
border: none;
resize: none;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
outline: none;
}
.paragraph-editor:disabled {
background: #f8f9fa;
color: #7f8c8d;
cursor: not-allowed;
}
.paragraph-editor.has-unsaved {
border-left: 3px solid #ff9800;
}
.editor-footer {
padding: 12px 16px;
background: #f8f9fa;
border-top: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #7f8c8d;
}
.auto-sync-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.editor-stats {
display: flex;
align-items: center;
gap: 12px;
}
.unsaved-badge {
padding: 2px 6px;
border-radius: 10px;
background: #ff9800;
color: white;
font-size: 10px;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
min-width: 120px;
}
.btn.primary {
background: #3498db;
color: white;
}
.btn.primary:hover:not(:disabled) {
background: #2980b9;
}
.btn.secondary {
background: #95a5a6;
color: white;
}
.btn.secondary:hover:not(:disabled) {
background: #7f8c8d;
}
.btn.danger {
background: #e74c3c;
color: white;
}
.btn.danger:hover:not(:disabled) {
background: #c0392b;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.connection-prompt {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.prompt-content {
text-align: center;
padding: 40px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: 1px solid #e1e5e9;
max-width: 400px;
}
.prompt-icon {
font-size: 48px;
margin-bottom: 16px;
}
.prompt-content h3 {
margin: 0 0 8px 0;
color: #2c3e50;
}
.prompt-content p {
margin: 0 0 20px 0;
color: #7f8c8d;
}
</style>

270
src/views/SettingsWindow.vue 普通文件
查看文件

@ -0,0 +1,270 @@
<template>
<div class="settings-window">
<div class="settings-header">
<h2>应用设置</h2>
<button class="close-btn" @click="closeWindow">×</button>
</div>
<div class="settings-content">
<div class="setting-group">
<h3>基本设置</h3>
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" v-model="localConfig.autoConnectWPS" @change="updateConfig">
<span class="checkmark"></span>
启动时自动连接WPS
</label>
</div>
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" v-model="localConfig.showFloatingWindow" @change="updateConfig">
<span class="checkmark"></span>
显示悬浮窗
</label>
</div>
</div>
<div class="setting-group">
<h3>WPS 设置</h3>
<div class="wps-status">
<h4>连接状态</h4>
<div class="status-info">
<div class="status-indicator" :class="{ connected: wpsConnected }"></div>
<span class="status-text">{{ wpsStatusText }}</span>
</div>
<button @click="testConnection" class="btn primary">测试连接</button>
</div>
</div>
</div>
<div class="settings-footer">
<button class="btn secondary" @click="closeWindow">关闭</button>
<button class="btn primary" @click="saveSettings">保存</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
const localConfig = reactive({
autoConnectWPS: true,
showFloatingWindow: true
});
const wpsConnected = ref(false);
const wpsStatusText = ref('未知');
const loadConfig = async () => {
try {
const config = await window.electronAPI.getAppConfig();
Object.assign(localConfig, config);
const status = await window.electronAPI.getWPSStatus();
wpsConnected.value = status.connected;
wpsStatusText.value = status.connected ? '已连接' : '未连接';
} catch (error) {
console.error('Failed to load config:', error);
}
};
const updateConfig = () => {
window.electronAPI.setAppConfig({ ...localConfig });
};
const saveSettings = () => {
window.electronAPI.setAppConfig({ ...localConfig });
closeWindow();
};
const closeWindow = () => {
window.electronAPI.closeWindow();
};
const testConnection = async () => {
try {
const status = await window.electronAPI.getWPSStatus();
wpsConnected.value = status.connected;
wpsStatusText.value = status.connected ? '连接成功' : '连接失败';
alert(status.connected ? '✅ WPS 连接正常' : '❌ WPS 连接失败');
} catch (error) {
alert('测试连接失败');
}
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.settings-window {
width: 100%;
height: 100vh;
background: #f8f9fa;
display: flex;
flex-direction: column;
}
.settings-header {
background: white;
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.settings-header h2 {
margin: 0;
color: #2c3e50;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6c757d;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: #e9ecef;
}
.settings-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.setting-group {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
}
.setting-group h3 {
margin: 0 0 20px 0;
color: #2c3e50;
border-bottom: 1px solid #e9ecef;
padding-bottom: 12px;
}
.setting-item {
margin-bottom: 16px;
}
.setting-label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
color: #495057;
}
.setting-label input[type="checkbox"] {
display: none;
}
.checkmark {
width: 18px;
height: 18px;
border: 2px solid #ced4da;
border-radius: 3px;
margin-right: 10px;
position: relative;
}
.setting-label input[type="checkbox"]:checked + .checkmark {
background: #2196f3;
border-color: #2196f3;
}
.setting-label input[type="checkbox"]:checked + .checkmark::after {
content: '';
position: absolute;
left: 4px;
top: 1px;
width: 6px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.wps-status {
background: #f8f9fa;
padding: 16px;
border-radius: 6px;
}
.wps-status h4 {
margin: 0 0 12px 0;
color: #2c3e50;
}
.status-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #e74c3c;
}
.status-indicator.connected {
background: #27ae60;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.settings-footer {
background: white;
padding: 20px;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
min-width: 80px;
}
.btn.primary {
background: #2196f3;
color: white;
}
.btn.secondary {
background: #6c757d;
color: white;
}
</style>

12
tsconfig.json 普通文件
查看文件

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}