Trae Assistant commited on
Commit
e19997d
·
0 Parent(s):

feat: enhance app and configure for huggingface

Browse files
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+
7
+ RUN npm install
8
+
9
+ COPY . .
10
+
11
+ # Build the web version
12
+ RUN npm run build
13
+
14
+ # Install serve to run the static site
15
+ RUN npm install -g serve
16
+
17
+ # Hugging Face Spaces default port
18
+ EXPOSE 7860
19
+
20
+ # Serve the 'dist' directory (Web build output) on port 7860
21
+ CMD ["serve", "-s", "dist", "-l", "7860"]
README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Electron 跨平台演示
3
+ emoji: ⚡
4
+ colorFrom: blue
5
+ colorTo: cyan
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: 一个基于 Electron + Vite + React 的跨平台应用示例
9
+ ---
10
+
11
+ # Electron 跨平台项目
12
+
13
+ 这是一个基于 Electron、Vite 和 React 的跨平台应用示例。
14
+ 本项目旨在展示如何兼顾桌面端 (Electron) 和 Web 端开发。
15
+
16
+ ## 功能特性
17
+
18
+ - ⚡ **Electron**: 桌面端跨平台支持 (Windows, macOS, Linux)
19
+ - ⚛️ **React**: 现代前端框架
20
+ - 🚀 **Vite**: 极速构建工具
21
+ - 🌐 **Web 兼容**: 支持在浏览器中运行
22
+ - 📦 **Docker**: 支持容器化部署 Web 版本
23
+
24
+ ## 快速开始
25
+
26
+ ### 1. 安装依赖
27
+
28
+ ```bash
29
+ npm install
30
+ ```
31
+
32
+ ### 2. 开发模式
33
+
34
+ ```bash
35
+ # 启动开发服务器 (同时启动 Electron 和 Web)
36
+ npm run dev
37
+ ```
38
+
39
+ ### 3. 构建
40
+
41
+ ```bash
42
+ # 构建 Electron 应用和 Web 版本
43
+ npm run build
44
+ ```
45
+
46
+ 构建产物:
47
+ - `dist`: Web 版本静态文件
48
+ - `dist-electron`: Electron 主进程文件
49
+ - `release`: 打包后的 Electron 安装包 (需配置 electron-builder)
50
+
51
+ ## Docker 部署
52
+
53
+ 本项目支持 Docker 部署 Web 版本,适用于 Hugging Face Spaces 等平台。
54
+
55
+ ```bash
56
+ docker build -t electron-web .
57
+ docker run -p 7860:7860 electron-web
58
+ ```
59
+
60
+ ## 项目结构
61
+
62
+ - `electron/`: Electron 主进程和预加载脚本
63
+ - `src/`: React 渲染进程源码
64
+ - `dist/`: Web 构建输出
65
+ - `dist-electron/`: Electron 构建输出
66
+
67
+ ## 许可证
68
+
69
+ MIT
dist-electron/main.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const path = require("node:path");
4
+ const os = require("node:os");
5
+ process.env.DIST = path.join(__dirname, "../dist");
6
+ process.env.VITE_PUBLIC = electron.app.isPackaged ? process.env.DIST : path.join(__dirname, "../public");
7
+ let win;
8
+ const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
9
+ function createWindow() {
10
+ win = new electron.BrowserWindow({
11
+ icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
12
+ webPreferences: {
13
+ preload: path.join(__dirname, "preload.js")
14
+ }
15
+ });
16
+ win.webContents.on("did-finish-load", () => {
17
+ win?.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString());
18
+ });
19
+ if (VITE_DEV_SERVER_URL) {
20
+ win.loadURL(VITE_DEV_SERVER_URL);
21
+ } else {
22
+ win.loadFile(path.join(process.env.DIST, "index.html"));
23
+ }
24
+ }
25
+ electron.app.on("window-all-closed", () => {
26
+ if (process.platform !== "darwin") {
27
+ electron.app.quit();
28
+ win = null;
29
+ }
30
+ });
31
+ electron.app.on("activate", () => {
32
+ if (electron.BrowserWindow.getAllWindows().length === 0) {
33
+ createWindow();
34
+ }
35
+ });
36
+ electron.app.whenReady().then(() => {
37
+ electron.ipcMain.handle("get-system-info", () => {
38
+ return {
39
+ platform: process.platform,
40
+ arch: process.arch,
41
+ node: process.versions.node,
42
+ electron: process.versions.electron,
43
+ chrome: process.versions.chrome,
44
+ cpus: os.cpus().length,
45
+ memory: Math.round(os.totalmem() / 1024 / 1024 / 1024) + " GB"
46
+ };
47
+ });
48
+ createWindow();
49
+ });
dist-electron/preload.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ const electron = require("electron");
3
+ electron.contextBridge.exposeInMainWorld("ipcRenderer", {
4
+ on(...args) {
5
+ const [channel, listener] = args;
6
+ return electron.ipcRenderer.on(channel, (event, ...args2) => listener(event, ...args2));
7
+ },
8
+ off(...args) {
9
+ const [channel, ...omit] = args;
10
+ return electron.ipcRenderer.off(channel, ...omit);
11
+ },
12
+ send(...args) {
13
+ const [channel, ...omit] = args;
14
+ return electron.ipcRenderer.send(channel, ...omit);
15
+ },
16
+ invoke(...args) {
17
+ const [channel, ...omit] = args;
18
+ return electron.ipcRenderer.invoke(channel, ...omit);
19
+ }
20
+ // You can expose other APTs you need here.
21
+ // ...
22
+ });
23
+ electron.contextBridge.exposeInMainWorld("electronAPI", {
24
+ getSystemInfo: () => electron.ipcRenderer.invoke("get-system-info")
25
+ });
electron/main.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { app, BrowserWindow, ipcMain } from 'electron'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+
5
+ // The built directory structure
6
+ //
7
+ // ├─┬ dist-electron
8
+ // │ ├─┬ main
9
+ // │ │ └── index.js
10
+ // │ └─┬ preload
11
+ // │ └── index.js
12
+ // ├─┬ dist
13
+ // │ └── index.html
14
+
15
+ process.env.DIST = path.join(__dirname, '../dist')
16
+ process.env.VITE_PUBLIC = app.isPackaged ? process.env.DIST : path.join(__dirname, '../public')
17
+
18
+ let win: BrowserWindow | null
19
+
20
+ const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
21
+
22
+ function createWindow() {
23
+ win = new BrowserWindow({
24
+ icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
25
+ webPreferences: {
26
+ preload: path.join(__dirname, 'preload.js'),
27
+ },
28
+ })
29
+
30
+ // Test active push message to Renderer-process.
31
+ win.webContents.on('did-finish-load', () => {
32
+ win?.webContents.send('main-process-message', (new Date).toLocaleString())
33
+ })
34
+
35
+ if (VITE_DEV_SERVER_URL) {
36
+ win.loadURL(VITE_DEV_SERVER_URL)
37
+ } else {
38
+ // win.loadFile('dist/index.html')
39
+ win.loadFile(path.join(process.env.DIST, 'index.html'))
40
+ }
41
+ }
42
+
43
+ // Quit when all windows are closed, except on macOS. There, it's common
44
+ // for applications and their menu bar to stay active until the user quits
45
+ // explicitly with Cmd + Q.
46
+ app.on('window-all-closed', () => {
47
+ if (process.platform !== 'darwin') {
48
+ app.quit()
49
+ win = null
50
+ }
51
+ })
52
+
53
+ app.on('activate', () => {
54
+ // On OS X it's common to re-create a window in the app when the
55
+ // dock icon is clicked and there are no other windows open.
56
+ if (BrowserWindow.getAllWindows().length === 0) {
57
+ createWindow()
58
+ }
59
+ })
60
+
61
+ app.whenReady().then(() => {
62
+ ipcMain.handle('get-system-info', () => {
63
+ return {
64
+ platform: process.platform,
65
+ arch: process.arch,
66
+ node: process.versions.node,
67
+ electron: process.versions.electron,
68
+ chrome: process.versions.chrome,
69
+ cpus: os.cpus().length,
70
+ memory: Math.round(os.totalmem() / 1024 / 1024 / 1024) + ' GB',
71
+ }
72
+ })
73
+ createWindow()
74
+ })
electron/preload.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { contextBridge, ipcRenderer } from 'electron'
2
+
3
+ // --------- Expose some API to the Renderer process ---------
4
+ contextBridge.exposeInMainWorld('ipcRenderer', {
5
+ on(...args: Parameters<typeof ipcRenderer.on>) {
6
+ const [channel, listener] = args
7
+ return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
8
+ },
9
+ off(...args: Parameters<typeof ipcRenderer.off>) {
10
+ const [channel, ...omit] = args
11
+ return ipcRenderer.off(channel, ...omit)
12
+ },
13
+ send(...args: Parameters<typeof ipcRenderer.send>) {
14
+ const [channel, ...omit] = args
15
+ return ipcRenderer.send(channel, ...omit)
16
+ },
17
+ invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
18
+ const [channel, ...omit] = args
19
+ return ipcRenderer.invoke(channel, ...omit)
20
+ },
21
+
22
+ // You can expose other APTs you need here.
23
+ // ...
24
+ })
25
+
26
+ contextBridge.exposeInMainWorld('electronAPI', {
27
+ getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
28
+ })
eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>electron-cross-platform</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "electron-cross-platform",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "main": "dist-electron/main.js",
6
+ "description": "一个跨平台的 Electron 项目示例",
7
+ "author": "Your Name",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "tsc -b && vite build && electron-builder",
11
+ "lint": "eslint .",
12
+ "preview": "vite preview"
13
+ },
14
+ "dependencies": {
15
+ "react": "^19.2.0",
16
+ "react-dom": "^19.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.39.1",
20
+ "@types/node": "^24.10.1",
21
+ "@types/react": "^19.2.7",
22
+ "@types/react-dom": "^19.2.3",
23
+ "@vitejs/plugin-react": "^5.1.1",
24
+ "electron": "^40.8.0",
25
+ "electron-builder": "^26.8.1",
26
+ "eslint": "^9.39.1",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.4.24",
29
+ "globals": "^16.5.0",
30
+ "typescript": "~5.9.3",
31
+ "typescript-eslint": "^8.48.0",
32
+ "vite": "^7.3.1",
33
+ "vite-plugin-electron": "^0.29.0",
34
+ "vite-plugin-electron-renderer": "^0.14.6"
35
+ }
36
+ }
public/vite.svg ADDED
src/App.css ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
43
+
44
+ /* Custom styles */
45
+ .status-badge {
46
+ display: inline-block;
47
+ padding: 8px 16px;
48
+ border-radius: 20px;
49
+ font-weight: bold;
50
+ margin: 10px 0;
51
+ }
52
+
53
+ .status-badge.electron {
54
+ background-color: #2b2e3b;
55
+ color: #9feaf9;
56
+ border: 1px solid #9feaf9;
57
+ }
58
+
59
+ .status-badge.web {
60
+ background-color: #f0f0f0;
61
+ color: #333;
62
+ border: 1px solid #ccc;
63
+ }
64
+
65
+ @media (prefers-color-scheme: dark) {
66
+ .status-badge.web {
67
+ background-color: #333;
68
+ color: #fff;
69
+ border: 1px solid #555;
70
+ }
71
+ }
72
+
73
+ .system-info {
74
+ margin-top: 15px;
75
+ text-align: left;
76
+ background: rgba(0, 0, 0, 0.05);
77
+ padding: 15px;
78
+ border-radius: 8px;
79
+ display: inline-block;
80
+ }
81
+
82
+ .system-info ul {
83
+ list-style: none;
84
+ padding: 0;
85
+ margin: 0;
86
+ }
87
+
88
+ .system-info li {
89
+ margin: 5px 0;
90
+ font-family: monospace;
91
+ }
92
+
93
+ .todo-input {
94
+ display: flex;
95
+ gap: 10px;
96
+ justify-content: center;
97
+ margin-bottom: 20px;
98
+ }
99
+
100
+ .todo-input input {
101
+ padding: 8px;
102
+ border-radius: 4px;
103
+ border: 1px solid #ccc;
104
+ width: 200px;
105
+ }
106
+
107
+ .todo-list {
108
+ list-style: none;
109
+ padding: 0;
110
+ max-width: 400px;
111
+ margin: 0 auto;
112
+ }
113
+
114
+ .todo-list li {
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ padding: 10px;
119
+ border-bottom: 1px solid #eee;
120
+ }
121
+
122
+ .todo-list li:last-child {
123
+ border-bottom: none;
124
+ }
125
+
126
+ .delete-btn {
127
+ background: none;
128
+ border: none;
129
+ cursor: pointer;
130
+ padding: 5px;
131
+ }
132
+
133
+ .delete-btn:hover {
134
+ background: rgba(255, 0, 0, 0.1);
135
+ border-radius: 50%;
136
+ }
src/App.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import reactLogo from './assets/react.svg'
3
+ import viteLogo from '/vite.svg'
4
+ import './App.css'
5
+
6
+ interface SystemInfo {
7
+ platform: string
8
+ arch: string
9
+ node: string
10
+ electron: string
11
+ chrome: string
12
+ cpus: number
13
+ memory: string
14
+ }
15
+
16
+ function App() {
17
+ const [count, setCount] = useState(0)
18
+ const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
19
+ const [isElectron, setIsElectron] = useState(false)
20
+ const [todos, setTodos] = useState<string[]>(() => {
21
+ const saved = localStorage.getItem('todos')
22
+ return saved ? JSON.parse(saved) : []
23
+ })
24
+ const [inputValue, setInputValue] = useState('')
25
+
26
+ useEffect(() => {
27
+ // Check if running in Electron
28
+ const checkElectron = async () => {
29
+ if (window.electronAPI) {
30
+ setIsElectron(true)
31
+ const info = await window.electronAPI.getSystemInfo()
32
+ setSystemInfo(info)
33
+ }
34
+ }
35
+ checkElectron()
36
+ }, [])
37
+
38
+ useEffect(() => {
39
+ localStorage.setItem('todos', JSON.stringify(todos))
40
+ }, [todos])
41
+
42
+ const addTodo = () => {
43
+ if (inputValue.trim()) {
44
+ setTodos([...todos, inputValue.trim()])
45
+ setInputValue('')
46
+ }
47
+ }
48
+
49
+ const removeTodo = (index: number) => {
50
+ setTodos(todos.filter((_, i) => i !== index))
51
+ }
52
+
53
+ return (
54
+ <div className="container">
55
+ <div>
56
+ <a href="https://vite.dev" target="_blank">
57
+ <img src={viteLogo} className="logo" alt="Vite logo" />
58
+ </a>
59
+ <a href="https://react.dev" target="_blank">
60
+ <img src={reactLogo} className="logo react" alt="React logo" />
61
+ </a>
62
+ </div>
63
+ <h1>Vite + React + Electron</h1>
64
+
65
+ <div className="card status-card">
66
+ <h2>运行环境检测</h2>
67
+ <div className={`status-badge ${isElectron ? 'electron' : 'web'}`}>
68
+ {isElectron ? '🖥️ Electron 桌面端' : '🌐 Web 浏览器端'}
69
+ </div>
70
+
71
+ {isElectron && systemInfo && (
72
+ <div className="system-info">
73
+ <h3>系统信息</h3>
74
+ <ul>
75
+ <li><strong>平台:</strong> {systemInfo.platform} ({systemInfo.arch})</li>
76
+ <li><strong>CPU核心:</strong> {systemInfo.cpus} 核</li>
77
+ <li><strong>内存:</strong> {systemInfo.memory}</li>
78
+ <li><strong>Chrome:</strong> {systemInfo.chrome}</li>
79
+ <li><strong>Electron:</strong> {systemInfo.electron}</li>
80
+ <li><strong>Node.js:</strong> {systemInfo.node}</li>
81
+ </ul>
82
+ </div>
83
+ )}
84
+ </div>
85
+
86
+ <div className="card">
87
+ <h2>功能演示 1: 计数器</h2>
88
+ <button onClick={() => setCount((count) => count + 1)}>
89
+ 计数器: {count}
90
+ </button>
91
+ </div>
92
+
93
+ <div className="card">
94
+ <h2>功能演示 2: 本地待办事项 (LocalStorage)</h2>
95
+ <div className="todo-input">
96
+ <input
97
+ value={inputValue}
98
+ onChange={(e) => setInputValue(e.target.value)}
99
+ onKeyDown={(e) => e.key === 'Enter' && addTodo()}
100
+ placeholder="输入待办事项..."
101
+ />
102
+ <button onClick={addTodo}>添加</button>
103
+ </div>
104
+ <ul className="todo-list">
105
+ {todos.map((todo, index) => (
106
+ <li key={index}>
107
+ <span>{todo}</span>
108
+ <button className="delete-btn" onClick={() => removeTodo(index)}>❌</button>
109
+ </li>
110
+ ))}
111
+ {todos.length === 0 && <li className="empty">暂无待办事项</li>}
112
+ </ul>
113
+ </div>
114
+
115
+ <p className="read-the-docs">
116
+ 本项目支持 Docker 部署到 Hugging Face Spaces
117
+ </p>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ export default App
src/assets/react.svg ADDED
src/index.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
src/vite-env.d.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface Window {
4
+ electronAPI?: {
5
+ getSystemInfo: () => Promise<{
6
+ platform: string
7
+ arch: string
8
+ node: string
9
+ electron: string
10
+ chrome: string
11
+ cpus: number
12
+ memory: string
13
+ }>
14
+ }
15
+ }
tsconfig.app.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
vite.config.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import electron from 'vite-plugin-electron/simple'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ electron({
10
+ main: {
11
+ // Shortcut of `build.lib.entry`
12
+ entry: 'electron/main.ts',
13
+ },
14
+ preload: {
15
+ // Shortcut of `build.rollupOptions.input`
16
+ input: 'electron/preload.ts',
17
+ },
18
+ // Optional: Use Node.js API in the Renderer-process
19
+ renderer: {},
20
+ }),
21
+ ],
22
+ })