init
这个提交包含在:
当前提交
66623aa663
11
.gitignore
vendored
普通文件
11
.gitignore
vendored
普通文件
@ -0,0 +1,11 @@
|
|||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
8
.idea/.gitignore
自动生成的
vendored
普通文件
8
.idea/.gitignore
自动生成的
vendored
普通文件
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
51
electron/logger.js
普通文件
51
electron/logger.js
普通文件
@ -0,0 +1,51 @@
|
|||||||
|
const log = require('electron-log');
|
||||||
|
const { app } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor() {
|
||||||
|
this.initializeLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeLogger() {
|
||||||
|
log.transports.file.resolvePath = () => {
|
||||||
|
const logsPath = path.join(app.getPath('userData'), 'logs');
|
||||||
|
if (!fs.existsSync(logsPath)) {
|
||||||
|
fs.mkdirSync(logsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
return path.join(logsPath, 'app.log');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
log.transports.console.level = 'info';
|
||||||
|
} else {
|
||||||
|
log.transports.console.level = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.transports.file.level = 'info';
|
||||||
|
log.transports.file.maxSize = 10 * 1024 * 1024;
|
||||||
|
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
|
||||||
|
|
||||||
|
this.logger = log;
|
||||||
|
this.info('Logger initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, ...args) {
|
||||||
|
this.logger.info(message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message, ...args) {
|
||||||
|
this.logger.warn(message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, ...args) {
|
||||||
|
this.logger.error(message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message, ...args) {
|
||||||
|
this.logger.debug(message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
||||||
273
electron/main.js
普通文件
273
electron/main.js
普通文件
@ -0,0 +1,273 @@
|
|||||||
|
const { app, BrowserWindow, Tray, Menu, ipcMain, dialog, shell } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const { wpsHelper } = require('./winaxHelper.js');
|
||||||
|
const Logger = require('./logger.js');
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
let loginWindow;
|
||||||
|
let settingsWindow;
|
||||||
|
let tray;
|
||||||
|
|
||||||
|
let appConfig = {
|
||||||
|
showFloatingWindow: true,
|
||||||
|
autoConnectWPS: true,
|
||||||
|
logLevel: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
let isLoggedIn = false;
|
||||||
|
|
||||||
|
function createMainWindow() {
|
||||||
|
const { width, height } = require('electron').screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1000,
|
||||||
|
height: 700,
|
||||||
|
x: width - 1100,
|
||||||
|
y: 50,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
},
|
||||||
|
title: 'AI Electron Client'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.loadURL('http://localhost:5173/#/main');
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile('dist/index.html', { hash: 'main' });
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
if (!app.isQuitting) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Main window created');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLoginWindow() {
|
||||||
|
loginWindow = new BrowserWindow({
|
||||||
|
width: 350,
|
||||||
|
height: 450,
|
||||||
|
show: false,
|
||||||
|
modal: true,
|
||||||
|
frame: false,
|
||||||
|
resizable: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
loginWindow.loadURL('http://localhost:5173/#/login');
|
||||||
|
} else {
|
||||||
|
loginWindow.loadFile('dist/index.html', { hash: 'login' });
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWindow.on('closed', () => {
|
||||||
|
loginWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
const iconPath = path.join(__dirname, '../assets/tray.png');
|
||||||
|
tray = new Tray(iconPath || require('electron').nativeImage.createEmpty());
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '打开主界面',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
app.isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
tray.setToolTip('AI Electron Client');
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC 处理
|
||||||
|
ipcMain.handle('login', async (event, credentials) => {
|
||||||
|
try {
|
||||||
|
logger.info('Login attempt for user:', credentials.username);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
if (credentials.username && credentials.password) {
|
||||||
|
isLoggedIn = true;
|
||||||
|
wpsHelper.connectToWPS();
|
||||||
|
|
||||||
|
if (loginWindow) loginWindow.hide();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, user: { name: credentials.username } };
|
||||||
|
} else {
|
||||||
|
return { success: false, message: '用户名和密码不能为空' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Login error:', error);
|
||||||
|
return { success: false, message: '登录失败' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('logout', () => {
|
||||||
|
isLoggedIn = false;
|
||||||
|
if (mainWindow) mainWindow.hide();
|
||||||
|
wpsHelper.cleanup();
|
||||||
|
showLoginWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-minimize', () => {
|
||||||
|
if (mainWindow) mainWindow.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-close', () => {
|
||||||
|
if (mainWindow) mainWindow.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('show-settings', () => {
|
||||||
|
if (!settingsWindow) {
|
||||||
|
settingsWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 500,
|
||||||
|
modal: true,
|
||||||
|
parent: mainWindow,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
settingsWindow.loadURL('http://localhost:5173/#/settings');
|
||||||
|
} else {
|
||||||
|
settingsWindow.loadFile('dist/index.html', { hash: 'settings' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settingsWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('open-file', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
if (!filePath) {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [
|
||||||
|
{ name: 'Word Documents', extensions: ['doc', 'docx'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled) return { success: false };
|
||||||
|
filePath = result.filePaths[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = wpsHelper.openDocument(filePath);
|
||||||
|
return { success, message: success ? '文件已打开' : '打开失败' };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error opening file:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-wps-status', () => {
|
||||||
|
return wpsHelper.getStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('navigate-paragraph', (event, direction) => {
|
||||||
|
return wpsHelper.navigateParagraph(direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-full-paragraph-content', () => {
|
||||||
|
return wpsHelper.getFullParagraphContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('update-paragraph-with-revisions', (event, content) => {
|
||||||
|
return wpsHelper.updateParagraphWithRevisions(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('handle-revision', (event, action, revisionIndex) => {
|
||||||
|
return wpsHelper.handleRevision(action, revisionIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('add-comment', (event, commentText) => {
|
||||||
|
return wpsHelper.addComment(commentText);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('set-track-revisions', (event, track) => {
|
||||||
|
return wpsHelper.setTrackRevisions(track);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('switch-document', (event, filePath) => {
|
||||||
|
return wpsHelper.switchToDocument(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
function showLoginWindow() {
|
||||||
|
if (!loginWindow) {
|
||||||
|
createLoginWindow();
|
||||||
|
}
|
||||||
|
loginWindow.show();
|
||||||
|
loginWindow.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
logger.info('App is ready');
|
||||||
|
|
||||||
|
createLoginWindow();
|
||||||
|
createMainWindow();
|
||||||
|
createTray();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loginWindow) {
|
||||||
|
loginWindow.show();
|
||||||
|
loginWindow.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
app.isQuitting = true;
|
||||||
|
wpsHelper.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
35
electron/preload.js
普通文件
35
electron/preload.js
普通文件
@ -0,0 +1,35 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// 窗口操作
|
||||||
|
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||||
|
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||||
|
|
||||||
|
// 登录相关
|
||||||
|
login: (credentials) => ipcRenderer.invoke('login', credentials),
|
||||||
|
logout: () => ipcRenderer.invoke('logout'),
|
||||||
|
|
||||||
|
// 窗口管理
|
||||||
|
showSettings: () => ipcRenderer.invoke('show-settings'),
|
||||||
|
|
||||||
|
// 文件操作
|
||||||
|
openFile: (filePath) => ipcRenderer.invoke('open-file', filePath),
|
||||||
|
|
||||||
|
// WPS操作
|
||||||
|
getWPSStatus: () => ipcRenderer.invoke('get-wps-status'),
|
||||||
|
navigateParagraph: (direction) => ipcRenderer.invoke('navigate-paragraph', direction),
|
||||||
|
getFullParagraphContent: () => ipcRenderer.invoke('get-full-paragraph-content'),
|
||||||
|
updateParagraphWithRevisions: (content) => ipcRenderer.invoke('update-paragraph-with-revisions', content),
|
||||||
|
handleRevision: (action, revisionIndex) => ipcRenderer.invoke('handle-revision', action, revisionIndex),
|
||||||
|
addComment: (commentText) => ipcRenderer.invoke('add-comment', commentText),
|
||||||
|
setTrackRevisions: (track) => ipcRenderer.invoke('set-track-revisions', track),
|
||||||
|
switchDocument: (filePath) => ipcRenderer.invoke('switch-document', filePath),
|
||||||
|
|
||||||
|
// 事件监听
|
||||||
|
onWPSStatusChange: (callback) => ipcRenderer.on('wps-status-changed', callback),
|
||||||
|
onDocumentsListChange: (callback) => ipcRenderer.on('documents-list-changed', callback),
|
||||||
|
onActiveDocumentChange: (callback) => ipcRenderer.on('active-document-changed', callback),
|
||||||
|
onFullParagraphContentChange: (callback) => ipcRenderer.on('full-paragraph-content-changed', callback),
|
||||||
|
|
||||||
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
|
||||||
|
});
|
||||||
572
electron/winaxHelper.js
普通文件
572
electron/winaxHelper.js
普通文件
@ -0,0 +1,572 @@
|
|||||||
|
const { ipcMain, BrowserWindow } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let winax;
|
||||||
|
try {
|
||||||
|
winax = require('winax');
|
||||||
|
console.log('✅ Winax loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('❌ Winax not available:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
class WPSHelper {
|
||||||
|
constructor() {
|
||||||
|
this.wpsApp = null;
|
||||||
|
this.activeDocument = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.documents = new Map();
|
||||||
|
this.checkInterval = null;
|
||||||
|
this.lastParagraphContent = '';
|
||||||
|
|
||||||
|
this.startMonitoring();
|
||||||
|
console.log('🔄 WPSHelper initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToWPS() {
|
||||||
|
if (!winax) {
|
||||||
|
console.warn('⚠️ Winax not available, using fallback mode');
|
||||||
|
this.setupFallbackMode();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.wpsApp = new winax.Object('KWPS.Application');
|
||||||
|
this.wpsApp.Visible = true;
|
||||||
|
this.isConnected = true;
|
||||||
|
|
||||||
|
console.log('✅ Connected to WPS');
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.startMonitoring();
|
||||||
|
|
||||||
|
this.sendWPSStatus();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to connect to WPS:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
this.setupFallbackMode();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
this.lastActiveDocument = null;
|
||||||
|
this.lastDocumentCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
startMonitoring() {
|
||||||
|
if (this.checkInterval) {
|
||||||
|
clearInterval(this.checkInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkInterval = setInterval(() => {
|
||||||
|
this.checkWPSStatus();
|
||||||
|
this.updateDocumentsList();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWPSStatus() {
|
||||||
|
try {
|
||||||
|
if (this.wpsApp && this.isConnected) {
|
||||||
|
try {
|
||||||
|
const name = this.wpsApp.Name;
|
||||||
|
this.isConnected = true;
|
||||||
|
} catch (error) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.wpsApp = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.wpsApp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDocumentsList() {
|
||||||
|
if (!this.isConnected || !this.wpsApp) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.simulateDocumentsList();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newDocs = new Map();
|
||||||
|
let documents;
|
||||||
|
|
||||||
|
try {
|
||||||
|
documents = this.wpsApp.Documents;
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= documents.Count; i++) {
|
||||||
|
try {
|
||||||
|
const doc = documents.Item(i);
|
||||||
|
const fullName = doc.FullName;
|
||||||
|
const name = doc.Name;
|
||||||
|
|
||||||
|
newDocs.set(fullName, {
|
||||||
|
name: name,
|
||||||
|
path: fullName,
|
||||||
|
document: doc,
|
||||||
|
isActive: doc === this.activeDocument
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error accessing document:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.documents = newDocs;
|
||||||
|
this.sendDocumentsList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating documents list:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullParagraphContent() {
|
||||||
|
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
||||||
|
return this.getFallbackParagraphContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selection = this.wpsApp.Selection;
|
||||||
|
if (!selection) {
|
||||||
|
return this.getFallbackParagraphContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalStart = selection.Start;
|
||||||
|
const originalEnd = selection.End;
|
||||||
|
|
||||||
|
try {
|
||||||
|
selection.Expand(5); // wdParagraph
|
||||||
|
|
||||||
|
const text = selection.Text;
|
||||||
|
const revisions = this.getRevisionsInSelection(selection);
|
||||||
|
const comments = this.getCommentsInSelection(selection);
|
||||||
|
|
||||||
|
selection.SetRange(originalStart, originalEnd);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: text.trim(),
|
||||||
|
html: text.trim(),
|
||||||
|
revisions: revisions,
|
||||||
|
comments: comments,
|
||||||
|
hasRevisions: revisions.length > 0,
|
||||||
|
hasComments: comments.length > 0
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
selection.SetRange(originalStart, originalEnd);
|
||||||
|
return this.getFallbackParagraphContent();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return this.getFallbackParagraphContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRevisionsInSelection(selection) {
|
||||||
|
const revisions = [];
|
||||||
|
if (!selection || !selection.Revisions) return revisions;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const revisionsCount = selection.Revisions.Count;
|
||||||
|
for (let i = 1; i <= revisionsCount; i++) {
|
||||||
|
try {
|
||||||
|
const revision = selection.Revisions.Item(i);
|
||||||
|
revisions.push({
|
||||||
|
type: 'insert',
|
||||||
|
author: revision.Author || '未知作者',
|
||||||
|
date: revision.Date || new Date(),
|
||||||
|
text: revision.Range.Text || '',
|
||||||
|
index: i
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error getting revision:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not access revisions:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return revisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommentsInSelection(selection) {
|
||||||
|
const comments = [];
|
||||||
|
if (!selection || !selection.Comments) return comments;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commentsCount = selection.Comments.Count;
|
||||||
|
for (let i = 1; i <= commentsCount; i++) {
|
||||||
|
try {
|
||||||
|
const comment = selection.Comments.Item(i);
|
||||||
|
comments.push({
|
||||||
|
author: comment.Author || '未知作者',
|
||||||
|
date: comment.Date || new Date(),
|
||||||
|
text: comment.Range.Text || '',
|
||||||
|
index: i
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error getting comment:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not access comments:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFallbackParagraphContent() {
|
||||||
|
return {
|
||||||
|
text: this.lastParagraphContent || '',
|
||||||
|
html: this.lastParagraphContent || '',
|
||||||
|
revisions: [],
|
||||||
|
comments: [],
|
||||||
|
hasRevisions: false,
|
||||||
|
hasComments: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParagraphWithRevisions(newContent) {
|
||||||
|
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
||||||
|
return { success: false, error: 'WPS not connected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selection = this.wpsApp.Selection;
|
||||||
|
if (!selection) {
|
||||||
|
return { success: false, error: 'No selection available' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalStart = selection.Start;
|
||||||
|
const originalEnd = selection.End;
|
||||||
|
|
||||||
|
try {
|
||||||
|
selection.Expand(5); // wdParagraph
|
||||||
|
const originalText = selection.Text.trim();
|
||||||
|
|
||||||
|
if (originalText === newContent.trim()) {
|
||||||
|
return { success: true, unchanged: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.Text = newContent;
|
||||||
|
this.lastParagraphContent = newContent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
originalContent: originalText,
|
||||||
|
newContent: newContent,
|
||||||
|
hasChanges: true
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
selection.SetRange(originalStart, originalStart);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateParagraph(direction) {
|
||||||
|
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selection = this.wpsApp.Selection;
|
||||||
|
if (!selection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 'prev') {
|
||||||
|
selection.MoveUp(5, 1); // wdParagraph
|
||||||
|
} else if (direction === 'next') {
|
||||||
|
selection.MoveDown(5, 1); // wdParagraph
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error navigating paragraphs:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openDocument(filePath) {
|
||||||
|
if (!this.isConnected || !this.wpsApp) {
|
||||||
|
console.log('WPS not connected, using fallback');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error('File does not exist: ' + filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = this.wpsApp.Documents.Open(filePath);
|
||||||
|
this.activeDocument = doc;
|
||||||
|
|
||||||
|
this.documents.set(filePath, {
|
||||||
|
name: doc.Name,
|
||||||
|
path: filePath,
|
||||||
|
document: doc,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sendDocumentsList();
|
||||||
|
this.sendActiveDocumentChange();
|
||||||
|
|
||||||
|
console.log('Document opened successfully:', path.basename(filePath));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening document:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToDocument(filePath) {
|
||||||
|
if (!this.isConnected || !this.wpsApp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docInfo = this.documents.get(filePath);
|
||||||
|
if (docInfo && docInfo.document) {
|
||||||
|
try {
|
||||||
|
docInfo.document.Activate();
|
||||||
|
this.activeDocument = docInfo.document;
|
||||||
|
this.sendActiveDocumentChange();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error switching document:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRevision(action, revisionIndex) {
|
||||||
|
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
||||||
|
return { success: false, error: 'WPS not connected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (revisionIndex !== null) {
|
||||||
|
const revision = this.activeDocument.Revisions.Item(revisionIndex);
|
||||||
|
if (action === 'accept') {
|
||||||
|
revision.Accept();
|
||||||
|
} else {
|
||||||
|
revision.Reject();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const revisions = this.activeDocument.Revisions;
|
||||||
|
if (action === 'accept') {
|
||||||
|
revisions.AcceptAll();
|
||||||
|
} else {
|
||||||
|
revisions.RejectAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addComment(commentText) {
|
||||||
|
if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
||||||
|
return { success: false, error: 'WPS not connected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selection = this.wpsApp.Selection;
|
||||||
|
if (!selection) {
|
||||||
|
return { success: false, error: 'No selection available' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = selection.Comments.Add(selection.Range);
|
||||||
|
comment.Range.Text = commentText;
|
||||||
|
|
||||||
|
return { success: true, comment: commentText };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrackRevisions(track) {
|
||||||
|
if (!this.isConnected || !this.wpsApp) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.wpsApp.ActiveDocument.TrackRevisions = track;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not set track revisions:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFallbackMode() {
|
||||||
|
console.log('Setting up fallback mode');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.startMonitoring();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.simulateDocumentsList();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
simulateDocumentsList() {
|
||||||
|
const mockDocuments = [
|
||||||
|
{
|
||||||
|
name: '示例文档.docx',
|
||||||
|
path: 'C:\\Documents\\示例文档.docx',
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.sendDocumentsList(mockDocuments);
|
||||||
|
|
||||||
|
const sampleParagraph = `这是一个示例段落内容。在回退模式下,您可以查看界面功能,但实际的WPS文档操作需要安装WPS并确保Winax正常工作。
|
||||||
|
|
||||||
|
当前功能包括:
|
||||||
|
• 文档列表显示
|
||||||
|
• 段落内容显示
|
||||||
|
• 基本的导航操作
|
||||||
|
• 文件拖拽处理
|
||||||
|
|
||||||
|
要启用完整的WPS集成功能,请确保:
|
||||||
|
1. 已安装WPS Office
|
||||||
|
2. Winax模块正确安装
|
||||||
|
3. 应用程序有足够的权限访问COM组件`;
|
||||||
|
|
||||||
|
this.sendFullParagraphContent({
|
||||||
|
text: sampleParagraph,
|
||||||
|
html: sampleParagraph,
|
||||||
|
revisions: [],
|
||||||
|
comments: [],
|
||||||
|
hasRevisions: false,
|
||||||
|
hasComments: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWPSStatus() {
|
||||||
|
if (ipcMain) {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
windows.forEach(window => {
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
window.webContents.send('wps-status-changed', {
|
||||||
|
connected: this.isConnected,
|
||||||
|
hasActiveDocument: !!this.activeDocument,
|
||||||
|
documentCount: this.documents.size,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error sending WPS status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDocumentsList(documents = null) {
|
||||||
|
if (ipcMain) {
|
||||||
|
const docs = documents || Array.from(this.documents.values()).map(doc => ({
|
||||||
|
name: doc.name,
|
||||||
|
path: doc.path,
|
||||||
|
isActive: doc.document === this.activeDocument
|
||||||
|
}));
|
||||||
|
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
windows.forEach(window => {
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
window.webContents.send('documents-list-changed', docs);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error sending documents list:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendActiveDocumentChange() {
|
||||||
|
if (ipcMain && this.activeDocument) {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
windows.forEach(window => {
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
window.webContents.send('active-document-changed', {
|
||||||
|
name: this.activeDocument.Name,
|
||||||
|
path: this.activeDocument.FullName
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error sending active document change:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFullParagraphContent(content) {
|
||||||
|
if (ipcMain) {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
windows.forEach(window => {
|
||||||
|
if (window && !window.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
window.webContents.send('full-paragraph-content-changed', content);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error sending full paragraph content:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
if (!this.isConnected) this.connectToWPS();
|
||||||
|
return {
|
||||||
|
connected: this.isConnected,
|
||||||
|
activeDocument: this.activeDocument ? {
|
||||||
|
name: this.activeDocument.Name,
|
||||||
|
path: this.activeDocument.FullName
|
||||||
|
} : null,
|
||||||
|
documents: Array.from(this.documents.values()).map(doc => ({
|
||||||
|
name: doc.name,
|
||||||
|
path: doc.path,
|
||||||
|
isActive: doc.document === this.activeDocument
|
||||||
|
})),
|
||||||
|
currentParagraph: this.lastParagraphContent,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
console.log('Cleaning up WPSHelper resources...');
|
||||||
|
|
||||||
|
if (this.checkInterval) {
|
||||||
|
clearInterval(this.checkInterval);
|
||||||
|
this.checkInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.wpsApp) {
|
||||||
|
try {
|
||||||
|
console.log('WPS application reference cleaned');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error cleaning WPS application:', error);
|
||||||
|
}
|
||||||
|
this.wpsApp = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeDocument = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.documents.clear();
|
||||||
|
|
||||||
|
console.log('WPSHelper cleanup completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wpsHelper = new WPSHelper();
|
||||||
|
module.exports = { wpsHelper };
|
||||||
12
index.html
普通文件
12
index.html
普通文件
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI Electron Client</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
package.json
普通文件
58
package.json
普通文件
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-electron-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "electron/main.js",
|
||||||
|
"description": "AI Electron Client",
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"yarn serve\" \"yarn electron-dev\"",
|
||||||
|
"serve": "vite",
|
||||||
|
"electron-dev": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"dist": "yarn build && electron-builder",
|
||||||
|
"postinstall": "electron-builder install-app-deps"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "3.5.22",
|
||||||
|
"vue-router": "4.5.0",
|
||||||
|
"pinia": "2.1.7",
|
||||||
|
"winax": "3.6.2",
|
||||||
|
"electron-updater": "6.3.0-alpha.6",
|
||||||
|
"electron-log": "5.1.2",
|
||||||
|
"chokidar": "4.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "24.6.0",
|
||||||
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
|
"concurrently": "9.2.1",
|
||||||
|
"cross-env": "10.1.0",
|
||||||
|
"electron": "38.2.2",
|
||||||
|
"electron-builder": "26.1.0",
|
||||||
|
"rimraf": "6.0.1",
|
||||||
|
"typescript": "5.9.3",
|
||||||
|
"vite": "npm:rolldown-vite@7.1.14",
|
||||||
|
"vue-tsc": "3.1.0",
|
||||||
|
"wait-on": "9.0.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.example.ai-electron",
|
||||||
|
"productName": "AI Electron Client",
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/App.vue
普通文件
25
src/App.vue
普通文件
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
src/main.ts
普通文件
26
src/main.ts
普通文件
@ -0,0 +1,26 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import App from './App.vue'
|
||||||
|
import LoginWindow from './views/LoginWindow.vue'
|
||||||
|
import MainWindow from './views/MainWindow.vue'
|
||||||
|
import SettingsWindow from './views/SettingsWindow.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/login', component: LoginWindow },
|
||||||
|
{ path: '/main', component: MainWindow },
|
||||||
|
{ path: '/settings', component: SettingsWindow },
|
||||||
|
{ path: '/', redirect: '/login' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
97
src/stores/wpsStore.ts
普通文件
97
src/stores/wpsStore.ts
普通文件
@ -0,0 +1,97 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
export const useWPSStore = defineStore('wps', () => {
|
||||||
|
const connectionStatus = ref<'connected' | 'disconnected'>('disconnected');
|
||||||
|
const activeDocument = ref<any>(null);
|
||||||
|
const documents = ref<any[]>([]);
|
||||||
|
const paragraphContent = ref<any>({
|
||||||
|
text: '',
|
||||||
|
html: '',
|
||||||
|
revisions: [],
|
||||||
|
comments: [],
|
||||||
|
hasRevisions: false,
|
||||||
|
hasComments: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUnsavedChanges = ref(false);
|
||||||
|
const autoSync = ref(true);
|
||||||
|
|
||||||
|
const isConnected = computed(() => connectionStatus.value === 'connected');
|
||||||
|
const hasRevisions = computed(() => paragraphContent.value.revisions.length > 0);
|
||||||
|
const hasComments = computed(() => paragraphContent.value.comments.length > 0);
|
||||||
|
const canEdit = computed(() => isConnected.value && !!activeDocument.value);
|
||||||
|
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
window.electronAPI.onWPSStatusChange((_event, status) => {
|
||||||
|
connectionStatus.value = status.connected ? 'connected' : 'disconnected';
|
||||||
|
documents.value = status.documents || [];
|
||||||
|
activeDocument.value = status.activeDocument || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onDocumentsListChange((_event, docs) => {
|
||||||
|
documents.value = docs;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onActiveDocumentChange((_event, doc) => {
|
||||||
|
activeDocument.value = doc;
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onFullParagraphContentChange((_event, content) => {
|
||||||
|
paragraphContent.value = content;
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshParagraph = async (): Promise<boolean> => {
|
||||||
|
if (!isConnected.value) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await window.electronAPI.getFullParagraphContent();
|
||||||
|
paragraphContent.value = content;
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh paragraph:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateParagraph = async (content: string) => {
|
||||||
|
if (!isConnected.value) return { success: false, error: 'WPS not connected' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.updateParagraphWithRevisions(content);
|
||||||
|
if (result.success) {
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
await refreshParagraph();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateParagraph = async (direction: 'prev' | 'next') => {
|
||||||
|
if (!isConnected.value) return false;
|
||||||
|
return await window.electronAPI.navigateParagraph(direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionStatus,
|
||||||
|
activeDocument,
|
||||||
|
documents,
|
||||||
|
paragraphContent,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
autoSync,
|
||||||
|
isConnected,
|
||||||
|
hasRevisions,
|
||||||
|
hasComments,
|
||||||
|
canEdit,
|
||||||
|
setupEventListeners,
|
||||||
|
refreshParagraph,
|
||||||
|
updateParagraph,
|
||||||
|
navigateParagraph
|
||||||
|
};
|
||||||
|
});
|
||||||
29
src/types/electron.d.ts
vendored
普通文件
29
src/types/electron.d.ts
vendored
普通文件
@ -0,0 +1,29 @@
|
|||||||
|
interface ElectronAPI {
|
||||||
|
minimizeWindow: () => Promise<void>;
|
||||||
|
closeWindow: () => Promise<void>;
|
||||||
|
login: (credentials: any) => Promise<any>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
showSettings: () => Promise<void>;
|
||||||
|
openFile: (filePath?: string) => Promise<any>;
|
||||||
|
getWPSStatus: () => Promise<any>;
|
||||||
|
navigateParagraph: (direction: string) => Promise<boolean>;
|
||||||
|
getFullParagraphContent: () => Promise<any>;
|
||||||
|
updateParagraphWithRevisions: (content: string) => Promise<any>;
|
||||||
|
handleRevision: (action: string, revisionIndex: number | null) => Promise<any>;
|
||||||
|
addComment: (commentText: string) => Promise<any>;
|
||||||
|
setTrackRevisions: (track: boolean) => Promise<boolean>;
|
||||||
|
switchDocument: (filePath: string) => Promise<boolean>;
|
||||||
|
onWPSStatusChange: (callback: (event: any, status: any) => void) => void;
|
||||||
|
onDocumentsListChange: (callback: (event: any, documents: any[]) => void) => void;
|
||||||
|
onActiveDocumentChange: (callback: (event: any, document: any) => void) => void;
|
||||||
|
onFullParagraphContentChange: (callback: (event: any, content: any) => void) => void;
|
||||||
|
removeAllListeners: (channel: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronAPI: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
227
src/views/LoginWindow.vue
普通文件
227
src/views/LoginWindow.vue
普通文件
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-window">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>AI Electron Client</h2>
|
||||||
|
<p>请登录您的账户</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleLogin" class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="credentials.username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="credentials.password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="login-btn"
|
||||||
|
:disabled="loading || !credentials.username || !credentials.password"
|
||||||
|
>
|
||||||
|
<span v-if="loading" class="loading-spinner"></span>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-info">
|
||||||
|
<p>演示账号: 任意用户名和密码</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const credentials = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!credentials.username.trim() || !credentials.password.trim()) {
|
||||||
|
error.value = '请输入用户名和密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.login(credentials);
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(result.user));
|
||||||
|
router.push('/main');
|
||||||
|
} else {
|
||||||
|
error.value = result?.message || '登录失败';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = '登录过程中发生错误';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||||
|
if (isLoggedIn === 'true') {
|
||||||
|
router.push('/main');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-window {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
882
src/views/MainWindow.vue
普通文件
882
src/views/MainWindow.vue
普通文件
@ -0,0 +1,882 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-window">
|
||||||
|
<div class="title-bar">
|
||||||
|
<div class="title">AI Electron Client - WPS文档助手</div>
|
||||||
|
<div class="window-controls">
|
||||||
|
<button class="control-btn minimize" @click="minimize">—</button>
|
||||||
|
<button class="control-btn close" @click="close">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>已打开文档</h3>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button @click="refreshDocuments" class="action-btn" title="刷新">
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
<button @click="openFile" class="action-btn" title="打开文件">
|
||||||
|
📁
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="document-list">
|
||||||
|
<div
|
||||||
|
v-for="doc in documents"
|
||||||
|
:key="doc.path"
|
||||||
|
class="doc-item"
|
||||||
|
:class="{ active: doc.isActive }"
|
||||||
|
@click="switchDocument(doc.path)"
|
||||||
|
>
|
||||||
|
<div class="doc-icon">📄</div>
|
||||||
|
<div class="doc-info">
|
||||||
|
<div class="doc-name">{{ doc.name }}</div>
|
||||||
|
<div class="doc-path">{{ getShortPath(doc.path) }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="doc.isActive" class="active-indicator">●</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="documents.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<p>暂无打开的文档</p>
|
||||||
|
<button class="open-file-btn" @click="openFile">打开文件</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wps-status" :class="{ connected: isConnected }">
|
||||||
|
<div class="status-icon"></div>
|
||||||
|
<span class="status-text">{{ isConnected ? 'WPS 已连接' : 'WPS 未连接' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="status-bar" :class="{ connected: isConnected }">
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-indicator"></div>
|
||||||
|
<span class="status-text">{{ statusText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<span v-if="activeDocument" class="active-doc">当前文档: {{ activeDocument.name }}</span>
|
||||||
|
<span class="document-count">文档数: {{ documents.length }}</span>
|
||||||
|
<span class="revision-count" v-if="hasRevisions">{{ paragraphContent.revisions.length }} 个修订</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-area">
|
||||||
|
<!-- 导航 -->
|
||||||
|
<div class="navigation">
|
||||||
|
<button @click="navigateParagraph('prev')" :disabled="!canNavigate">⬅️ 上一段</button>
|
||||||
|
<div class="navigation-info">
|
||||||
|
<span v-if="paragraphContent.text && isConnected" class="has-content">
|
||||||
|
当前段落 ({{ paragraphContent.text.length }} 字符)
|
||||||
|
</span>
|
||||||
|
<span v-else-if="isConnected" class="no-content">未选择段落</span>
|
||||||
|
<span v-else class="disconnected">WPS 未连接</span>
|
||||||
|
</div>
|
||||||
|
<button @click="navigateParagraph('next')" :disabled="!canNavigate">下一段 ➡️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修订面板 -->
|
||||||
|
<div v-if="hasRevisions" class="revisions-panel">
|
||||||
|
<h4>修订内容 ({{ paragraphContent.revisions.length }})</h4>
|
||||||
|
<div class="revisions-list">
|
||||||
|
<div v-for="revision in paragraphContent.revisions" :key="revision.index" class="revision-item">
|
||||||
|
<div class="revision-header">
|
||||||
|
<span class="revision-type">{{ revision.type }}</span>
|
||||||
|
<span class="revision-author">{{ revision.author }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="revision-content">{{ revision.text }}</div>
|
||||||
|
<div class="revision-actions">
|
||||||
|
<button @click="handleRevision('accept', revision.index)" class="action-btn accept">接受</button>
|
||||||
|
<button @click="handleRevision('reject', revision.index)" class="action-btn reject">拒绝</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑器 -->
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="editor-header">
|
||||||
|
<h4>段落内容编辑</h4>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button @click="updateParagraphWithRevisions" :disabled="!hasUnsavedChanges" class="save-btn">
|
||||||
|
💾 保存
|
||||||
|
</button>
|
||||||
|
<button @click="refreshParagraphContent" class="refresh-btn">
|
||||||
|
🔄 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-model="editableContent"
|
||||||
|
@input="onParagraphInput"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
class="paragraph-editor"
|
||||||
|
:class="{ 'has-unsaved': hasUnsavedChanges }"
|
||||||
|
placeholder="段落内容将显示在这里..."
|
||||||
|
rows="12"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="editor-footer">
|
||||||
|
<label class="auto-sync-toggle">
|
||||||
|
<input type="checkbox" v-model="autoSync"> 自动保存
|
||||||
|
</label>
|
||||||
|
<div class="editor-stats">
|
||||||
|
<span class="char-count">{{ editableContent.length }} 字符</span>
|
||||||
|
<span v-if="hasUnsavedChanges" class="unsaved-badge">未保存</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="openFile" :disabled="!isConnected" class="btn primary">
|
||||||
|
📁 打开文件
|
||||||
|
</button>
|
||||||
|
<button @click="refreshDocuments" class="btn secondary">
|
||||||
|
🔄 刷新列表
|
||||||
|
</button>
|
||||||
|
<button @click="openSettings" class="btn secondary">
|
||||||
|
⚙️ 设置
|
||||||
|
</button>
|
||||||
|
<button @click="logout" class="btn danger">
|
||||||
|
🚪 退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 连接提示 -->
|
||||||
|
<div v-if="!isConnected" class="connection-prompt">
|
||||||
|
<div class="prompt-content">
|
||||||
|
<div class="prompt-icon">⚠️</div>
|
||||||
|
<h3>WPS 未连接</h3>
|
||||||
|
<p>无法与 WPS Office 建立连接</p>
|
||||||
|
<button @click="refreshDocuments" class="btn primary">
|
||||||
|
重新连接
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useWPSStore } from '../stores/wpsStore';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const wpsStore = useWPSStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
connectionStatus: isConnected,
|
||||||
|
activeDocument,
|
||||||
|
documents,
|
||||||
|
paragraphContent,
|
||||||
|
hasUnsavedChanges,
|
||||||
|
autoSync,
|
||||||
|
hasRevisions,
|
||||||
|
canEdit,
|
||||||
|
setupEventListeners,
|
||||||
|
refreshParagraph,
|
||||||
|
updateParagraph,
|
||||||
|
navigateParagraph
|
||||||
|
} = wpsStore;
|
||||||
|
|
||||||
|
const editableContent = ref('');
|
||||||
|
const editTimer = ref<any>(null);
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (!isConnected.value) return 'WPS 未连接';
|
||||||
|
if (!activeDocument.value) return 'WPS 已连接 - 无活动文档';
|
||||||
|
return `WPS 已连接 - 正在编辑: ${activeDocument.value.name}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const minimize = () => window.electronAPI.minimizeWindow();
|
||||||
|
const close = () => window.electronAPI.closeWindow();
|
||||||
|
|
||||||
|
const openFile = async () => {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.openFile();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDocuments = async () => {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.getWPSStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh documents:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshParagraphContent = () => {
|
||||||
|
refreshParagraph().then(() => {
|
||||||
|
editableContent.value = paragraphContent.value.text;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateParagraphWithRevisions = async () => {
|
||||||
|
const result = await updateParagraph(editableContent.value);
|
||||||
|
if (result.success) {
|
||||||
|
console.log('段落已更新');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevision = async (action: string, revisionIndex: number) => {
|
||||||
|
await window.electronAPI.handleRevision(action, revisionIndex);
|
||||||
|
refreshParagraphContent();
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchDocument = async (filePath: string) => {
|
||||||
|
await window.electronAPI.switchDocument(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSettings = () => {
|
||||||
|
window.electronAPI.showSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
localStorage.removeItem('userInfo');
|
||||||
|
await window.electronAPI.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShortPath = (fullPath: string) => {
|
||||||
|
if (!fullPath) return '';
|
||||||
|
const parts = fullPath.split('\\');
|
||||||
|
return parts.length > 2 ? `...\\${parts.slice(-2).join('\\')}` : fullPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onParagraphInput = () => {
|
||||||
|
hasUnsavedChanges.value = true;
|
||||||
|
|
||||||
|
if (autoSync.value) {
|
||||||
|
clearTimeout(editTimer.value);
|
||||||
|
editTimer.value = setTimeout(updateParagraphWithRevisions, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupEventListeners();
|
||||||
|
refreshParagraphContent();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (isConnected.value) {
|
||||||
|
refreshParagraphContent();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
if (editTimer.value) clearTimeout(editTimer.value);
|
||||||
|
window.electronAPI.removeAllListeners('wps-status-changed');
|
||||||
|
window.electronAPI.removeAllListeners('documents-list-changed');
|
||||||
|
window.electronAPI.removeAllListeners('active-document-changed');
|
||||||
|
window.electronAPI.removeAllListeners('full-paragraph-content-changed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-window {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
height: 32px;
|
||||||
|
background: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.close:hover {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.minimize:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: white;
|
||||||
|
border-right: 1px solid #e1e5e9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e1e5e9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-item.active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-path {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-top: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-indicator {
|
||||||
|
color: #2196f3;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-file-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #e1e5e9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status.connected {
|
||||||
|
background: rgba(39, 174, 96, 0.1);
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status:not(.connected) {
|
||||||
|
background: rgba(231, 76, 60, 0.1);
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status.connected .status-icon {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.connected {
|
||||||
|
background: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-doc {
|
||||||
|
max-width: 200px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-count {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(243, 156, 18, 0.2);
|
||||||
|
color: #f39c12;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation button {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation button:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-info {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-content {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnected {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revisions-panel {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revisions-panel h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-item {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-type {
|
||||||
|
background: #f39c12;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-author {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-content {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-actions .action-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-actions .accept {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-actions .reject {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e1e5e9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn, .refresh-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph-editor {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph-editor:disabled {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #7f8c8d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph-editor.has-unsaved {
|
||||||
|
border-left: 3px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e1e5e9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-sync-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsaved-badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover:not(:disabled) {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary:hover:not(:disabled) {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger:hover:not(:disabled) {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-prompt {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-content h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-content p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
270
src/views/SettingsWindow.vue
普通文件
270
src/views/SettingsWindow.vue
普通文件
@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-window">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>应用设置</h2>
|
||||||
|
<button class="close-btn" @click="closeWindow">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>基本设置</h3>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">
|
||||||
|
<input type="checkbox" v-model="localConfig.autoConnectWPS" @change="updateConfig">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
启动时自动连接WPS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">
|
||||||
|
<input type="checkbox" v-model="localConfig.showFloatingWindow" @change="updateConfig">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
显示悬浮窗
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>WPS 设置</h3>
|
||||||
|
|
||||||
|
<div class="wps-status">
|
||||||
|
<h4>连接状态</h4>
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-indicator" :class="{ connected: wpsConnected }"></div>
|
||||||
|
<span class="status-text">{{ wpsStatusText }}</span>
|
||||||
|
</div>
|
||||||
|
<button @click="testConnection" class="btn primary">测试连接</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-footer">
|
||||||
|
<button class="btn secondary" @click="closeWindow">关闭</button>
|
||||||
|
<button class="btn primary" @click="saveSettings">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const localConfig = reactive({
|
||||||
|
autoConnectWPS: true,
|
||||||
|
showFloatingWindow: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const wpsConnected = ref(false);
|
||||||
|
|
||||||
|
const wpsStatusText = ref('未知');
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const config = await window.electronAPI.getAppConfig();
|
||||||
|
Object.assign(localConfig, config);
|
||||||
|
|
||||||
|
const status = await window.electronAPI.getWPSStatus();
|
||||||
|
wpsConnected.value = status.connected;
|
||||||
|
wpsStatusText.value = status.connected ? '已连接' : '未连接';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfig = () => {
|
||||||
|
window.electronAPI.setAppConfig({ ...localConfig });
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = () => {
|
||||||
|
window.electronAPI.setAppConfig({ ...localConfig });
|
||||||
|
closeWindow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeWindow = () => {
|
||||||
|
window.electronAPI.closeWindow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConnection = async () => {
|
||||||
|
try {
|
||||||
|
const status = await window.electronAPI.getWPSStatus();
|
||||||
|
wpsConnected.value = status.connected;
|
||||||
|
wpsStatusText.value = status.connected ? '连接成功' : '连接失败';
|
||||||
|
alert(status.connected ? '✅ WPS 连接正常' : '❌ WPS 连接失败');
|
||||||
|
} catch (error) {
|
||||||
|
alert('测试连接失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-window {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6c757d;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #ced4da;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label input[type="checkbox"]:checked + .checkmark {
|
||||||
|
background: #2196f3;
|
||||||
|
border-color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label input[type="checkbox"]:checked + .checkmark::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
top: 1px;
|
||||||
|
width: 6px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wps-status h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected {
|
||||||
|
background: #27ae60;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-footer {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
tsconfig.json
普通文件
12
tsconfig.json
普通文件
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户