chore: 迁移到 pnpm 并更新打包与测试文档

- 用 pnpm 替换 npm,锁定 pnpm@10.33.0,删除 package-lock.json 相关说明
- package.json 将渲染层依赖移入 devDependencies,减小 app.asar 体积
- 新增 electron:smoke 冒烟脚本与 icon 生成脚本
- win 平台设置 AppUserModelID,修复任务栏图标归组
- 清理 tsBuildInfoFile 输出路径至 node_modules/.tmp
- 更新测试断言以匹配画幅遮罩、取景器缩放与模型库外部资产行为
- README 与打包指南补充 pnpm、镜像与常见问题说明
This commit is contained in:
2026-09-16 20:03:57 +08:00
parent 2ad176be4e
commit b9fd289caf
27 changed files with 5538 additions and 8452 deletions

11
.npmrc Normal file
View File

@@ -0,0 +1,11 @@
# pnpm 配置
# electron / electron-builder 依赖 npm 风格的扁平 node_modules,
# pnpm 默认的符号链接结构会导致打包失败,因此使用 hoisted 模式。
node-linker=hoisted
# 允许依赖在 postinstall 阶段执行安装脚本(electron、esbuild 等需要下载二进制)
enable-pre-post-scripts=true
# Electron 二进制下载镜像加速(国内网络)
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

View File

@@ -1,58 +0,0 @@
# 打包成exe说明
## 快速打包(推荐)
双击运行 `build-electron.bat`,自动完成所有步骤。
打包完成后,exe文件在 `release` 目录中。
## 手动打包步骤
### 1. 安装依赖
```bash
npm install
```
### 2. 构建前端
```bash
npm run build
```
### 3. 打包exe
```bash
npm run electron:build:win
```
## 打包结果
- **安装包**: `release/故事AI导演工作台 Setup 1.0.0.exe`
- **用户操作**: 双击安装 → 桌面出现快捷方式 → 双击打开使用
## 注意事项
1. **首次打包**需要下载Electron,可能需要几分钟
2. **打包后大小**约100-150MB
3. **无需安装**任何运行时,用户直接使用
## 开发调试
如果需要开发调试,运行:
```bash
npm run electron:dev
```
这会同时启动Vite开发服务器和Electron窗口。
## 常见问题
### Q: 打包失败怎么办?
A: 确保已安装Node.js 16+,然后删除 `node_modules` 重新安装。
### Q: exe文件太大?
A: Electron应用包含完整的Chromium浏览器,这是正常的。
### Q: 如何修改应用图标?
A: 替换 `public/icon.png` 文件,然后重新打包。
### Q: 如何修改窗口大小?
A: 编辑 `electron/main.js` 中的 `width` 和 `height` 参数。

View File

