From 66623aa6632456220261118a10bf3744283f8fdf Mon Sep 17 00:00:00 2001 From: xuqm Date: Fri, 24 Oct 2025 17:31:14 +0800 Subject: [PATCH] init --- .gitignore | 11 + .idea/.gitignore | 8 + electron/logger.js | 51 ++ electron/main.js | 273 +++++++++++ electron/preload.js | 35 ++ electron/winaxHelper.js | 572 +++++++++++++++++++++++ index.html | 12 + package.json | 58 +++ src/App.vue | 25 + src/main.ts | 26 ++ src/stores/wpsStore.ts | 97 ++++ src/types/electron.d.ts | 29 ++ src/views/LoginWindow.vue | 227 +++++++++ src/views/MainWindow.vue | 882 +++++++++++++++++++++++++++++++++++ src/views/SettingsWindow.vue | 270 +++++++++++ tsconfig.json | 12 + 16 files changed, 2588 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 electron/logger.js create mode 100644 electron/main.js create mode 100644 electron/preload.js create mode 100644 electron/winaxHelper.js create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.vue create mode 100644 src/main.ts create mode 100644 src/stores/wpsStore.ts create mode 100644 src/types/electron.d.ts create mode 100644 src/views/LoginWindow.vue create mode 100644 src/views/MainWindow.vue create mode 100644 src/views/SettingsWindow.vue create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32b877b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/tmp +/out-tsc + +/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/.pnp +.pnp.js + +.vscode/* \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.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 diff --git a/electron/logger.js b/electron/logger.js new file mode 100644 index 0000000..fd69ad7 --- /dev/null +++ b/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; diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..c1576cb --- /dev/null +++ b/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(); + } + }); +} diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 0000000..cdd5a25 --- /dev/null +++ b/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) +}); diff --git a/electron/winaxHelper.js b/electron/winaxHelper.js new file mode 100644 index 0000000..e96e31d --- /dev/null +++ b/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 }; diff --git a/index.html b/index.html new file mode 100644 index 0000000..b5dc4b2 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + AI Electron Client + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..8d52685 --- /dev/null +++ b/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 + } + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..0a3f0ea --- /dev/null +++ b/src/App.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a3bcec5 --- /dev/null +++ b/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') diff --git a/src/stores/wpsStore.ts b/src/stores/wpsStore.ts new file mode 100644 index 0000000..f3149ce --- /dev/null +++ b/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(null); + const documents = ref([]); + const paragraphContent = ref({ + 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 => { + 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 + }; +}); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts new file mode 100644 index 0000000..545d4e9 --- /dev/null +++ b/src/types/electron.d.ts @@ -0,0 +1,29 @@ +interface ElectronAPI { + minimizeWindow: () => Promise; + closeWindow: () => Promise; + login: (credentials: any) => Promise; + logout: () => Promise; + showSettings: () => Promise; + openFile: (filePath?: string) => Promise; + getWPSStatus: () => Promise; + navigateParagraph: (direction: string) => Promise; + getFullParagraphContent: () => Promise; + updateParagraphWithRevisions: (content: string) => Promise; + handleRevision: (action: string, revisionIndex: number | null) => Promise; + addComment: (commentText: string) => Promise; + setTrackRevisions: (track: boolean) => Promise; + switchDocument: (filePath: string) => Promise; + 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 {}; diff --git a/src/views/LoginWindow.vue b/src/views/LoginWindow.vue new file mode 100644 index 0000000..48540c2 --- /dev/null +++ b/src/views/LoginWindow.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/MainWindow.vue b/src/views/MainWindow.vue new file mode 100644 index 0000000..30c6323 --- /dev/null +++ b/src/views/MainWindow.vue @@ -0,0 +1,882 @@ + + + + + diff --git a/src/views/SettingsWindow.vue b/src/views/SettingsWindow.vue new file mode 100644 index 0000000..5e9f7af --- /dev/null +++ b/src/views/SettingsWindow.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1efdceb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +}