Feat: Add real-time progress and logging to data extractor

- Implement IPC event system for pushing progress updates from main to renderer
- Add Zustand store for centralized extractor state management
- Refactor useExtractor hook to use store pattern
- Add chromium-bidi dependency and externalize Playwright for build compatibility
- Show detailed logs during extraction (DB connection, order resolution, ERP login, data import)
This commit is contained in:
Misaka
2026-03-04 20:51:54 +08:00
parent fed76b4fe0
commit 9e1b5530ea
12 changed files with 297 additions and 55 deletions

View File

@@ -9,8 +9,10 @@ files:
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- 'package.json'
asarUnpack: asarUnpack:
- resources/** - resources/**
- '**/node_modules/playwright/**'
win: win:
executableName: erpauto executableName: erpauto
nsis: nsis:

View File

@@ -4,7 +4,13 @@ import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
main: {}, main: {
build: {
rollupOptions: {
external: ['playwright', 'playwright-core']
}
}
},
preload: {}, preload: {},
renderer: { renderer: {
resolve: { resolve: {

70
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"chromium-bidi": "^15.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
@@ -105,6 +106,7 @@
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.1.2", "@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0", "@azure/core-auth": "^1.10.0",
@@ -166,6 +168,7 @@
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
"integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.1.2", "@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.10.0", "@azure/core-auth": "^1.10.0",
@@ -357,6 +360,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1125,7 +1129,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1147,7 +1150,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -1164,7 +1166,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -1179,7 +1180,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -3054,6 +3054,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
"integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -3076,6 +3077,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -3184,6 +3186,7 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1", "@typescript-eslint/types": "8.56.1",
@@ -3617,6 +3620,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -3649,6 +3653,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -4396,6 +4401,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -4815,6 +4821,28 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/chromium-bidi": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-15.0.0.tgz",
"integrity": "sha512-ESWZM1u85CoeSozBXXG9M73S5tH0EjkqnFJoQ6F3MHs2YGe0CLVMaRvhGxetLP6w4GVR59+/cpWvDLUpLvJXLQ==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/chromium-bidi/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/chromium-pickle-js": { "node_modules/chromium-pickle-js": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
@@ -5100,8 +5128,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -5412,6 +5439,13 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/devtools-protocol": {
"version": "0.0.1593706",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1593706.tgz",
"integrity": "sha512-13WD1OZ/T9E8+6o53JRd1VJrLgAc1e5v9g/ywzdD/6t0bvI+OmOju6jDBrjEKyTjKngiSvq7hjodc6dSTC25Pg==",
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dir-compare": { "node_modules/dir-compare": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -5460,6 +5494,7 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.8.1", "app-builder-lib": "26.8.1",
"builder-util": "26.8.1", "builder-util": "26.8.1",
@@ -5686,6 +5721,7 @@
"integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==", "integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^22.7.7", "@types/node": "^22.7.7",
@@ -5874,7 +5910,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -5895,7 +5930,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -6224,6 +6258,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -6284,6 +6319,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -9336,6 +9372,12 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -10055,6 +10097,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -10137,6 +10180,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -10160,7 +10204,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -10178,7 +10221,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -10199,6 +10241,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -10331,6 +10374,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -11618,7 +11662,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -12136,6 +12179,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -12370,6 +12414,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -12916,6 +12961,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/expect": "4.0.18", "@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18", "@vitest/mocker": "4.0.18",
@@ -13124,6 +13170,7 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8", "@dabh/diagnostics": "^2.0.8",
@@ -13361,6 +13408,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -30,6 +30,7 @@
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"chromium-bidi": "^15.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",

View File

@@ -1,4 +1,4 @@
import { ipcMain } from 'electron' import { ipcMain, webContents } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth' import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor' import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver' import { OrderNumberResolver } from '../services/erp/order-resolver'
@@ -10,13 +10,35 @@ import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
const log = createLogger('ExtractorHandler') const log = createLogger('ExtractorHandler')
function sendProgress(windowId: number, message: string, progress: number): void {
try {
webContents.getAllWebContents().forEach((wc) => {
wc.send('extractor:progress', { message, progress })
})
} catch (error) {
log.warn('Failed to send progress event', { error })
}
}
function sendLog(windowId: number, level: string, message: string): void {
try {
webContents.getAllWebContents().forEach((wc) => {
wc.send('extractor:log', { level, message })
})
} catch (error) {
log.warn('Failed to send log event', { error })
}
}
/** /**
* Register IPC handlers for extractor service * Register IPC handlers for extractor service
*/ */
export function registerExtractorHandlers(): void { export function registerExtractorHandlers(): void {
ipcMain.handle( ipcMain.handle(
'extractor:run', 'extractor:run',
async (_event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => { async (event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
const windowId = event.sender.id
return withErrorHandling(async () => { return withErrorHandling(async () => {
let authService: ErpAuthService | null = null let authService: ErpAuthService | null = null
let dbService: IDatabaseService | null = null let dbService: IDatabaseService | null = null
@@ -41,6 +63,9 @@ export function registerExtractorHandlers(): void {
// Create database service using factory // Create database service using factory
log.info('Connecting to database for order resolution...') log.info('Connecting to database for order resolution...')
sendProgress(windowId, '连接数据库...', 5)
sendLog(windowId, 'system', '正在连接数据库...')
try { try {
dbService = await create() dbService = await create()
} catch (error) { } catch (error) {
@@ -52,6 +77,9 @@ export function registerExtractorHandlers(): void {
} }
// Resolve order numbers (convert productionIDs to 生产订单号) // Resolve order numbers (convert productionIDs to 生产订单号)
sendProgress(windowId, '解析订单号...', 10)
sendLog(windowId, 'info', '正在解析订单号...')
const resolver = new OrderNumberResolver(dbService) const resolver = new OrderNumberResolver(dbService)
const mappings = await resolver.resolve(input.orderNumbers) const mappings = await resolver.resolve(input.orderNumbers)
@@ -71,6 +99,7 @@ export function registerExtractorHandlers(): void {
} }
log.info('Resolved order numbers', { count: validOrderNumbers.length }) log.info('Resolved order numbers', { count: validOrderNumbers.length })
sendLog(windowId, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`)
// Create auth service and login // Create auth service and login
authService = new ErpAuthService({ authService = new ErpAuthService({
@@ -80,6 +109,9 @@ export function registerExtractorHandlers(): void {
headless: true headless: true
}) })
sendProgress(windowId, '登录 ERP 系统...', 15)
sendLog(windowId, 'system', '正在登录 ERP 系统...')
log.info('Logging in to ERP...') log.info('Logging in to ERP...')
try { try {
await authService.login() await authService.login()
@@ -91,6 +123,7 @@ export function registerExtractorHandlers(): void {
) )
} }
log.info('Login successful') log.info('Login successful')
sendLog(windowId, 'success', 'ERP 登录成功')
// Create extractor service and run extraction with resolved order numbers // Create extractor service and run extraction with resolved order numbers
const extractor = new ExtractorService(authService) const extractor = new ExtractorService(authService)
@@ -98,9 +131,19 @@ export function registerExtractorHandlers(): void {
const modifiedInput: ExtractorInput = { const modifiedInput: ExtractorInput = {
...input, ...input,
orderNumbers: validOrderNumbers orderNumbers: validOrderNumbers,
onProgress: (message, progress) => {
sendProgress(windowId, message, progress)
sendLog(windowId, 'info', message)
},
onLog: (level, message) => {
sendLog(windowId, level, message)
}
} }
sendProgress(windowId, '开始提取数据...', 20)
sendLog(windowId, 'system', '提取引擎启动,开始下载数据...')
const result = await extractor.extract(modifiedInput) const result = await extractor.extract(modifiedInput)
// Add warnings to result errors if any // Add warnings to result errors if any

View File

@@ -3,7 +3,12 @@ import fs from 'fs/promises'
import { ExtractorCore } from './extractor-core' import { ExtractorCore } from './extractor-core'
import { ErpAuthService } from './erp-auth' import { ErpAuthService } from './erp-auth'
import { ExcelParser } from '../excel/excel-parser' import { ExcelParser } from '../excel/excel-parser'
import type { ExtractorInput, ExtractorResult, ImportResult } from '../../types/extractor.types' import type {
ExtractorInput,
ExtractorResult,
ImportResult,
LogLevel
} from '../../types/extractor.types'
import { DataImportService } from '../database/data-importer' import { DataImportService } from '../database/data-importer'
/** /**
@@ -75,7 +80,10 @@ export class ExtractorService {
// Auto-import to database if merge was successful // Auto-import to database if merge was successful
if (result.mergedFile) { if (result.mergedFile) {
input.onProgress?.('正在写入数据库...', 98) input.onProgress?.('正在写入数据库...', 98)
const importResult = await this.importToDatabase(result.mergedFile) const importResult = await this.importToDatabaseWithLogging(
result.mergedFile,
input.onLog
)
result.importResult = importResult result.importResult = importResult
if (!importResult.success && importResult.errors.length > 0) { if (!importResult.success && importResult.errors.length > 0) {
@@ -317,4 +325,55 @@ export class ExtractorService {
} }
} }
} }
/**
* Import merged Excel data to database with logging
* @param filePath - Path to the merged Excel file
* @param onLog - Optional log callback
* @returns Import result with statistics
*/
private async importToDatabaseWithLogging(
filePath: string,
onLog?: (level: LogLevel, message: string) => void
): Promise<ImportResult> {
console.log(`[Extractor] Starting database import from: ${filePath}`)
onLog?.('info', `开始导入数据到数据库...`)
const importService = new DataImportService()
try {
const result = await importService.importFromExcel(filePath, 1000)
console.log(`[Extractor] Import completed`, {
success: result.success,
recordsRead: result.recordsRead,
recordsDeleted: result.recordsDeleted,
recordsImported: result.recordsImported
})
if (result.success) {
onLog?.(
'success',
`导入完成:读取 ${result.recordsRead} 条,删除 ${result.recordsDeleted} 条,导入 ${result.recordsImported}`
)
} else if (result.errors.length > 0) {
result.errors.forEach((err) => onLog?.('error', err))
}
return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error(`[Extractor] Import failed: ${errorMsg}`)
onLog?.('error', `导入失败:${errorMsg}`)
return {
success: false,
recordsRead: 0,
recordsDeleted: 0,
recordsImported: 0,
uniqueSourceNumbers: 0,
errors: [errorMsg]
}
}
}
} }

View File

@@ -1,9 +1,12 @@
import type { ErpSession } from './erp.types' import type { ErpSession } from './erp.types'
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
export interface ExtractorInput { export interface ExtractorInput {
orderNumbers: string[] orderNumbers: string[]
batchSize?: number batchSize?: number
onProgress?: (message: string, progress: number) => void onProgress?: (message: string, progress: number) => void
onLog?: (level: LogLevel, message: string) => void
} }
/** /**

View File

@@ -77,6 +77,18 @@ export interface ExtractorAPI {
runExtractor: ( runExtractor: (
input: ExtractorInput input: ExtractorInput
) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }> ) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }>
/**
* Subscribe to progress updates
* @param callback - Callback function receiving progress data
* @returns Unsubscribe function
*/
onProgress: (callback: (data: { message: string; progress: number }) => void) => () => void
/**
* Subscribe to log messages
* @param callback - Callback function receiving log data
* @returns Unsubscribe function
*/
onLog: (callback: (data: { level: string; message: string }) => void) => () => void
} }
/** /**

View File

@@ -27,7 +27,23 @@ const api = {
// Extractor service // Extractor service
extractor: { extractor: {
runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input) runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input),
onProgress: (callback: (data: { message: string; progress: number }) => void) => {
const subscription = (
_event: Electron.IpcRendererEvent,
data: { message: string; progress: number }
) => callback(data)
ipcRenderer.on('extractor:progress', subscription)
return () => ipcRenderer.removeListener('extractor:progress', subscription)
},
onLog: (callback: (data: { level: string; message: string }) => void) => {
const subscription = (
_event: Electron.IpcRendererEvent,
data: { level: string; message: string }
) => callback(data)
ipcRenderer.on('extractor:log', subscription)
return () => ipcRenderer.removeListener('extractor:log', subscription)
}
}, },
// Cleaner service // Cleaner service

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { Terminal } from 'lucide-react' import { Terminal } from 'lucide-react'
import type { LogEntry, LogLevel, ExtractorProgress } from '../../hooks/useExtractor' import type { LogEntry, LogLevel, ExtractorProgress } from '../../stores/extractorStore'
interface LogPanelProps { interface LogPanelProps {
logs: LogEntry[] logs: LogEntry[]

View File

@@ -1,38 +1,34 @@
import { useState, useEffect } from 'react' import { useEffect } from 'react'
import { useExtractorStore } from '../stores/extractorStore'
export interface ExtractorProgress {
message: string
progress: number
}
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
export interface LogEntry {
timestamp: string
level: LogLevel
message: string
}
export function useExtractor() { export function useExtractor() {
const [isRunning, setIsRunning] = useState(false) const {
const [progress, setProgress] = useState<ExtractorProgress | null>(null) isRunning,
const [error, setError] = useState<string | null>(null) progress,
const [logs, setLogs] = useState<LogEntry[]>([]) error,
logs,
const addLog = (level: LogLevel, message: string) => { setRunning,
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) setProgress,
setLogs((prev) => [...prev, { timestamp, level, message }]) setError,
} addLog,
clearLogs,
resetState
} = useExtractorStore()
useEffect(() => { useEffect(() => {
if (progress) { const unsubscribeProgress = window.electron.extractor.onProgress((data) => {
addLog('info', progress.message) setProgress({ message: data.message, progress: data.progress })
} })
}, [progress])
const clearLogs = () => { const unsubscribeLog = window.electron.extractor.onLog((data) => {
setLogs([]) addLog(data.level as any, data.message)
} })
return () => {
unsubscribeProgress()
unsubscribeLog()
}
}, [setProgress, addLog])
const startExtraction = async (orderNumbers: string) => { const startExtraction = async (orderNumbers: string) => {
if (!orderNumbers.trim()) { if (!orderNumbers.trim()) {
@@ -40,11 +36,8 @@ export function useExtractor() {
return return
} }
setIsRunning(true) resetState()
setProgress(null) setRunning(true)
setError(null)
setLogs([])
addLog('system', '提取引擎启动,准备执行...') addLog('system', '提取引擎启动,准备执行...')
try { try {
@@ -77,7 +70,7 @@ export function useExtractor() {
setError(errMsg) setError(errMsg)
addLog('error', errMsg) addLog('error', errMsg)
} finally { } finally {
setIsRunning(false) setRunning(false)
setProgress(null) setProgress(null)
} }
} }

View File

@@ -0,0 +1,59 @@
import { create } from 'zustand'
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
export interface LogEntry {
timestamp: string
level: LogLevel
message: string
}
export interface ExtractorProgress {
message: string
progress: number
}
export interface ExtractorState {
isRunning: boolean
progress: ExtractorProgress | null
error: string | null
logs: LogEntry[]
}
export interface ExtractorActions {
setRunning: (isRunning: boolean) => void
setProgress: (progress: ExtractorProgress | null) => void
setError: (error: string | null) => void
addLog: (level: LogLevel, message: string) => void
clearLogs: () => void
resetState: () => void
}
const initialState: ExtractorState = {
isRunning: false,
progress: null,
error: null,
logs: []
}
export const useExtractorStore = create<ExtractorState & ExtractorActions>((set) => ({
...initialState,
setRunning: (isRunning: boolean) => set({ isRunning }),
setProgress: (progress: ExtractorProgress | null) => set({ progress }),
setError: (error: string | null) => set({ error }),
addLog: (level: LogLevel, message: string) =>
set((state) => {
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false })
return {
logs: [...state.logs, { timestamp, level, message }]
}
}),
clearLogs: () => set({ logs: [] }),
resetState: () => set(initialState)
}))