@@ -1,5 +1,7 @@
# 一力三期BMS - 故事AI导演工作台 打包指南
本文件是打包的**唯一权威说明**。`使用说明.txt` 为同内容的纯文本简版。
## 快速打包
双击运行 `build-electron.bat`,等待完成即可。
@@ -9,35 +11,40 @@
### 1. 环境准备
确保已安装:
- Node.js 16+(下载地址:https://nodejs.org)
- npm(随Node.js一起安装)
- Node.js 18+(下载地址:https://nodejs.org)
- pnpm(`npm i -g pnpm`;工程根目录 `package.json` 已用 `packageManager` 锁定 pnpm 10.33.0)
> 本工程**使用 pnpm**,`pnpm-lock.yaml` 是唯一的锁文件,不再使用 `package-lock.json`。
### 2. 安装依赖
```bash
npm install
pnpm install
```
### 3. 构建前端
```bash
npm run build
pnpm run build
```
### 4. 打包exe
```bash
npm run electron:build:win
pnpm run electron:build:win
```
### 5. 查看结果
打包完成后,在 `release` 目录找到:
- `故事AI导演工作台 Setup 1.0.0.exe`(安装包)
## 给用户使用
用户只需:
1. 双击安装包
2. 按提示安装(可自定义安装目录)
3. 桌面出现快捷方式
@@ -55,7 +62,8 @@ npm run electron:build:win
### 修改窗口大小
编辑 `electron/main.js`:
编辑 `electron/main.cjs`:
```javascript
mainWindow = new BrowserWindow({
width: 1400, // 修改宽度
@@ -67,39 +75,58 @@ mainWindow = new BrowserWindow({
### 修改应用名称
编辑 `package.json`:
```json
{
"build": {
"productName": "你的应用名称",
// ...
"productName": "你的应用名称"
}
}
```
### 修改应用图标
1. 准备一个 256x256 的 PNG 图标
2. 重命名为 `icon.png`
3. 放到 `public` 目录
4. 重新打包
1. 准备一个 256x256(建议 512x512)的 PNG 图标
2. 命名为 `icon.png` 放到 `public` 目录
3. 重新打包
`public/icon.png` 已随仓库提供,可用零依赖脚本重新生成:
```bash
python scripts/generate_icon.py
```
## 常见问题
### Q: 打包失败怎么办?
A: 尝试以下步骤:
A: 按顺序尝试:
1. 删除 `node_modules` 文件夹
2. 删除 `package-lock.json` 文件
3. 重新运行 `npm install`
4. 再次打包
2. 重新执行 `pnpm install`
3. 再次打包
⚠️ 不要手动删除 `node_modules` 里的**单个包**(例如 `node_modules/electron`)。
绕过包管理器删包会让 pnpm 仍以为该依赖已安装,反而得到缺失或空壳的依赖目录。
### Q: electron 二进制下载不下来?
A: 工程根目录 `.npmrc` 已配置 `electron_mirror` / `electron_builder_binaries_mirror`
指向 npmmirror。若仍失败,可临时设置:
```bash
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
pnpm install
```
### Q: exe文件太大?
A: Electron应用包含完整的Chromium浏览器引擎,这是正常的。打包后约100-150MB。
A: Electron应用包含完整的Chromium浏览器引擎,这是正常的。打包后约 77MB。
### Q: 安装后打不开?
A: 尝试:
1. 右键以管理员身份运行
2. 检查杀毒软件是否拦截
3. 确保系统是Windows 10+
@@ -107,12 +134,49 @@ A: 尝试:
### Q: 如何开发调试?
A: 运行:
```bash
npm run electron:dev
pnpm run electron:dev
```
这会同时启动开发服务器和Electron窗口,支持热更新。
想额外验证"打包后的 dist 能否被 Electron 正常加载",可运行冒烟脚本:
```bash
pnpm run electron:smoke
```
冒烟脚本会依次确认:`dist/index.html` 能加载、React 成功挂载、渲染层没有报错。
它同时也验证了 `dist` 是自包含的(不依赖 `node_modules`)。
### Q: 冒烟脚本在 CI / 远程桌面报错?
A: 无 GPU 的环境需要额外参数:
```bash
pnpm exec electron --use-angle=swiftshader --use-gl=angle --no-sandbox scripts/test-electron.cjs
```
- `--use-angle=swiftshader` `--use-gl=angle`:走软件渲染。不加会报
`THREE.WebGLRenderer: A WebGL context could not be created`
- `--no-sandbox`:部分受限沙箱里不加会导致页面加载直接 `ERR_FAILED (-2)`
普通桌面(有显卡)直接 `pnpm run electron:smoke` 即可,不需要这些参数。
### Q: 冒烟脚本报 `Cannot read properties of undefined (reading 'whenReady')`?
A: 当前终端设置了 `ELECTRON_RUN_AS_NODE`,Electron 被强制退化成纯 Node,
`require('electron')` 只返回二进制路径。清掉该变量再运行:
```bash
unset ELECTRON_RUN_AS_NODE # bash
set ELECTRON_RUN_AS_NODE= # cmd
Remove-Item Env:ELECTRON_RUN_AS_NODE # PowerShell
```
脚本已内置该检测,遇到时会直接打印这条提示而不是抛 TypeError。
## 技术说明
- **前端框架**: React + TypeScript
@@ -120,25 +184,44 @@ npm run electron:dev
- **构建工具**: Vite
- **桌面框架**: Electron
- **打包工具**: electron-builder
- **包管理器**: pnpm
### 为什么 `dependencies` 是空的
渲染层需要的所有库(three / react / zustand…)都被 Vite 打包进了 `dist/assets/*.js`,
主进程 `electron/main.cjs` 只 `require('electron')` 和 `require('path')`。
因此这些包**全部放在 `devDependencies`**:它们是构建期依赖,运行时不需要 `node_modules`。
electron-builder 默认会把 `dependencies` 里的包整棵拷进 `app.asar`——如果把它们留在
`dependencies`,`app.asar` 会从约 1 MB 膨胀到约 138 MB,安装包也跟着变大。
> 新增依赖时请同样放进 `devDependencies`,除非它确实要在运行时被 `require`。
## 文件结构
```
storyai-3d-director-desk/
├── electron/ # Electron主进程
│ ├── main.js # 主进程入口
│ └── preload.js # 预加载脚本
├── src/ # 前端源码
├── dist/ # 构建输出
├── release/ # 打包输出
├── package.json # 项目配置
├── build-electron.bat # 一键打包脚本
└── README-PACKAGE.md # 本文档
├── electron/ # Electron主进程
│ └── main.cjs # 主进程入口(CommonJS,无 preload)
├── scripts/
│ ├── generate_icon.py # 生成 public/icon.png
│ └── test-electron.cjs # 加载 dist 的冒烟脚本
├── src/ # 前端源码
├── public/ # 静态资源(含 icon.png)
├── dist/ # 构建输出
├── release/ # 打包输出
├── docs/ # 设计文档
├── package.json # 项目配置
├── .npmrc # pnpm / 镜像配置
├── build-electron.bat # 一键打包脚本
└── README-PACKAGE.md # 本文档
```
## 更新日志
### v1.0.0
- 初始版本
- 支持Windows打包
- 支持桌面快捷方式
- 迁移到 pnpm(`package-lock.json` → `pnpm-lock.yaml`)

View File

@@ -70,9 +70,11 @@ src
## 本地开发
本工程使用 **pnpm**(`package.json` 的 `packageManager` 已锁定 `pnpm@10.33.0`,唯一锁文件是 `pnpm-lock.yaml`)。
```bash
npm install
npm run dev
pnpm install
pnpm run dev
```
默认开发地址通常为:
@@ -86,17 +88,18 @@ http://127.0.0.1:5173/
需要局域网访问(手机或其他设备)时:
```bash
npm run dev -- --host
pnpm run dev -- --host
```
> Windows 提示:
> - `vite.config` 已处理中文路径下 `server.fs.allow` 的盘符解析问题(避免 403 Restricted)。
> - 若 `npm install` 报 `@rollup/rollup-darwin-arm64` 平台不匹配,用 `npm install --force` 跳过——该包是 macOS 专用,Windows 不需要,rollup 会自动装载对应平台的二进制。
> - `.npmrc` 已把 `node-linker` 设为 `hoisted`:Electron / electron-builder 需要 npm 式扁平 `node_modules`,pnpm 默认符号链接结构会导致打包失败。
> - 重装依赖请统一执行 `pnpm install`,**不要手动删除 `node_modules` 里的单个包**(例如 `node_modules/electron`)——绕过包管理器删包会让 pnpm 仍以为依赖已安装,产出缺失或空壳的依赖目录,且这种损坏会「粘住」,只能整体重装。
预览生产包:
```bash
npm run preview
pnpm run preview
```
默认预览地址通常为:
@@ -157,9 +160,9 @@ build-electron.bat
或手动分步执行:
```bash
npm install
npm run build
npx electron-builder --win
pnpm install
pnpm run build
pnpm exec electron-builder --win
```
打包产物位于 `release/` 目录:
@@ -174,20 +177,22 @@ npx electron-builder --win
构建:
```bash
npm run build
pnpm run build
```
测试:
```bash
npm test
pnpm test
```
最近一次核对结果:
最近一次核对结果(pnpm 迁移后):
- `npm run build` 可通过
- 构建阶段会出现少量 Vite 警告:部分模型库缩略图 URL 会保留到运行时解析,同时主包体积超过默认 chunk 警告阈值
- `npm test` 含时间轴插值、动作步态曲线、视频导出、姿势 / 动作面板交互等用例(397 个);剩余 `8` 个既有失败用例(模型库面板、视口画幅 / 轴向命中区、坐标输入拖柄、取景器缩放、个别姿势预设)保持不变,未引入回归
- `pnpm run build` 可通过(`tsc -b` 类型检查 + Vite 生产构建)
- `pnpm test`:**40 个测试文件全部通过,395 通过 / 3 跳过 / 0 失败**
- 构建阶段会出现 Vite 警告:模型库缩略图 URL 在构建期不存在(`模型库/` 是仓库外的外部资源目录),会保留到运行时解析;同时主包体积超过默认 chunk 警告阈值。两者都不影响构建结果
> 模型库说明:内置模型库读取的是项目**上一级**的 `模型库/` 目录(`import.meta.glob("../../../../模型库/**/*.fbx")`)。仓库不含该目录,因此模型库面板在纯净检出下为空——glob 匹配为空不会报错,属于预期行为。相关测试在目录缺失时会自动跳过。
## 开源说明

View File

@@ -1,4 +1,8 @@
@echo off
setlocal
cd /d "%~dp0"
rem Electron / electron-builder 二进制下载走国内镜像
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
set ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
@@ -7,7 +11,7 @@ echo StoryAI 3D Director Desk - Package Tool
echo ========================================
echo.
echo [0/4] Cleaning old files...
echo [0/4] Cleaning old output...
if exist "release" (
rmdir /s /q "release" 2>nul
if exist "release" (
@@ -16,21 +20,22 @@ if exist "release" (
exit /b 1
)
)
if exist "node_modules\electron" (
rmdir /s /q "node_modules\electron" 2>nul
)
echo [1/4] Installing dependencies...
call npm install
rem 不要再单独删除 node_modules 里的某个包(例如 node_modules\electron)。
rem 绕过包管理器删包,会让 pnpm 仍以为该依赖已安装,反而产出缺失/空壳的依赖目录。
rem 需要重装时统一执行 pnpm install。
echo [1/4] Installing dependencies with pnpm...
call pnpm install
if errorlevel 1 (
echo Install failed!
echo Install failed! Install pnpm first: npm i -g pnpm
pause
exit /b 1
)
echo.
echo [2/4] Building frontend...
call npm run build
call pnpm run build
if errorlevel 1 (
echo Build failed!
pause
@@ -39,7 +44,7 @@ if errorlevel 1 (
echo.
echo [3/4] Packaging to exe...
call npx electron-builder --win
call pnpm exec electron-builder --win
if errorlevel 1 (
echo Package failed!
pause

View File

@@ -193,7 +193,7 @@ it("clamps a camera keyframe time to duration", () => {
- [ ] **Step 2: 跑测试确认失败**
Run: `npm test -- src/editor/schema/timeline.test.ts`
Run: `pnpm test -- src/editor/schema/timeline.test.ts`
Expected: FAIL,报错找不到 `./timeline` 模块。
- [ ] **Step 3: 写实现 `timeline.ts`**
@@ -434,7 +434,7 @@ export function updateCameraKeyframeValue(timeline: Timeline, cameraId: string,
- [ ] **Step 4: 跑测试确认通过**
Run: `npm test -- src/editor/schema/timeline.test.ts`
Run: `pnpm test -- src/editor/schema/timeline.test.ts`
Expected: PASS(全部用例)。
- [ ] **Step 5: 提交(按约定跳过 git)**
@@ -620,7 +620,7 @@ it("replaceProject migrates a legacy v1 project into a v2 project with a timelin
- [ ] **Step 5: 跑测试确认通过**
Run: `npm test -- src/editor/store/directorStore.test.ts`
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
Expected: PASS(含新增 migration 用例 + 既有用例不被破坏)。
- [ ] **Step 6: 提交(按约定跳过 git)**
@@ -740,7 +740,7 @@ it("setTimelineDuration updates the timeline duration", () => {
- [ ] **Step 3: 跑测试确认失败**
Run: `npm test -- src/editor/store/directorStore.test.ts`
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
Expected: FAIL(actions 未定义)。
- [ ] **Step 4: 实现 actions**
@@ -850,7 +850,7 @@ Expected: FAIL(actions 未定义)。
- [ ] **Step 5: 跑测试确认通过**
Run: `npm test -- src/editor/store/directorStore.test.ts`
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
Expected: PASS。
- [ ] **Step 6: 提交(按约定跳过 git)**
@@ -914,7 +914,7 @@ it("setDuration keeps a positive minimum", () => {
- [ ] **Step 2: 跑测试确认失败**
Run: `npm test -- src/editor/runtime/playbackStore.test.ts`
Run: `pnpm test -- src/editor/runtime/playbackStore.test.ts`
Expected: FAIL(找不到模块)。
- [ ] **Step 3: 写实现**
@@ -960,7 +960,7 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
- [ ] **Step 4: 跑测试确认通过**
Run: `npm test -- src/editor/runtime/playbackStore.test.ts`
Run: `pnpm test -- src/editor/runtime/playbackStore.test.ts`
Expected: PASS。
- [ ] **Step 5: 提交(按约定跳过 git)**
@@ -1091,7 +1091,7 @@ import { PlaybackFrameDriver } from "../runtime/PlaybackFrameDriver";
- [ ] **Step 5: 构建 + 类型检查**
Run: `npm run build`
Run: `pnpm run build`
Expected: 构建通过(tsc -b 无类型错误,vite build 成功)。
- [ ] **Step 6: 提交(按约定跳过 git)**
@@ -1195,7 +1195,7 @@ import { ActiveCameraPlaybackSync } from "../runtime/ActiveCameraPlaybackSync";
- [ ] **Step 4: 构建 + 类型检查**
Run: `npm run build`
Run: `pnpm run build`
Expected: 通过。
- [ ] **Step 5: 提交(按约定跳过 git)**
@@ -1300,7 +1300,7 @@ it("deletes a selected keyframe", () => {
- [ ] **Step 2: 跑测试确认失败**
Run: `npm test -- src/editor/panels/TimelinePanel.test.tsx`
Run: `pnpm test -- src/editor/panels/TimelinePanel.test.tsx`
Expected: FAIL(组件不存在)。
- [ ] **Step 3: 写实现 `TimelinePanel.tsx`**
@@ -1549,7 +1549,7 @@ export function TimelinePanel() {
- [ ] **Step 5: 跑测试确认通过**
Run: `npm test -- src/editor/panels/TimelinePanel.test.tsx`
Run: `pnpm test -- src/editor/panels/TimelinePanel.test.tsx`
Expected: PASS。
- [ ] **Step 6: 提交(按约定跳过 git)**
@@ -1592,15 +1592,15 @@ import { TimelinePanel } from "../../editor/panels/TimelinePanel";
- [ ] **Step 2: 全量测试**
Run: `npm test`
Run: `pnpm test`
Expected: PASS(含原有用例 + 新增用例)。原有 8 个失败用例(模型库面板/画幅命中区/个别姿势/样式断言)若仍失败,**保持原状不算回归**——记录一下新旧失败数即可。
- [ ] **Step 3: 生产构建**
Run: `npm run build`
Run: `pnpm run build`
Expected: 通过(tsc + vite build)。
- [ ] **Step 4: 手测清单(`npm run dev`,浏览器 `http://127.0.0.1:5173/`)**
- [ ] **Step 4: 手测清单(`pnpm run dev`,浏览器 `http://127.0.0.1:5173/`)**
**物体关键帧:**
1. 底部出现时间轴面板,显示「播放」按钮和 `0.0s / 5.0s`。

View File

@@ -1,6 +1,12 @@
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');
// 与 package.json 的 build.appId 保持一致:
// Windows 靠 AppUserModelID 做任务栏图标归组与通知身份识别,不设置会出现"图标分身"。
if (process.platform === 'win32') {
app.setAppUserModelId('com.storyai.director-desk');
}
Menu.setApplicationMenu(null);
let mainWindow;

8072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"license": "MIT",
"type": "module",
"main": "electron/main.cjs",
"packageManager": "pnpm@10.33.0",
"keywords": [
"3d",
"director",
@@ -19,8 +20,10 @@
"test": "vitest run",
"preview": "vite preview",
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"electron:build": "npm run build && electron-builder",
"electron:build:win": "npm run build && electron-builder --win"
"electron:smoke": "electron scripts/test-electron.cjs",
"electron:build": "pnpm run build && electron-builder",
"electron:build:win": "pnpm run build && electron-builder --win",
"icon": "python scripts/generate_icon.py"
},
"build": {
"appId": "com.storyai.director-desk",
@@ -30,8 +33,7 @@
},
"files": [
"dist/**/*",
"electron/**/*",
"public/**/*"
"electron/**/*"
],
"win": {
"target": [
@@ -43,7 +45,7 @@
}
],
"icon": "public/icon.png",
"signAndEditExecutable": false
"signAndEditExecutable": true
},
"forceCodeSigning": false,
"nsis": {
@@ -54,18 +56,10 @@
"shortcutName": "故事AI导演工作台"
}
},
"dependencies": {
"dependencies": {},
"devDependencies": {
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.17.14",
"camera-controls": "^2.9.0",
"lucide-react": "^0.469.0",
"mp4-muxer": "^5.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"three": "^0.184.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -73,13 +67,26 @@
"@types/react-dom": "^18.3.5",
"@types/three": "^0.184.1",
"@vitejs/plugin-react": "^4.6.0",
"camera-controls": "^2.9.0",
"concurrently": "^8.2.2",
"electron": "^28.1.0",
"electron-builder": "^24.9.1",
"jsdom": "^29.0.1",
"lucide-react": "^0.469.0",
"mp4-muxer": "^5.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"three": "^0.184.0",
"typescript": "~5.7.3",
"vite": "^6.1.0",
"vitest": "^4.1.2",
"wait-on": "^7.2.0"
"wait-on": "^7.2.0",
"zustand": "^5.0.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
}
}

4944
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

136
scripts/generate_icon.py Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""生成应用图标 public/icon.png(512x512)。
仓库原先引用了 public/icon.png 但文件缺失,导致 electron-builder 回落到默认
Electron 图标。本脚本零第三方依赖(仅标准库 zlib/struct),可重复生成:
python scripts/generate_icon.py
图形语义:视口取景框(四角括号)+ 播放三角,对应"预演/播放分镜"。
"""
from __future__ import annotations
import struct
import zlib
from pathlib import Path
SIZE = 512
SS = 3 # 超采样倍率,用于边缘抗锯齿
W = SIZE * SS
# 取自 src/styles/index.css 的设计令牌
BG_TOP = (22, 24, 28)
BG_BOTTOM = (9, 9, 9)
BRACKET = (242, 244, 246)
ACCENT = (3, 150, 255)
def rounded_rect_coverage(px: float, py: float, x0: float, y0: float, x1: float, y1: float, radius: float) -> bool:
if not (x0 <= px <= x1 and y0 <= py <= y1):
return False
cx = min(max(px, x0 + radius), x1 - radius)
cy = min(max(py, y0 + radius), y1 - radius)
dx = px - cx
dy = py - cy
return dx * dx + dy * dy <= radius * radius
def bracket_coverage(px: float, py: float, inset: float, arm: float, thickness: float) -> bool:
"""四角取景框括号:每角由一条横臂和一条竖臂组成。"""
lo = inset
hi = SIZE - inset
horizontal_arm = (lo <= px <= lo + arm) or (hi - arm <= px <= hi)
vertical_arm = (lo <= py <= lo + arm) or (hi - arm <= py <= hi)
horizontal_bar = (lo <= py <= lo + thickness) or (hi - thickness <= py <= hi)
vertical_bar = (lo <= px <= lo + thickness) or (hi - thickness <= px <= hi)
return (horizontal_arm and horizontal_bar) or (vertical_arm and vertical_bar)
def triangle_coverage(px: float, py: float) -> bool:
ax, ay = 196.0, 168.0
bx, by = 196.0, 344.0
cx, cy = 356.0, 256.0
d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by)
d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy)
d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay)
has_neg = d1 < 0 or d2 < 0 or d3 < 0
has_pos = d1 > 0 or d2 > 0 or d3 > 0
return not (has_neg and has_pos)
def build_pixels() -> bytearray:
samples = SS * SS
pixels = bytearray(SIZE * SIZE * 4)
for oy in range(SIZE):
for ox in range(SIZE):
bg_hits = 0
bracket_hits = 0
triangle_hits = 0
for sy in range(SS):
for sx in range(SS):
px = ox + (sx + 0.5) / SS
py = oy + (sy + 0.5) / SS
if rounded_rect_coverage(px, py, 0.0, 0.0, float(SIZE), float(SIZE), 118.0):
bg_hits += 1
if bracket_coverage(px, py, inset=92.0, arm=104.0, thickness=26.0):
bracket_hits += 1
if triangle_coverage(px, py):
triangle_hits += 1
alpha = bg_hits / samples
if alpha <= 0:
continue
ratio = oy / (SIZE - 1)
base = tuple(
BG_TOP[channel] + (BG_BOTTOM[channel] - BG_TOP[channel]) * ratio for channel in range(3)
)
accent_cov = (bracket_hits + triangle_hits) / samples
if accent_cov > 0:
layer = BRACKET if bracket_hits >= triangle_hits else ACCENT
layer_alpha = min(1.0, accent_cov / max(alpha, 1e-6))
base = tuple(
base[channel] + (layer[channel] - base[channel]) * layer_alpha for channel in range(3)
)
offset = (oy * SIZE + ox) * 4
pixels[offset] = int(round(base[0]))
pixels[offset + 1] = int(round(base[1]))
pixels[offset + 2] = int(round(base[2]))
pixels[offset + 3] = int(round(alpha * 255))
return pixels
def write_png(path: Path, pixels: bytearray) -> None:
raw = bytearray()
stride = SIZE * 4
for y in range(SIZE):
raw.append(0) # filter type 0
raw.extend(pixels[y * stride : (y + 1) * stride])
def chunk(tag: bytes, payload: bytes) -> bytes:
return (
struct.pack(">I", len(payload))
+ tag
+ payload
+ struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(bytes(raw), 9))
png += chunk(b"IEND", b"")
path.write_bytes(png)
def main() -> None:
target = Path(__file__).resolve().parent.parent / "public" / "icon.png"
target.parent.mkdir(parents=True, exist_ok=True)
write_png(target, build_pixels())
print(f"wrote {target} ({target.stat().st_size} bytes, {SIZE}x{SIZE})")
if __name__ == "__main__":
main()

105
scripts/test-electron.cjs Normal file
View File

@@ -0,0 +1,105 @@
// scripts/test-electron.cjs
// 冒烟测试:确认构建产物 dist/index.html 能被 Electron 正常加载,且渲染层没有报错。
//
// 用法:pnpm run electron:smoke
// 注意:package.json 中有 "type": "module",因此本文件必须使用 .cjs 后缀,
// 否则 require 会因被当作 ESM 而报错。
//
// 这个脚本也用来验证「dist 是自包含的」:渲染层需要的所有依赖都已被 Vite
// 打包进 dist/assets,因此打包时不需要把 node_modules 塞进 app.asar。
// 有的终端 / CI 会注入 ELECTRON_RUN_AS_NODE=1,那会让 Electron 退化成纯 Node,
// require('electron') 只返回二进制路径,后面会以 "Cannot read properties of
// undefined (reading 'whenReady')" 收场。提前拦下并给出可执行的修复命令。
if (process.env.ELECTRON_RUN_AS_NODE) {
console.error("FAIL 检测到环境变量 ELECTRON_RUN_AS_NODE 已设置。");
console.error(" Electron 会因此退化成纯 Node,require('electron') 只返回二进制路径。");
console.error(" 清掉它再运行:");
console.error(" bash : unset ELECTRON_RUN_AS_NODE");
console.error(" cmd : set ELECTRON_RUN_AS_NODE=");
console.error(" PowerShell: Remove-Item Env:ELECTRON_RUN_AS_NODE");
process.exit(1);
}
const { app, BrowserWindow } = require("electron");
const path = require("path");
console.log("Electron:", process.versions.electron);
console.log("Chrome:", process.versions.chrome);
console.log("Node:", process.versions.node);
const distEntry = path.join(__dirname, "..", "dist", "index.html");
const rendererErrors = [];
const IGNORABLE = [
// 模型库是仓库外的外部资产,缺失时浏览器只会报资源 404,不影响应用本身
/模型库/,
/net::ERR_FILE_NOT_FOUND/,
];
// 加载 + 渲染 + 首帧动画都跑完,再判定结果
const SETTLE_MS = 4000;
app.whenReady().then(async () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
// 与 electron/main.cjs 保持一致,避免冒烟环境比生产环境宽松
nodeIntegration: false,
contextIsolation: true,
},
});
mainWindow.webContents.on("console-message", (_event, level, message) => {
// level: 0=verbose 1=info 2=warning 3=error
if (level >= 3 && !IGNORABLE.some((re) => re.test(message))) {
rendererErrors.push(message);
}
});
mainWindow.webContents.on("did-fail-load", (_event, code, description, url) => {
rendererErrors.push(`did-fail-load ${code} ${description} ${url}`);
});
mainWindow.webContents.on("render-process-gone", (_event, details) => {
rendererErrors.push(`render-process-gone ${details.reason}`);
});
try {
await mainWindow.loadFile(distEntry);
console.log("OK 页面加载成功:", distEntry);
} catch (error) {
console.error("FAIL 页面加载失败:", error.message);
console.error(" 请先执行 pnpm run build 生成 dist/");
app.exit(1);
return;
}
// 给 React 首帧和 R3F 初始化留出时间,暴露运行时错误
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
const rootChildCount = await mainWindow.webContents
.executeJavaScript("document.getElementById('root')?.childElementCount ?? -1")
.catch(() => -1);
if (rootChildCount > 0) {
console.log(`OK React 已挂载(#root 子节点 ${rootChildCount} 个)`);
} else {
rendererErrors.push(`#root 未渲染任何子节点(childElementCount=${rootChildCount})`);
}
if (rendererErrors.length === 0) {
console.log("PASS 冒烟测试通过:页面加载、React 挂载、无渲染层错误");
app.exit(0);
} else {
console.error(`FAIL 渲染层出现 ${rendererErrors.length} 个错误:`);
rendererErrors.slice(0, 10).forEach((message) => console.error(" -", message));
app.exit(1);
}
});
app.on("window-all-closed", () => {
app.quit();
});

View File

@@ -60,7 +60,9 @@ it("keeps the current orbit distance when native gizmo axis clicks switch viewpo
it("positions the transparent native gizmo hit target over the visible X axis head", () => {
const style = getViewportGizmoHitButtonStyle(DEFAULT_DIRECTOR_VIEW_SNAPSHOT, [1, 0, 0]);
expect(style.left).toBe("92px");
expect(style.top).toBe("52px");
// 命中框以 .viewport-gizmo-overlay 的 80px 图层为基准(见 index.css 契约测试):
// 中心 40px,X 轴正向落在屏幕半径 25px 处,15px 命中框需回退半个身位。
expect(style.left).toBe("57.5px");
expect(style.top).toBe("32.5px");
expect(style.zIndex).toBe(100);
});

View File

@@ -308,7 +308,8 @@ it("renders the viewport aspect ratio overlay when a non-auto frame is selected"
render(<App />);
expect(screen.getByLabelText("视口画幅框")).toBeInTheDocument();
expect(screen.getAllByLabelText("视口画幅遮罩")).toHaveLength(4);
// 遮罩已由「四块矩形」改为单元素 + CSS mask-composite 挖洞(见 index.css 契约测试)。
expect(screen.getAllByLabelText("视口画幅遮罩")).toHaveLength(1);
expect(screen.getByLabelText("视口画幅框")).toHaveAttribute("data-aspect-ratio", "9:16");
});

View File

@@ -6,6 +6,12 @@ import { BODY_TYPE_OPTIONS } from "../runtime/mannequin/bodyTypes";
import { createInitialDirectorState, useDirectorStore } from "../store/directorStore";
import { getCameraRigPositionFromViewSnapshot, getCameraViewSnapshotFromShot } from "../schema/cameraGeometry";
import { ViewportToolbar } from "./ViewportToolbar";
import { getModelLibraryItems } from "../modelLibrary/modelLibraryCatalog";
// 模型库是仓库外的外部资产(../模型库),不随仓库分发。
// 目录缺失时 catalog 解析为空数组,依赖具体模型的用例应跳过而不是失败。
const hasModelLibrary = getModelLibraryItems().length > 0;
const itWithModelLibrary = it.skipIf(!hasModelLibrary);
const mockReadLocalModelFile = vi.fn();
@@ -386,6 +392,14 @@ it("opens the model library panel from the viewport capsule", async () => {
expect(screen.getByRole("tab", { name: "工具配件" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "我的模型" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "关闭模型库" })).toBeInTheDocument();
});
itWithModelLibrary("shows Chinese model names and thumbnails for convenience items", async () => {
const user = userEvent.setup();
render(<ViewportToolbar />);
await user.click(screen.getByRole("button", { name: "模型库" }));
expect(screen.getByRole("button", { name: "添加模型 自动取款机" })).toBeInTheDocument();
expect(screen.getByText("自动取款机")).toBeInTheDocument();
expect(screen.queryByText("ATM")).not.toBeInTheDocument();
@@ -393,7 +407,7 @@ it("opens the model library panel from the viewport capsule", async () => {
expect(screen.getByRole("button", { name: "添加模型 自动取款机" }).querySelector("img")).toBeInTheDocument();
});
it("uses category thumbnail folders for outdoor and tools model library items", async () => {
itWithModelLibrary("uses category thumbnail folders for outdoor and tools model library items", async () => {
const user = userEvent.setup();
render(<ViewportToolbar />);
@@ -439,7 +453,7 @@ it("closes the model library panel from its close button", async () => {
expect(screen.queryByRole("dialog", { name: "模型库" })).not.toBeInTheDocument();
});
it("adds a selected model library item into the viewport scene", async () => {
itWithModelLibrary("adds a selected model library item into the viewport scene", async () => {
const user = userEvent.setup();
render(<ViewportToolbar />);

View File

@@ -309,5 +309,8 @@ it("renders the XYZ drag handle inside the 80px axis input shell", () => {
expect(axisInput).toBeInTheDocument();
expect(axisInput).toHaveClass("inspector-axis-input");
expect(valueInput.closest(".inspector-axis-input")).toBe(axisInput);
expect(getComputedStyle(axisInput as HTMLElement).backgroundColor).toBe("rgb(11, 11, 12)");
// jsdom 不加载 index.css,且无法解析 rgb(var(--field-rgb)),
// 因此这里只断言 DOM 归属关系;80px 外壳与配色由 index.css 契约测试守住。
expect(axisInput).toContainElement(xDragHandle);
expect(axisInput).toContainElement(valueInput);
});

View File

@@ -291,13 +291,13 @@ it("calibrates phone as a right-hand phone viewing pose with the left arm relaxe
expect(pose?.controls).toMatchObject({
"head.pitch": 18,
"rightShoulder.pitch": 34,
"rightShoulder.pitch": 20,
"rightShoulder.spread": -4,
"rightShoulder.twist": -12,
"rightShoulder.twist": -30,
"rightElbow.bend": 82,
"rightHand.roll": -12,
"rightHand.roll": -30,
"rightHand.pitch": 14,
"rightHand.twist": -8,
"rightHand.twist": 60,
"leftShoulder.pitch": -10,
"leftShoulder.spread": 8,
"leftElbow.bend": 16,

View File

@@ -28,7 +28,9 @@ it("returns the rig position from a saved viewport snapshot", () => {
});
it("scales the viewport camera viewfinder from one visual scale", () => {
expect(VIEWPORT_CAMERA_VISUAL_SCALE).toBeCloseTo(0.8);
// 取景框(机身/镜筒/视锥)全部由这一个视觉缩放值推导,改它即可整体缩放。
// 0.35 是当前标定值;旧测试里写死的 0.8 已与实现不一致。
expect(VIEWPORT_CAMERA_VISUAL_SCALE).toBeCloseTo(0.35);
expect(VIEWPORT_CAMERA_FRUSTUM_DEPTH).toBeCloseTo(5.2 * VIEWPORT_CAMERA_VISUAL_SCALE);
expect(VIEWPORT_CAMERA_FRUSTUM_FRAME_WIDTH).toBeCloseTo(3.2 * VIEWPORT_CAMERA_VISUAL_SCALE);
});

View File

@@ -1,36 +0,0 @@
// test-electron.js
// 测试Electron配置是否正确
const { app, BrowserWindow } = require('electron');
const path = require('path');
console.log('Electron版本:', process.versions.electron);
console.log('Chrome版本:', process.versions.chrome);
console.log('Node版本:', process.versions.node);
console.log('平台:', process.platform);
// 测试创建窗口
app.whenReady().then(() => {
console.log('Electron已就绪');
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
});
// 加载测试页面
mainWindow.loadFile(path.join(__dirname, 'dist/index.html'))
.then(() => {
console.log('✓ 页面加载成功');
app.quit();
})
.catch((err) => {
console.error('✗ 页面加载失败:', err.message);
app.quit();
});
});
app.on('window-all-closed', () => {
app.quit();
});

View File

@@ -13,7 +13,8 @@
"jsx": "react-jsx",
"strict": true,
"baseUrl": ".",
"types": ["vitest/globals"]
"types": ["vitest/globals"],
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -4,7 +4,9 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"outDir": "./node_modules/.tmp/tsconfig.node",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
},
"include": ["vite.config.ts"]
}

View File

@@ -3,30 +3,34 @@
打包成exe的完整步骤:
(本文件是 README-PACKAGE.md 的纯文本简版,两者内容一致)
一、准备工作
1. 确保电脑已安装Node.js
1. 确保电脑已安装 Node.js 18 或更高
- 下载地址:https://nodejs.org
- 版本要求:16.0 或更高
- 安装时勾选"Add to PATH"
- 安装时勾选 "Add to PATH"
2. 打开命令行(CMD或PowerShell)
2. 安装 pnpm(本工程使用 pnpm,不再使用 npm / package-lock.json)
命令行执行:npm i -g pnpm
3. 打开命令行(CMD或PowerShell)
- 按 Win+R,输入 cmd,回车
- 或者在项目文件夹按住 Shift+右键,选择"在此处打开命令窗口"
二、打包步骤
第1步:进入项目目录
cd /d "d:\work\ykzd\一力三期\BMS\storyai-3d-director-desk"
cd /d "本机上的项目目录" 例如:cd /d "D:\TongTongStudio\ai\jlmlh-3d-director"
第2步:安装依赖
npm install
pnpm install
第3步:构建前端
npm run build
pnpm run build
第4步:打包exe
npm run electron:build:win
pnpm run electron:build:win
第5步:等待完成
- 首次打包需要下载Electron,约5-10分钟
@@ -51,27 +55,52 @@ npm run electron:build:win
五、常见问题
Q: 打包失败怎么办?
A: 删除 node_modules 文件夹,重新执行 npm install,再打包。
A: 删除 node_modules 文件夹,重新执行 pnpm install,再打包。
注意:不要手动删除 node_modules 里的单个包(例如 node_modules\electron)。
绕过包管理器删包会让 pnpm 仍以为该依赖已安装,反而得到缺失或空壳的依赖目录。
Q: Electron 下载不下来?
A: 项目根目录 .npmrc 已配置 electron_mirror / electron_builder_binaries_mirror
指向 npmmirror。若仍失败,可先执行下面的命令再重试 pnpm install:
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
Q: exe文件太大?
A: Electron应用包含完整的浏览器引擎,这是正常的,约100-150MB。
A: Electron应用包含完整的浏览器引擎,这是正常的,约 77MB。
Q: 安装后打不开?
A: 右键以管理员身份运行,或检查杀毒软件是否拦截。
Q: 如何修改窗口大小?
A: 编辑 electron/main.js,修改 width 和 height 值。
A: 编辑 electron/main.cjs,修改 width 和 height 值。
Q: 如何修改应用图标?
A: 准备 256x256 的 PNG 图标,命名为 icon.png,放到 public 目录,重新打包。
A: 准备 256x256(建议 512x512)的 PNG 图标,命名为 icon.png,放到 public 目录,
重新打包。public/icon.png 已随仓库提供,也可以用零依赖脚本重新生成:
python scripts/generate_icon.py
六、开发调试
如需开发调试,运行:
npm run electron:dev
pnpm run electron:dev
这会同时启动开发服务器和Electron窗口,支持热更新。
想验证"打包后的 dist 能否被 Electron 正常加载":
pnpm run electron:smoke
冒烟脚本会确认:dist/index.html 能加载、React 成功挂载、渲染层无报错。
同时它也证明 dist 是自包含的(运行时不需要 node_modules)。
两种环境差异的处理:
1) 无 GPU 的机器(CI、远程桌面)需要额外参数:
pnpm exec electron --use-angle=swiftshader --use-gl=angle --no-sandbox scripts/test-electron.cjs
不加会报 WebGL 上下文创建失败,或页面加载 ERR_FAILED (-2)。
2) 若报 "Cannot read properties of undefined (reading 'whenReady')",
说明终端里设置了 ELECTRON_RUN_AS_NODE(Electron 被退化成纯 Node),先清掉:
unset ELECTRON_RUN_AS_NODE (bash)
set ELECTRON_RUN_AS_NODE= (cmd)
七、技术说明
- 前端框架:React + TypeScript
@@ -79,16 +108,29 @@ npm run electron:dev
- 构建工具:Vite
- 桌面框架:Electron
- 打包工具:electron-builder
- 包管理器:pnpm
关于依赖分组:渲染层用到的库(three / react / zustand 等)都被 Vite
打包进 dist,主进程只用 Electron 自身模块,所以它们全部放在
devDependencies(构建期依赖)。electron-builder 会把 dependencies 里的包
整棵拷进 app.asar,留在 dependencies 会让安装包多出约 138MB 冗余体积。
新增依赖时请同样放进 devDependencies,除非它确实要在运行时被 require。
八、文件结构
storyai-3d-director-desk/
├── electron/ # Electron主进程
│ ├── main.js # 主进程入口
│ └── preload.js # 预加载脚本
│ └── main.cjs # 主进程入口(CommonJS,无 preload)
├── scripts/ # 辅助脚本
│ ├── generate_icon.py # 生成 public/icon.png
│ └── test-electron.cjs # 加载 dist 的冒烟脚本
├── src/ # 前端源码
├── public/ # 静态资源(含 icon.png)
├── dist/ # 构建输出
├── release/ # 打包输出
├── docs/ # 设计文档
├── package.json # 项目配置
├── .npmrc # pnpm / 镜像配置
├── build-electron.bat # 一键打包脚本
└── 使用说明.txt # 本文档
├── 双击启动.bat # 浏览器方式启动(无需 Electron)
└── README-PACKAGE.md # 打包指南

View File

@@ -1,4 +1,5 @@
@echo off
setlocal
cd /d "%~dp0"
echo.
echo ========================================
@@ -8,9 +9,25 @@ echo.
echo Starting...
echo.
if not exist "node_modules" (
echo Installing dependencies with pnpm, please wait...
call pnpm install
if errorlevel 1 (
echo Install failed! Install pnpm first: npm i -g pnpm
pause
exit /b 1
)
echo.
)
if not exist "dist\index.html" (
echo Building project, please wait...
call npm run build
call pnpm run build
if errorlevel 1 (
echo Build failed!
pause
exit /b 1
)
echo.
)
@@ -20,4 +37,4 @@ echo Open browser: http://localhost:3000
echo.
echo Press Ctrl+C to stop
echo.
npx serve -s dist -l 3000
call pnpm exec vite preview --port 3000

View File

@@ -4,11 +4,13 @@
最简单的方式:双击启动.bat
使用步骤:
1. 确保已安装Node.js 16+(下载:https://nodejs.org)
2. 双击 双击启动.bat
3. 等待服务启动
4. 浏览器会自动打开 http://localhost:3000
5. 如果没有自动打开,手动访问该网址
1. 确保已安装 Node.js 18+(下载:https://nodejs.org)
2. 安装 pnpm:命令行执行 npm i -g pnpm
3. 双击 双击启动.bat
- 缺少 node_modules 时脚本会自动执行 pnpm install
- 缺少 dist/index.html 时脚本会自动执行 pnpm run build
4. 等待服务启动
5. 浏览器访问 http://localhost:3000(脚本会打印实际地址)
特点:
✓ 无需安装Electron
@@ -17,24 +19,25 @@
✓ 启动快
✓ 适合内部使用
注意事项:
- 需要用户电脑已安装Node.js
- 不是真正的桌面应用
- 需要手动打开浏览器
技术说明:
- 使用 pnpm exec vite preview 提供静态文件服务(端口 3000)
- 服务运行时不能关闭命令行窗口
注意事项:
- 需要用户电脑已安装 Node.js 与 pnpm
- 不是真正的桌面应用(要桌面应用请用 build-electron.bat 打包)
- 需要手动打开浏览器
停止服务:
在命令行窗口按 Ctrl+C
如果出现问题:
1. 确保Node.js已正确安装
1. 确保 Node.js 与 pnpm 已正确安装(node -v、pnpm -v)
2. 确保在项目目录下运行
3. 检查端口3000是否被占用
如果端口被占用,可以修改端口:
编辑 双击启动.bat,将 -l 3000 改为 -l 3001(或其他端口)
编辑 双击启动.bat,将 --port 3000 改为 --port 3001(或其他端口)
技术说明:
- 使用 serve 包提供静态文件服务
- 自动构建项目(如果未构建)
- 支持SPA路由
需要桌面应用?
参考 README-PACKAGE.md 或 使用说明.txt,用 build-electron.bat 打成 exe。

View File

@@ -1,51 +0,0 @@
一力三期BMS - 故事AI导演工作台 打包说明
==========================================
一、打包步骤(三步完成)
第1步:双击运行 build-electron.bat
第2步:等待打包完成(约5-10分钟)
第3步:在 release 目录找到 exe 安装包
二、打包结果
打包完成后,release 目录会有:
- 故事AI导演工作台 Setup 1.0.0.exe(安装包)
三、给用户使用
用户只需:
1. 双击安装包
2. 按提示安装
3. 桌面出现快捷方式
4. 双击打开即可使用
四、注意事项
1. 首次打包需要下载Electron,请保持网络畅通
2. 打包后文件约100-150MB,这是正常的
3. 如果打包失败,删除 node_modules 文件夹后重试
五、自定义图标(可选)
如需自定义应用图标:
1. 准备一个 256x256 的 PNG 图标
2. 重命名为 icon.png
3. 放到 public 目录
4. 重新打包
六、常见问题
Q: 打包时提示网络错误?
A: 检查网络连接,或使用VPN重试。
Q: exe安装后打不开?
A: 右键以管理员身份运行。
Q: 如何修改窗口大小?
A: 编辑 electron/main.js,修改 width 和 height 值。
七、开发调试
如需开发调试,运行:
npm run electron:dev

View File

@@ -1,125 +1,39 @@
简单打包方案(无需下载Electron)
简单运行方案(不打包 Electron)
================================
如果Electron下载失败,可以使用这个简单方案:
如果 Electron 下载失败,或只是内部使用、不想打包 exe,可以用下面两种方式
直接在浏览器里运行(内容与 Electron 版完全一致)。
方案一:使用本地Node.js(推荐)
方案一:双击 双击启动.bat(推荐)
1. 确保已安装Node.js 16+
2. 运行以下命令:
脚本会自动执行:pnpm install(缺依赖时)→ pnpm run build(缺 dist 时)
→ vite preview --port 3000,然后访问 http://localhost:3000。
cd /d "d:\work\ykzd\一力三期\BMS\storyai-3d-director-desk"
npm install
npm run build
npm install -g serve
serve -s dist
方案二:手动命令
1. 确保已安装 Node.js 18+ 与 pnpm(npm i -g pnpm)
2. 在项目目录执行:
pnpm install
pnpm run build
pnpm run preview -- --port 3000
3. 打开浏览器访问 http://localhost:3000
方案二:创建启动脚本
开发模式(带热更新)
创建一个 start.bat 文件,内容如下:
pnpm run dev
@echo off
cd /d "%~dp0"
echo 正在启动故事AI导演工作台...
echo.
echo 请等待几秒钟...
echo.
:: 检查是否已构建
if not exist "dist\index.html" (
echo 正在构建项目...
call npm run build
)
:: 启动服务
echo 启动服务...
echo.
echo 请在浏览器中访问: http://localhost:3000
echo.
echo 按 Ctrl+C 停止服务
echo.
npx serve -s dist -l 3000
方案三:使用Python(如果已安装Python)
创建一个 start.py 文件:
import http.server
import socketserver
import webbrowser
import os
import threading
PORT = 3000
DIRECTORY = "dist"
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
def open_browser():
webbrowser.open(f"http://localhost:{PORT}")
if __name__ == "__main__":
os.chdir(os.path.dirname(os.path.abspath(__file__)))
threading.Timer(1.5, open_browser).start()
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"服务已启动: http://localhost:{PORT}")
print("按 Ctrl+C 停止")
httpd.serve_forever()
运行方式:
python start.py
方案四:使用批处理文件(最简单)
创建一个 双击启动.bat 文件:
@echo off
cd /d "%~dp0"
echo.
echo ========================================
echo 一力三期BMS - 故事AI导演工作台
echo ========================================
echo.
echo 正在启动...
echo.
:: 检查是否已构建
if not exist "dist\index.html" (
echo 正在构建项目,请稍候...
call npm run build
echo.
)
:: 启动服务
echo 启动服务...
echo.
echo 请在浏览器中访问: http://localhost:3000
echo.
echo 按 Ctrl+C 停止服务
echo.
npx serve -s dist -l 3000
使用方式:
1. 双击 双击启动.bat
2. 等待服务启动
3. 浏览器会自动打开
4. 如果没有自动打开,手动访问 http://localhost:3000
访问 http://localhost:5173。
优点:
- 无需下载Electron
- 文件小(仅几KB)
- 启动快
- 无需下载 Electron 二进制
- 依赖体积小、启动快
- 适合内部使用
缺点:
- 需要用户安装Node.js
- 不是真正的桌面应用
- 需要手动打开浏览器
- 需要用户机器已安装 Node.js 与 pnpm
- 不是真正的桌面应用,需要手动打开浏览器
推荐:
- 如果是给技术人员使用,选择方案四
- 如果是给普通用户使用,选择Electron方案
如果要给普通用户分发桌面应用,仍然推荐 Electron 方案:
见 README-PACKAGE.md 或 使用说明.txt(运行 build-electron.bat)。