xuqm 1 день тому
коміт
66623aa663

+ 11 - 0
.gitignore

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

+ 8 - 0
.idea/.gitignore

@@ -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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
src/types/electron.d.ts

@@ -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 - 0
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 - 0
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 - 0
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 - 0
tsconfig.json

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