You've already forked Nano-Banana-AI-Image-Editor
新增 现在支持编译为Windows桌面端
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,3 +23,5 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
release
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
54
build/README.md
Normal file
54
build/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 应用图标
|
||||
|
||||
此目录包含用于打包桌面应用程序的图标文件。
|
||||
|
||||
## Windows 平台
|
||||
|
||||
Windows 平台需要 ICO 格式的图标文件,命名为 `icon.ico`。
|
||||
|
||||
### 如何创建 ICO 文件
|
||||
|
||||
有几种方法可以创建 ICO 文件:
|
||||
|
||||
#### 方法1:使用 Python 脚本(推荐)
|
||||
|
||||
项目根目录下提供了一个 Python 脚本 `convert_svg_to_ico.py`,可以直接将 SVG 转换为 ICO 格式:
|
||||
|
||||
1. 确保已安装 Python 3 和必要的依赖包:
|
||||
```
|
||||
pip install Pillow cairosvg
|
||||
```
|
||||
|
||||
2. 在 Windows 上,您可能还需要安装 GTK3 运行时环境:
|
||||
- 下载并安装 GTK3: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer
|
||||
|
||||
3. 运行转换脚本:
|
||||
```
|
||||
npm run electron:icon
|
||||
```
|
||||
或直接运行:
|
||||
```
|
||||
python convert_svg_to_ico.py
|
||||
```
|
||||
|
||||
脚本会自动将 `public/favicon.svg` 转换为 `build/icon.ico`,并包含多个尺寸。
|
||||
|
||||
#### 方法2:使用在线转换工具
|
||||
|
||||
- [ConvertICO](https://convertico.com/)
|
||||
- [Online-Convert](https://image.online-convert.com/convert-to-ico)
|
||||
- [CloudConvert](https://cloudconvert.com/png-to-ico)
|
||||
|
||||
#### 方法3:使用图像编辑软件
|
||||
|
||||
使用支持 ICO 格式的图像编辑软件(如 GIMP、Photoshop 插件等)手动创建。
|
||||
|
||||
### 图标尺寸要求
|
||||
|
||||
Windows 应用程序图标应包含多个尺寸:
|
||||
- 16x16
|
||||
- 32x32
|
||||
- 48x48
|
||||
- 256x256
|
||||
|
||||
确保在生成 ICO 文件时包含这些尺寸。
|
||||
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
14
copy-favicon.js
Normal file
14
copy-favicon.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { copyFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const source = resolve('./public/favicon.svg');
|
||||
const dest = resolve('./dist/favicon.svg');
|
||||
|
||||
// 确保 dist 目录存在
|
||||
if (!existsSync('./dist')) {
|
||||
mkdirSync('./dist');
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
copyFileSync(source, dest);
|
||||
console.log('favicon.svg copied successfully');
|
||||
56
electron/dev-runner.js
Normal file
56
electron/dev-runner.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 先编译 TypeScript 文件
|
||||
const tsc = spawn('npx', ['tsc', '-p', 'tsconfig.electron.json'], {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
|
||||
tsc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// 编译成功后复制 preload.js 文件
|
||||
const sourcePreload = path.join(__dirname, 'preload.js');
|
||||
const destPreload = path.join(__dirname, 'dist', 'preload.js');
|
||||
|
||||
try {
|
||||
fs.copyFileSync(sourcePreload, destPreload);
|
||||
console.log('preload.js copied successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy preload.js:', err);
|
||||
}
|
||||
|
||||
// 编译成功后运行 Electron 应用
|
||||
const electron = require('electron');
|
||||
const appPath = path.join(__dirname, 'dist', 'main.js');
|
||||
|
||||
// 启动 Vite 开发服务器
|
||||
const vite = spawn('npm', ['run', 'dev'], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: 'http://localhost:5173',
|
||||
},
|
||||
});
|
||||
|
||||
// 等待一段时间让 Vite 服务器启动,然后启动 Electron
|
||||
setTimeout(() => {
|
||||
const child = spawn(electron, [appPath], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: 'http://localhost:5173',
|
||||
},
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
}, 5000); // 等待 5 秒钟让 Vite 服务器启动
|
||||
} else {
|
||||
console.error('TypeScript compilation failed');
|
||||
process.exit(code || 1);
|
||||
}
|
||||
});
|
||||
3
electron/index.js
Normal file
3
electron/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// Electron 入口文件
|
||||
// 使用 require 而不是 import 来避免 ES 模块问题
|
||||
require('./dist/main.js');
|
||||
59
electron/main.js
Normal file
59
electron/main.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||
}
|
||||
|
||||
// Open the DevTools.
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
};
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
93
electron/main.ts
Normal file
93
electron/main.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import * as path from 'path';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (require('electron-squirrel-startup')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false, // 隐藏默认的窗口框架
|
||||
icon: path.join(__dirname, '../../build/icon.ico'), // 设置应用图标
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
// 使用绝对路径从项目根目录的 dist 文件夹加载
|
||||
const indexPath = path.join(__dirname, '../../dist/index.html');
|
||||
console.log('Loading index.html from:', indexPath);
|
||||
mainWindow.loadFile(indexPath);
|
||||
}
|
||||
|
||||
// Open the DevTools.
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
};
|
||||
|
||||
// IPC 处理程序
|
||||
ipcMain.handle('window-minimize', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-maximize', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-close', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-is-maximized', () => {
|
||||
return mainWindow ? mainWindow.isMaximized() : false;
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
3
electron/package.json
Normal file
3
electron/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
20
electron/preload.js
Normal file
20
electron/preload.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { contextBridge, ipcRenderer, remote } = require('electron');
|
||||
|
||||
// As an example, we expose a function to the renderer process
|
||||
// that shows a dialog
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
showDialog: () => ipcRenderer.invoke('show-dialog'),
|
||||
});
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {};
|
||||
|
||||
// 窗口控制 API
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
minimize: () => ipcRenderer.invoke('window-minimize'),
|
||||
maximize: () => ipcRenderer.invoke('window-maximize'),
|
||||
close: () => ipcRenderer.invoke('window-close'),
|
||||
isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||
});
|
||||
|
||||
module.exports = api;
|
||||
52
electron/prod-runner.js
Normal file
52
electron/prod-runner.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 先编译 TypeScript 文件
|
||||
const tsc = spawn('npx', ['tsc', '-p', 'tsconfig.electron.json'], {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
|
||||
tsc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// 编译成功后复制 preload.js 文件
|
||||
const sourcePreload = path.join(__dirname, 'preload.js');
|
||||
const destPreload = path.join(__dirname, 'dist', 'preload.js');
|
||||
|
||||
try {
|
||||
fs.copyFileSync(sourcePreload, destPreload);
|
||||
console.log('preload.js copied successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy preload.js:', err);
|
||||
}
|
||||
|
||||
// 编译成功后运行 Electron 应用
|
||||
const electron = require('electron');
|
||||
const appPath = path.join(__dirname, 'dist', 'main.js');
|
||||
|
||||
// 先确保应用已构建
|
||||
const build = spawn('npm', ['run', 'build'], {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
|
||||
build.on('close', (buildCode) => {
|
||||
if (buildCode === 0) {
|
||||
const child = spawn(electron, [appPath], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
} else {
|
||||
console.error('Build failed');
|
||||
process.exit(buildCode || 1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('TypeScript compilation failed');
|
||||
process.exit(code || 1);
|
||||
}
|
||||
});
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
-->
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍌</text></svg>" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nano Banana AI 图像编辑器 - AI 图像生成器和编辑器</title>
|
||||
<meta name="description" content="由 Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑。使用自然语言提示创建、编辑和增强图像。" />
|
||||
|
||||
8833
package-lock.json
generated
8833
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -8,13 +8,24 @@
|
||||
"type": "git",
|
||||
"url": "https://git.pandorastudio.cn/yuantao/Nano-Banana-AI-Image-Editor.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Mark Fulton",
|
||||
"email": "markfulton@example.com",
|
||||
"url": "https://markfulton.com"
|
||||
},
|
||||
"main": "electron/index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "vite build && node copy-favicon.js",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
"test:watch": "jest --watch",
|
||||
"electron:dev": "node electron/dev-runner.js",
|
||||
"electron:build": "vite build && copy public\\favicon.svg dist\\ && electron-builder --win --x64",
|
||||
"electron:preview": "node electron/prod-runner.js",
|
||||
"electron:compile": "tsc -p tsconfig.electron.json",
|
||||
"postinstall": "echo Skipping electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.16.0",
|
||||
@@ -25,6 +36,8 @@
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"fabric": "^6.7.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"konva": "^9.3.22",
|
||||
@@ -46,6 +59,8 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"electron": "^38.2.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
@@ -53,10 +68,38 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest-environment-jsdom": "^30.1.2",
|
||||
"postcss": "^8.4.35",
|
||||
"pump": "^3.0.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tar": "^7.4.3",
|
||||
"ts-jest": "^29.4.3",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.nanobanana.editor",
|
||||
"productName": "Nano Banana AI Image Editor",
|
||||
"npmRebuild": false,
|
||||
"electronDownload": {
|
||||
"mirror": "https://npmmirror.com/mirrors/electron/",
|
||||
"customDir": "38.2.1",
|
||||
"customFilename": "electron-v38.2.1-win32-x64.zip"
|
||||
},
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "build"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"icon": "build/icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" fill="#FDE047"/>
|
||||
<text x="50" y="70" font-size="60" text-anchor="middle" fill="#000">🍌</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cn } from './utils/cn';
|
||||
import { Header } from './components/Header';
|
||||
import { CustomTitleBar } from './components/CustomTitleBar';
|
||||
import { PromptComposer } from './components/PromptComposer';
|
||||
import { ImageCanvas } from './components/ImageCanvas';
|
||||
import { HistoryPanel } from './components/HistoryPanel';
|
||||
@@ -91,9 +91,7 @@ function AppContent() {
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
|
||||
<div className="card card-lg rounded-none">
|
||||
<Header />
|
||||
</div>
|
||||
<CustomTitleBar />
|
||||
|
||||
<div className="flex-1 flex overflow-hidden p-4 gap-4 relative">
|
||||
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showPromptPanel ? "w-72" : "w-8")}>
|
||||
|
||||
107
src/components/CustomTitleBar.tsx
Normal file
107
src/components/CustomTitleBar.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from './ui/Button';
|
||||
import { HelpCircle, Minus, Square, X } from 'lucide-react';
|
||||
import { InfoModal } from './InfoModal';
|
||||
|
||||
export const CustomTitleBar: React.FC = () => {
|
||||
const [showInfoModal, setShowInfoModal] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
|
||||
// 检查窗口是否最大化
|
||||
useEffect(() => {
|
||||
const checkMaximized = () => {
|
||||
if (window.electron && window.electron.isMaximized) {
|
||||
window.electron.isMaximized().then(setIsMaximized);
|
||||
}
|
||||
};
|
||||
|
||||
checkMaximized();
|
||||
|
||||
// 监听窗口状态变化
|
||||
const handleResize = () => {
|
||||
checkMaximized();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const minimizeWindow = () => {
|
||||
if (window.electron && window.electron.minimize) {
|
||||
window.electron.minimize();
|
||||
}
|
||||
};
|
||||
|
||||
const maximizeWindow = () => {
|
||||
if (window.electron && window.electron.maximize) {
|
||||
window.electron.maximize();
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = () => {
|
||||
if (window.electron && window.electron.close) {
|
||||
window.electron.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="h-12 bg-white flex items-center justify-between px-3 rounded-t-xl border-b border-gray-200 draggable">
|
||||
<div className="flex items-center space-x-2 non-draggable">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-yellow-50">
|
||||
{/* 使用应用的主要图标 */}
|
||||
<img src="./favicon.svg" alt="App Icon" className="w-6 h-6" />
|
||||
</div>
|
||||
<h1 className="text-base font-medium text-gray-800 hidden sm:block">
|
||||
Nano Banana AI 图像编辑器
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 non-draggable">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowInfoModal(true)}
|
||||
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* 窗口控制按钮 - 仅在 Electron 环境中显示 */}
|
||||
{window.electron && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={minimizeWindow}
|
||||
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={maximizeWindow}
|
||||
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={closeWindow}
|
||||
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-red-100 rounded-full card non-draggable"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<InfoModal open={showInfoModal} onOpenChange={setShowInfoModal} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -233,3 +233,52 @@ body {
|
||||
.card-lg {
|
||||
@apply shadow-card-lg;
|
||||
}
|
||||
|
||||
/* Electron window drag regions */
|
||||
.draggable {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.non-draggable {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* 窗口控制按钮 */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 10px;
|
||||
height: 30px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-controls button {
|
||||
width: 45px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.window-controls button:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.window-controls .close:hover {
|
||||
background: #ff5f56;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.window-controls .minimize:hover {
|
||||
background: #ffbd2e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.window-controls .maximize:hover {
|
||||
background: #27c93f;
|
||||
color: white;
|
||||
}
|
||||
30
tsconfig.electron.json
Normal file
30
tsconfig.electron.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2023"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"skipLibCheck": true,
|
||||
"outDir": "electron/dist",
|
||||
"rootDir": "electron",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"electron/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import * as path from 'path';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import * as path from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './', // 添加这行以确保资源路径正确
|
||||
publicDir: path.resolve(__dirname, 'public'), // 使用绝对路径指定 public 目录
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
@@ -14,4 +21,20 @@ export default defineConfig({
|
||||
'react-day-picker/locale': 'date-fns/locale',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'assets/[name].[hash].js',
|
||||
chunkFileNames: 'assets/[name].[hash].js',
|
||||
assetFileNames: 'assets/[name].[hash].[ext]',
|
||||
},
|
||||
},
|
||||
// 确保 public 目录下的文件被复制到 dist 目录
|
||||
copyPublicDir: true,
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VITE_DEV_SERVER_URL': JSON.stringify(process.env.VITE_DEV_SERVER_URL),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user