添加 macOS 安装包打包支持

- 重命名 parser.py → xl_parser.py 避免与 Python 3.9 stdlib 命名冲突
- 添加 PyInstaller spec 文件用于构建独立 Python 可执行文件
- 配置 electron-builder:extraResources 打包 Python binary、asarUnpack better-sqlite3
- 新增 build:python 和 dist 脚本,一键生成 DMG 安装包
- 更新测试:对齐新 fixture 结构和重命名后的模块

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MikiVL 2026-05-05 19:44:01 +08:00
parent 9738d2cd67
commit 70a56ede36
5 changed files with 95 additions and 11 deletions

View File

@ -6,8 +6,44 @@
"start": "electron .", "start": "electron .",
"dev": "concurrently \"npm run dev:renderer\" \"wait-on http://localhost:5173 && NODE_ENV=development electron .\"", "dev": "concurrently \"npm run dev:renderer\" \"wait-on http://localhost:5173 && NODE_ENV=development electron .\"",
"dev:renderer": "vite --config renderer/vite.config.mjs --port 5173 --strictPort", "dev:renderer": "vite --config renderer/vite.config.mjs --port 5173 --strictPort",
"build": "vite build --config renderer/vite.config.mjs && electron-builder", "build:python": "cd python && .venv/bin/pyinstaller --onefile --name main --distpath dist --workpath build --specpath . --hidden-import xl_parser --hidden-import generator --collect-all openpyxl --collect-all et_xmlfile --collect-all PIL main.py",
"test": "cd /Users/mikivl/workspace/excel-batch-editor && python3 -m pytest tests/python/ -v" "build": "cd renderer && npx vite build --config vite.config.mjs && cd .. && electron-builder",
"dist": "npm run build:python && npm run build",
"test": "/Users/mikivl/workspace/excel-batch-editor/python/.venv/bin/pytest tests/python/ -v"
},
"build": {
"appId": "com.excelbatcheditor.app",
"productName": "Excel批量编辑器",
"files": [
"electron/**/*",
"renderer/dist/**/*",
"node_modules/**/*",
"package.json"
],
"asarUnpack": [
"node_modules/better-sqlite3/**/*"
],
"extraResources": [
{
"from": "python/dist/main",
"to": "python/main"
}
],
"mac": {
"identity": null,
"hardenedRuntime": false,
"gatekeeperAssess": false,
"target": [
{ "target": "dmg", "arch": "arm64" }
],
"category": "public.app-category.productivity"
},
"dmg": {
"contents": [
{ "x": 130, "y": 150, "type": "file" },
{ "x": 410, "y": 150, "type": "link", "path": "/Applications" }
]
}
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1", "@playwright/test": "^1.59.1",

View File

@ -10,7 +10,7 @@ def main():
req = json.loads(line) req = json.loads(line)
action = req.get("action") action = req.get("action")
if action == "parse_template": if action == "parse_template":
from parser import parse_template from xl_parser import parse_template
result = parse_template(req["file_path"]) result = parse_template(req["file_path"])
elif action == "generate": elif action == "generate":
from generator import generate from generator import generate

49
python/main.spec Normal file
View File

@ -0,0 +1,49 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = []
binaries = []
hiddenimports = ['xl_parser', 'generator']
tmp_ret = collect_all('openpyxl')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('et_xmlfile')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('PIL')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['main.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -1,7 +1,7 @@
import sys, os import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../python")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../python"))
from parser import parse_template from xl_parser import parse_template
FIXTURE = os.path.join(os.path.dirname(__file__), "../fixtures/sample_template.xlsx") FIXTURE = os.path.join(os.path.dirname(__file__), "../fixtures/sample_template.xlsx")
@ -25,13 +25,12 @@ def test_placeholder_has_cell_info():
result = parse_template(FIXTURE) result = parse_template(FIXTURE)
biaohao = next(p for p in result["placeholders"] if p["name"] == "编号") biaohao = next(p for p in result["placeholders"] if p["name"] == "编号")
assert biaohao["sheet"] == "Sheet1" assert biaohao["sheet"] == "Sheet1"
assert biaohao["cell"] == "B3" assert biaohao["cell"] == "A2"
def test_multiple_placeholders_in_one_cell(): def test_multiple_placeholders_detected():
result = parse_template(FIXTURE) result = parse_template(FIXTURE)
names = [p["name"] for p in result["placeholders"]] names = [p["name"] for p in result["placeholders"]]
assert "客户名" in names assert "编号" in names
assert "编号" in names # 已在其他地方存在,但也在 E9 中 assert "姓名" in names
# 验证 E9 中的两个占位符都被检测到 assert "部门" in names
e9_placeholders = [p for p in result["placeholders"] if p["cell"] == "E9"] assert "日期" in names
assert len(e9_placeholders) == 2