From 64b41f58412439feda60f2a057542d92bb3cd978 Mon Sep 17 00:00:00 2001 From: johnathan <952508490@qq.com> Date: Fri, 20 Jun 2025 11:12:32 +0800 Subject: [PATCH] Update --- package.json | 1 + pnpm-lock.yaml | 626 +++++++++++++++++- src/components/ControlStrip.vue | 13 +- src/composables/useCloudExporter.ts | 135 ++++ src/composables/useLongTask.ts | 22 +- src/env.ts | 2 + src/logic/data-io.ts | 27 +- src/logic/page-worker/amazon.ts | 2 +- src/logic/page-worker/homedepot.ts | 2 +- src/logic/page-worker/types.d.ts | 91 ++- .../amazon.ts} | 101 +-- src/logic/web-injectors/base.ts | 20 + src/logic/web-injectors/homedepot.ts | 87 +++ src/logic/web-injectors/lowes.ts | 85 +++ src/options/App.vue | 16 +- src/options/Options.vue | 20 +- src/options/views/AmazonResultTable.vue | 204 ++++-- src/sidepanel/App.vue | 16 +- src/sidepanel/views/AmazonSidepanel.vue | 8 +- tsconfig.json | 1 + vite.config.mts | 6 +- 21 files changed, 1263 insertions(+), 222 deletions(-) create mode 100644 src/composables/useCloudExporter.ts rename src/logic/{web-injectors.ts => web-injectors/amazon.ts} (82%) create mode 100644 src/logic/web-injectors/base.ts create mode 100644 src/logic/web-injectors/homedepot.ts create mode 100644 src/logic/web-injectors/lowes.ts diff --git a/package.json b/package.json index 8b6d08c..ae41214 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/node": "^22.10.5", "@types/webextension-polyfill": "^0.12.1", "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-vue-jsx": "^4.2.0", "@vue/test-utils": "^2.4.6", "@vueuse/core": "^12.3.0", "chokidar": "^4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32084dd..8025534 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@vitejs/plugin-vue': specifier: ^5.2.1 version: 5.2.1(vite@6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.2.0 + version: 4.2.0(vite@6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -93,7 +96,7 @@ importers: version: 22.1.0(@vue/compiler-sfc@3.5.13) unplugin-vue-components: specifier: ^28.4.1 - version: 28.4.1(@babel/parser@7.27.0)(vue@3.5.13(typescript@5.8.2)) + version: 28.4.1(@babel/parser@7.27.5)(vue@3.5.13(typescript@5.8.2)) vite: specifier: ^6.2.4 version: 6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1) @@ -120,6 +123,13 @@ importers: version: 0.12.0 packages: + '@ampproject/remapping@2.3.0': + resolution: + { + integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, + } + engines: { node: '>=6.0.0' } + '@antfu/install-pkg@1.0.0': resolution: { @@ -145,6 +155,110 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/code-frame@7.27.1': + resolution: + { + integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, + } + engines: { node: '>=6.9.0' } + + '@babel/compat-data@7.27.5': + resolution: + { + integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==, + } + engines: { node: '>=6.9.0' } + + '@babel/core@7.27.4': + resolution: + { + integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==, + } + engines: { node: '>=6.9.0' } + + '@babel/generator@7.27.5': + resolution: + { + integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: + { + integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-compilation-targets@7.27.2': + resolution: + { + integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: + { + integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: + { + integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-module-imports@7.27.1': + resolution: + { + integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-module-transforms@7.27.3': + resolution: + { + integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: + { + integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-plugin-utils@7.27.1': + resolution: + { + integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-replace-supers@7.27.1': + resolution: + { + integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: + { + integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==, + } + engines: { node: '>=6.9.0' } + '@babel/helper-string-parser@7.25.9': resolution: { @@ -152,6 +266,13 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/helper-string-parser@7.27.1': + resolution: + { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, + } + engines: { node: '>=6.9.0' } + '@babel/helper-validator-identifier@7.25.9': resolution: { @@ -159,6 +280,27 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/helper-validator-identifier@7.27.1': + resolution: + { + integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-validator-option@7.27.1': + resolution: + { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: '>=6.9.0' } + + '@babel/helpers@7.27.6': + resolution: + { + integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==, + } + engines: { node: '>=6.9.0' } + '@babel/parser@7.27.0': resolution: { @@ -167,6 +309,41 @@ packages: engines: { node: '>=6.0.0' } hasBin: true + '@babel/parser@7.27.5': + resolution: + { + integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==, + } + engines: { node: '>=6.0.0' } + hasBin: true + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: + { + integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: + { + integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.27.1': + resolution: + { + integrity: sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.26.10': resolution: { @@ -174,6 +351,20 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/template@7.27.2': + resolution: + { + integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==, + } + engines: { node: '>=6.9.0' } + + '@babel/traverse@7.27.4': + resolution: + { + integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==, + } + engines: { node: '>=6.9.0' } + '@babel/types@7.27.0': resolution: { @@ -181,6 +372,13 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/types@7.27.6': + resolution: + { + integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==, + } + engines: { node: '>=6.9.0' } + '@bufbuild/protobuf@2.2.3': resolution: { @@ -826,12 +1024,39 @@ packages: } engines: { node: '>=12' } + '@jridgewell/gen-mapping@0.3.8': + resolution: + { + integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==, + } + engines: { node: '>=6.0.0' } + + '@jridgewell/resolve-uri@3.1.2': + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: '>=6.0.0' } + + '@jridgewell/set-array@1.2.1': + resolution: + { + integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, + } + engines: { node: '>=6.0.0' } + '@jridgewell/sourcemap-codec@1.5.0': resolution: { integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, } + '@jridgewell/trace-mapping@0.3.25': + resolution: + { + integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, + } + '@juggle/resize-observer@3.4.0': resolution: { @@ -899,6 +1124,12 @@ packages: } engines: { node: '>=12' } + '@rolldown/pluginutils@1.0.0-beta.17': + resolution: + { + integrity: sha512-i6p5fc1+lAmR3OHmNlv7/3PIY3EtuUu4kVARjkid38p7cmyIyqr0QFnA+k3xoB06wQUpBA91H1HFlRreZ2v5oA==, + } + '@rollup/rollup-android-arm-eabi@4.39.0': resolution: { @@ -1160,6 +1391,16 @@ packages: integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==, } + '@vitejs/plugin-vue-jsx@4.2.0': + resolution: + { + integrity: sha512-DSTrmrdLp+0LDNF77fqrKfx7X0ErRbOcUAgJL/HbSesqQwoUvUQ4uYQqaex+rovqgGcoPqVk+AwUh3v9CuiYIw==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.0.0 + '@vitejs/plugin-vue@5.2.1': resolution: { @@ -1220,6 +1461,31 @@ packages: integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==, } + '@vue/babel-helper-vue-transform-on@1.4.0': + resolution: + { + integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==, + } + + '@vue/babel-plugin-jsx@1.4.0': + resolution: + { + integrity: sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==, + } + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.4.0': + resolution: + { + integrity: sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==, + } + peerDependencies: + '@babel/core': ^7.0.0-0 + '@vue/compiler-core@3.5.13': resolution: { @@ -1659,6 +1925,14 @@ packages: } engines: { node: '>=8' } + browserslist@4.25.0: + resolution: + { + integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + buffer-builder@0.2.0: resolution: { @@ -1752,6 +2026,12 @@ packages: } engines: { node: '>=16' } + caniuse-lite@1.0.30001723: + resolution: + { + integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==, + } + chai@5.2.0: resolution: { @@ -2014,6 +2294,12 @@ packages: } engines: { node: '>=18' } + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + core-util-is@1.0.3: resolution: { @@ -2371,6 +2657,12 @@ packages: engines: { node: '>=14' } hasBin: true + electron-to-chromium@1.5.170: + resolution: + { + integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==, + } + emittery@1.1.0: resolution: { @@ -2905,6 +3197,13 @@ packages: } hasBin: true + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: '>=6.9.0' } + get-caller-file@2.0.5: resolution: { @@ -3017,6 +3316,13 @@ packages: } engines: { node: '>=18' } + globals@11.12.0: + resolution: + { + integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, + } + engines: { node: '>=4' } + globals@13.24.0: resolution: { @@ -3691,6 +3997,14 @@ packages: canvas: optional: true + jsesc@3.1.0: + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: '>=6' } + hasBin: true + json-buffer@3.0.1: resolution: { @@ -3740,6 +4054,14 @@ packages: integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, } + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: '>=6' } + hasBin: true + jsonfile@6.1.0: resolution: { @@ -4001,6 +4323,12 @@ packages: } engines: { node: 20 || >=22 } + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + magic-string@0.30.17: resolution: { @@ -4208,6 +4536,12 @@ packages: integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==, } + node-releases@2.0.19: + resolution: + { + integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==, + } + node-rsa@1.1.1: resolution: { @@ -5210,6 +5544,13 @@ packages: } hasBin: true + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + semver@7.6.3: resolution: { @@ -6012,6 +6353,15 @@ packages: } engines: { node: '>=4' } + update-browserslist-db@1.1.3: + resolution: + { + integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==, + } + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + update-notifier@7.3.1: resolution: { @@ -6463,6 +6813,12 @@ packages: } engines: { node: '>=10' } + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + yaml@2.7.1: resolution: { @@ -6519,6 +6875,11 @@ packages: engines: { node: '>= 10' } snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + '@antfu/install-pkg@1.0.0': dependencies: package-manager-detector: 0.2.11 @@ -6540,23 +6901,188 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.5': {} + + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.5': + dependencies: + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.27.6 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.27.6 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + '@babel/parser@7.27.0': dependencies: '@babel/types': 7.27.0 + '@babel/parser@7.27.5': + dependencies: + '@babel/types': 7.27.6 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.5 + '@babel/template': 7.27.2 + '@babel/types': 7.27.6 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.0': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.27.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@bufbuild/protobuf@2.2.3': {} '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': @@ -6851,8 +7377,23 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@juggle/resize-observer@3.4.0': {} '@mdn/browser-compat-data@5.7.3': {} @@ -6886,6 +7427,8 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@rolldown/pluginutils@1.0.0-beta.17': {} + '@rollup/rollup-android-arm-eabi@4.39.0': optional: true @@ -6987,6 +7530,17 @@ snapshots: '@ungap/structured-clone@1.2.1': {} + '@vitejs/plugin-vue-jsx@4.2.0(vite@6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))': + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.4) + '@rolldown/pluginutils': 1.0.0-beta.17 + '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.27.4) + vite: 6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1) + vue: 3.5.13(typescript@5.8.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-vue@5.2.1(vite@6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))': dependencies: vite: 6.2.4(@types/node@22.10.5)(sass-embedded@1.86.2)(tsx@4.19.2)(yaml@2.7.1) @@ -7032,6 +7586,35 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + '@vue/babel-helper-vue-transform-on@1.4.0': {} + + '@vue/babel-plugin-jsx@1.4.0(@babel/core@7.27.4)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.0 + '@vue/babel-helper-vue-transform-on': 1.4.0 + '@vue/babel-plugin-resolve-type': 1.4.0(@babel/core@7.27.4) + '@vue/shared': 3.5.13 + optionalDependencies: + '@babel/core': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.4.0(@babel/core@7.27.4)': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.27.0 + '@vue/compiler-sfc': 3.5.13 + transitivePeerDependencies: + - supports-color + '@vue/compiler-core@3.5.13': dependencies: '@babel/parser': 7.27.0 @@ -7359,6 +7942,13 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001723 + electron-to-chromium: 1.5.170 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + buffer-builder@0.2.0: {} buffer-crc32@0.2.13: {} @@ -7406,6 +7996,8 @@ snapshots: camelcase@8.0.0: {} + caniuse-lite@1.0.30001723: {} + chai@5.2.0: dependencies: assertion-error: 2.0.1 @@ -7576,6 +8168,8 @@ snapshots: graceful-fs: 4.2.11 xdg-basedir: 5.1.0 + convert-source-map@2.0.0: {} + core-util-is@1.0.3: {} cosmiconfig@9.0.0(typescript@5.8.2): @@ -7786,6 +8380,8 @@ snapshots: minimatch: 9.0.1 semver: 7.6.3 + electron-to-chromium@1.5.170: {} + emittery@1.1.0: {} emoji-regex@10.4.0: {} @@ -8221,6 +8817,8 @@ snapshots: which: 1.2.4 winreg: 0.0.12 + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} @@ -8307,6 +8905,8 @@ snapshots: dependencies: ini: 4.1.1 + globals@11.12.0: {} + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -8654,6 +9254,8 @@ snapshots: - supports-color - utf-8-validate + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-merge-patch@1.0.2: @@ -8672,6 +9274,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -8818,6 +9422,10 @@ snapshots: lru-cache@11.0.2: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -8937,6 +9545,8 @@ snapshots: uuid: 8.3.2 which: 2.0.2 + node-releases@2.0.19: {} + node-rsa@1.1.1: dependencies: asn1: 0.2.6 @@ -9527,6 +10137,8 @@ snapshots: semver@5.7.2: {} + semver@6.3.1: {} + semver@7.6.3: {} semver@7.7.1: {} @@ -9963,7 +10575,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.2 - unplugin-vue-components@28.4.1(@babel/parser@7.27.0)(vue@3.5.13(typescript@5.8.2)): + unplugin-vue-components@28.4.1(@babel/parser@7.27.5)(vue@3.5.13(typescript@5.8.2)): dependencies: chokidar: 3.6.0 debug: 4.4.0 @@ -9975,7 +10587,7 @@ snapshots: unplugin-utils: 0.2.4 vue: 3.5.13(typescript@5.8.2) optionalDependencies: - '@babel/parser': 7.27.0 + '@babel/parser': 7.27.5 transitivePeerDependencies: - supports-color @@ -10001,6 +10613,12 @@ snapshots: upath@2.0.1: {} + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + update-notifier@7.3.1: dependencies: boxen: 8.0.1 @@ -10331,6 +10949,8 @@ snapshots: y18n@5.0.8: {} + yallist@3.1.1: {} + yaml@2.7.1: {} yargs-parser@21.1.1: {} diff --git a/src/components/ControlStrip.vue b/src/components/ControlStrip.vue index dc23823..9f631a6 100644 --- a/src/components/ControlStrip.vue +++ b/src/components/ControlStrip.vue @@ -48,7 +48,18 @@ const emit = defineEmits<{ 导入 - + + + + + diff --git a/src/composables/useCloudExporter.ts b/src/composables/useCloudExporter.ts new file mode 100644 index 0000000..7991f8c --- /dev/null +++ b/src/composables/useCloudExporter.ts @@ -0,0 +1,135 @@ +import { remoteHost } from '~/env'; +import { useLongTask } from './useLongTask'; + +type AddDataFragmentsCommand = { + commandType: 'add-data-fragments'; + params: Array; +}; + +type ExportExcelCommand = { + commandType: 'export-excel'; +}; + +type Command = AddDataFragmentsCommand | ExportExcelCommand; + +type WebSocketResponse = + | { type: 'progress'; current: number; total: number } + | { type: 'result'; result: string }; + +class ExportExcelPipeline { + private socket: WebSocket; + + constructor() { + this.socket = new WebSocket(`ws://${remoteHost}/ws/daa0b9f1-4e4a-4e7c-9269-f5f0e86ae271`); + } + + private sendCommand(command: Command) { + const commandJson = JSON.stringify({ command }); + this.socket!.send(commandJson); + } + + public load() { + switch (this.socket.readyState) { + case WebSocket.CLOSED: + case WebSocket.CLOSING: + this.socket = new WebSocket(`ws://${remoteHost}/ws/daa0b9f1-4e4a-4e7c-9269-f5f0e86ae271`); + case WebSocket.CONNECTING: + return new Promise((resolve) => { + this.socket!.onopen = () => resolve(this); + }); + case WebSocket.OPEN: + return Promise.resolve(this); + default: + return Promise.resolve(this); + } + } + + public close() { + this.socket.close(); + return new Promise(async (resolve) => { + while (this.socket.readyState != WebSocket.CLOSED) { + await new Promise((r) => setTimeout(r, 100)); + } + resolve(); + }); + } + + public addFragments(...fragments: DataFragment[]) { + this.sendCommand({ commandType: 'add-data-fragments', params: fragments }); + return this; + } + + public exportExcel(progress?: (current: number, total: number) => Promise | void) { + return new Promise((resolve, reject) => { + this.socket.onmessage = (ev) => { + const response: WebSocketResponse = JSON.parse(ev.data); + switch (response.type) { + case 'progress': + const { current, total } = response; + progress && progress(current, total); + break; + case 'result': + this.socket!.onmessage = null; + const fileUrl = response.result; + resolve(fileUrl); + break; + default: + console.error('Unknown message type:', response); + } + }; + this.socket.onclose = () => { + reject('Connection is closed'); + }; + this.sendCommand({ commandType: 'export-excel' }); + }); + } +} + +export type DataFragment = { + data: Array>; + imageColumn?: string; + name?: string; +}; + +export const useCloudExporter = () => { + const { isRunning, startTask } = useLongTask(); + const progress = reactive({ current: 0, total: 0 }); + let pipeline: ExportExcelPipeline | null = null; + + const stop = async () => { + if (pipeline) { + await pipeline.close(); + pipeline = null; + } + }; + + const doExport = (fragments: DataFragment[]) => + startTask(async () => { + progress.current = 0; + progress.total = 0; + + pipeline = new ExportExcelPipeline(); + await pipeline.load(); + pipeline.addFragments(...fragments); + const file = await pipeline + .exportExcel((current, total) => { + progress.current = current; + progress.total = total; + }) + .catch(() => null); + await pipeline.close(); + + if (file) { + const url = `http://${remoteHost}${file}`; + const link = document.createElement('a'); + link.href = url; + link.download = `${dayjs().format('YYYY-MM-DD')}.xlsx`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + return file?.split('/').pop(); + }); + return { isRunning, progress, doExport, stop }; +}; diff --git a/src/composables/useLongTask.ts b/src/composables/useLongTask.ts index 0a835cf..6be353a 100644 --- a/src/composables/useLongTask.ts +++ b/src/composables/useLongTask.ts @@ -1,17 +1,19 @@ export function useLongTask() { const isRunning = ref(false); - const startTask = async (task: () => Promise) => { - isRunning.value = true; - - try { - await task(); - isRunning.value = false; - } catch (error) { - isRunning.value = false; - console.error('Task failed:', error); - throw error; + const startTask = async (task: () => Promise) => { + if (isRunning.value) { + throw Error('Task is still running.'); } + isRunning.value = true; + let result = undefined; + try { + result = await task(); + } catch (error) { + console.error('Task failed:', error); + } + isRunning.value = false; + return result as T; }; return { diff --git a/src/env.ts b/src/env.ts index bd19ad3..b9ad559 100644 --- a/src/env.ts +++ b/src/env.ts @@ -12,3 +12,5 @@ export function isForbiddenUrl(url: string): boolean { } export const isFirefox = navigator.userAgent.includes('Firefox'); + +export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '127.0.0.1:8000'; diff --git a/src/logic/data-io.ts b/src/logic/data-io.ts index 16b3a3b..95e3aa4 100644 --- a/src/logic/data-io.ts +++ b/src/logic/data-io.ts @@ -22,9 +22,6 @@ class Worksheet { const cols = headers.filter((h) => h.ignore?.out !== true); for (let j = 0; j < cols.length; j++) { const header = cols[j]; - if (header.ignore?.out) { - continue; - } const value = getAttribute(item, header.prop); if (header.formatOutputValue) { record[header.label] = await header.formatOutputValue(value, i); @@ -196,6 +193,30 @@ export type ImportBaseOptions = { headers?: Header[]; }; +export function castRecordsByHeaders>( + jsonData: Record[], + headers: Omit[], +): Promise { + return Promise.all( + jsonData.map(async (item, i) => { + const record: Record = {}; + const cols = headers.filter((h) => h.ignore?.out !== true); + for (let j = 0; j < cols.length; j++) { + const header = cols[j]; + const value = getAttribute(item, header.prop); + if (header.formatOutputValue) { + record[header.label] = await header.formatOutputValue(value, i); + } else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) { + record[header.label] = value; + } else { + record[header.label] = JSON.stringify(value); + } + } + return record as T; + }), + ); +} + /** * 导出为XLSX * @param data 数据数组 diff --git a/src/logic/page-worker/amazon.ts b/src/logic/page-worker/amazon.ts index 18cd5a3..7aa4348 100644 --- a/src/logic/page-worker/amazon.ts +++ b/src/logic/page-worker/amazon.ts @@ -6,7 +6,7 @@ import { AmazonDetailPageInjector, AmazonReviewPageInjector, AmazonSearchPageInjector, -} from '../web-injectors'; +} from '~/logic/web-injectors/amazon'; import { isForbiddenUrl } from '~/env'; /** diff --git a/src/logic/page-worker/homedepot.ts b/src/logic/page-worker/homedepot.ts index d4e65f7..3e762c3 100644 --- a/src/logic/page-worker/homedepot.ts +++ b/src/logic/page-worker/homedepot.ts @@ -2,7 +2,7 @@ import Emittery from 'emittery'; import { HomedepotEvents, HomedepotWorker } from './types'; import { Tabs } from 'webextension-polyfill'; import { withErrorHandling } from '../error-handler'; -import { HomedepotDetailPageInjector } from '../web-injectors'; +import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot'; class HomedepotWorkerImpl implements HomedepotWorker { private static _instance: HomedepotWorker | null = null; diff --git a/src/logic/page-worker/types.d.ts b/src/logic/page-worker/types.d.ts index 4d68c7f..6ec86ad 100644 --- a/src/logic/page-worker/types.d.ts +++ b/src/logic/page-worker/types.d.ts @@ -39,18 +39,6 @@ type AmazonItem = Pick & Partial & Partial & { hasDetail: boolean }; -type HomedepotDetailItem = { - OSMID: string; - link: string; - brandName?: string; - title: string; - price: string; - rate?: string; - innerText: string; - reviewCount?: number; - mainImageUrl: string; -}; - interface AmazonPageWorkerEvents { /** * The event is fired when worker collected links to items on the Amazon search page. @@ -86,17 +74,6 @@ interface AmazonPageWorkerEvents { ['error']: { message: string; url?: string }; } -interface HomedepotEvents { - /** - * The event is fired when detail items collect - */ - ['detail-item-collected']: { item: HomedepotDetailItem }; - /** - * The event is fired when error occurs. - */ - ['error']: { message: string; url?: string }; -} - interface AmazonPageWorker { /** * The channel for communication with the Amazon page worker. @@ -137,6 +114,30 @@ interface AmazonPageWorker { stop(): Promise; } +type HomedepotDetailItem = { + OSMID: string; + link: string; + brandName?: string; + title: string; + price: string; + rate?: string; + innerText: string; + reviewCount?: number; + mainImageUrl: string; + modelInfo?: string; +}; + +interface HomedepotEvents { + /** + * The event is fired when detail items collect + */ + ['detail-item-collected']: { item: HomedepotDetailItem }; + /** + * The event is fired when error occurs. + */ + ['error']: { message: string; url?: string }; +} + interface HomedepotWorker { /** * The channel for communication with the Homedepot page worker. @@ -156,3 +157,47 @@ interface HomedepotWorker { */ stop(): Promise; } + +type LowesDetailItem = { + OSMID: string; + link: string; + brandName?: string; + title: string; + price: string; + rate?: string; + innerText: string; + reviewCount?: number; + mainImageUrl: string; + modelInfo?: string; +}; + +interface LowesEvents { + /** + * The event is fired when detail items collect + */ + ['detail-item-collected']: { item: LowesDetailItem }; + /** + * The event is fired when error occurs. + */ + ['error']: { message: string; url?: string }; +} + +interface LowesWorker { + /** + * The channel for communication with the Lowes page worker. + */ + readonly channel: Emittery; + + /** + * Browsing goods detail page and collect target information + */ + runDetailPageTask( + urls: string[], + progress?: (remains: string[]) => Promise | void, + ): Promise; + + /** + * Stop the worker. + */ + stop(): Promise; +} diff --git a/src/logic/web-injectors.ts b/src/logic/web-injectors/amazon.ts similarity index 82% rename from src/logic/web-injectors.ts rename to src/logic/web-injectors/amazon.ts index 4cd6909..01984bd 100644 --- a/src/logic/web-injectors.ts +++ b/src/logic/web-injectors/amazon.ts @@ -1,24 +1,5 @@ -import { exec } from './execute-script'; -import type { Tabs } from 'webextension-polyfill'; -import type { AmazonReview, AmazonSearchItem, HomedepotDetailItem } from './page-worker/types'; - -class BaseInjector { - readonly _tab: Tabs.Tab; - - readonly _timeout: number; - - constructor(tab: Tabs.Tab, timeout: number = 30000) { - this._tab = tab; - this._timeout = timeout; - } - - run>( - func: (payload: P) => Promise, - payload?: P, - ): Promise { - return exec(this._tab.id!, func, payload as P, { timeout: this._timeout }); - } -} +import { BaseInjector } from './base'; +import { AmazonReview, AmazonSearchItem } from '../page-worker/types'; export class AmazonSearchPageInjector extends BaseInjector { public waitForPageLoaded() { @@ -167,7 +148,7 @@ export class AmazonDetailPageInjector extends BaseInjector { const targetNode = document.querySelector( '#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li), .av-page-desktop', ); - const exceptionalNodeSelectors = ['music-detail-header', '.avu-retail-page']; + const exceptionalNodeSelectors = ['.music-detail-header', '.avu-retail-page']; for (const selector of exceptionalNodeSelectors) { if (document.querySelector(selector)) { return false; @@ -444,79 +425,3 @@ export class AmazonReviewPageInjector extends BaseInjector { ); } } - -export class HomedepotDetailPageInjector extends BaseInjector { - constructor(tab: Tabs.Tab) { - super(tab, 60000); - } - - public waitForPageLoad() { - return this.run(async () => { - let timeout = false; - setTimeout(() => (timeout = true), 15000); - const isLoaded = () => { - const reviewPlaceholderEl = document.querySelector( - `#product-section-rr div[role='button'][aria-expanded='true']`, - ); - return reviewPlaceholderEl && (document.readyState == 'complete' || timeout); - }; - while (true) { - await new Promise((resolve) => setTimeout(resolve, 500 + ~~(Math.random() * 500))); - document - .querySelector( - `#product-section-rr div[role='button'][aria-expanded='false'], #product-section-overview div[role='button'][aria-expanded='false']`, - ) - ?.click(); - const { scrollHeight, scrollTop } = document.documentElement; - scrollHeight - scrollTop > 100 - ? window.scrollBy({ top: 100, behavior: 'smooth' }) - : document - .querySelector('[data-component^="product-details:ProductDetailsTitle"]') - ?.scrollIntoView({ behavior: 'smooth' }); - if (isLoaded()) { - break; - } - } - }); - } - - public getInfo() { - return this.run(async () => { - const link = document.location.toString(); - const brandName = document.querySelector( - `[data-component^="product-details:ProductDetailsBrandCollection"]`, - )?.innerText; - const title = document.querySelector( - `[data-component^="product-details:ProductDetailsTitle"]`, - )!.innerText; - const price = document - .querySelector(`#standard-price`)! - .innerText.replaceAll('\n', ''); - const rateEl = document.querySelector( - `[data-component^="ratings-and-reviews"] .sui-mr-1`, - ); - const rate = rateEl ? /\d(\.\d)?/.exec(rateEl.innerText)![0] : undefined; - const reviewCount = rateEl - ? Number( - /\d+/.exec( - document.querySelector( - `[data-component^="ratings-and-reviews"] [name="simple-rating"] + span`, - )!.innerText, - )![0], - ) - : undefined; - const mainImageUrl = document.querySelector( - `.mediagallery__mainimage img`, - )!.src; - return { - link, - brandName, - title, - price, - rate, - reviewCount, - mainImageUrl, - } as Omit; - }); - } -} diff --git a/src/logic/web-injectors/base.ts b/src/logic/web-injectors/base.ts new file mode 100644 index 0000000..d3daab3 --- /dev/null +++ b/src/logic/web-injectors/base.ts @@ -0,0 +1,20 @@ +import { Tabs } from 'webextension-polyfill'; +import { exec } from '~/logic/execute-script'; + +export class BaseInjector { + readonly _tab: Tabs.Tab; + + readonly _timeout: number; + + constructor(tab: Tabs.Tab, timeout: number = 30000) { + this._tab = tab; + this._timeout = timeout; + } + + run>( + func: (payload: P) => Promise, + payload?: P, + ): Promise { + return exec(this._tab.id!, func, payload as P, { timeout: this._timeout }); + } +} diff --git a/src/logic/web-injectors/homedepot.ts b/src/logic/web-injectors/homedepot.ts new file mode 100644 index 0000000..01c0706 --- /dev/null +++ b/src/logic/web-injectors/homedepot.ts @@ -0,0 +1,87 @@ +import { Tabs } from 'webextension-polyfill'; +import { BaseInjector } from './base'; +import { HomedepotDetailItem } from '../page-worker/types'; + +export class HomedepotDetailPageInjector extends BaseInjector { + constructor(tab: Tabs.Tab) { + super(tab, 60000); + } + + public waitForPageLoad() { + return this.run(async () => { + let timeout = false; + setTimeout(() => (timeout = true), 15000); + const isLoaded = () => { + const reviewPlaceholderEl = document.querySelector( + `#product-section-rr div[role='button'][aria-expanded='true']`, + ); + return reviewPlaceholderEl && (document.readyState == 'complete' || timeout); + }; + while (true) { + await new Promise((resolve) => setTimeout(resolve, 500 + ~~(Math.random() * 500))); + document + .querySelector( + `#product-section-rr div[role='button'][aria-expanded='false'], #product-section-overview div[role='button'][aria-expanded='false']`, + ) + ?.click(); + const { scrollHeight, scrollTop } = document.documentElement; + scrollHeight - scrollTop > 100 + ? window.scrollBy({ top: 100, behavior: 'smooth' }) + : document + .querySelector('[data-component^="product-details:ProductDetailsTitle"]') + ?.scrollIntoView({ behavior: 'smooth' }); + if (isLoaded()) { + break; + } + } + }); + } + + public getInfo() { + return this.run(async () => { + const link = document.location.toString(); + const brandName = document.querySelector( + `[data-component^="product-details:ProductDetailsBrandCollection"]`, + )?.innerText; + const title = document.querySelector( + `[data-component^="product-details:ProductDetailsTitle"]`, + )!.innerText; + const price = document + .querySelector(`#standard-price`)! + .innerText.replaceAll('\n', ''); + const rateEl = document.querySelector( + `[data-component^="ratings-and-reviews"] .sui-mr-1`, + ); + const rate = rateEl ? /\d(\.\d)?/.exec(rateEl.innerText)![0] : undefined; + const reviewCount = rateEl + ? Number( + /\d+/.exec( + document.querySelector( + `[data-component^="ratings-and-reviews"] [name="simple-rating"] + span`, + )!.innerText, + )![0], + ) + : undefined; + const mainImageUrl = document.querySelector( + `.mediagallery__mainimage img`, + )!.src; + const modelInfoEl = document.evaluate( + `//*[@id="product-section-product-overview"]//*[@class="product-info-bar"]//*[starts-with(text(), "Model #")]`, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + ).singleNodeValue as HTMLDivElement | null; + const [modelInfo] = /(?<=#\s).+/.exec(modelInfoEl?.innerText || '') || []; + return { + link, + brandName, + title, + price, + rate, + reviewCount, + mainImageUrl, + modelInfo, + } as Omit; + }); + } +} diff --git a/src/logic/web-injectors/lowes.ts b/src/logic/web-injectors/lowes.ts new file mode 100644 index 0000000..c487129 --- /dev/null +++ b/src/logic/web-injectors/lowes.ts @@ -0,0 +1,85 @@ +import { Tabs } from 'webextension-polyfill'; +import { BaseInjector } from './base'; + +export class LowesDetailPageInjector extends BaseInjector { + constructor(tab: Tabs.Tab) { + super(tab, 60000); + } + + public waitForPageLoad() { + return this.run(async () => { + while (true) { + if (document.readyState === 'complete') { + break; + } + } + }); + } + + public getBaseInfo() { + return this.run(async () => { + // 获取Item # + const itemNumberEl = document.evaluate( + `//p[starts-with(text(), "Item #")]`, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + ).singleNodeValue as HTMLDivElement | null; + const itemSeries = itemNumberEl?.innerText.replace('Item #', '').trim(); + + // 获取Model # + const modelNumberEl = document.evaluate( + `//p[starts-with(text(), "Model #")]`, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + ).singleNodeValue as HTMLDivElement | null; + const modelSeries = modelNumberEl?.innerText.replace('Model #', '').trim(); + + // 获取品牌名称 + const brandName = ( + document.evaluate( + `//h1[contains(@class, "product-brand-description")]/parent::*/parent::*/following-sibling::*[1]//a`, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + ).singleNodeValue as HTMLDivElement + ).innerText; + + // 获取标题 + const title = document.querySelector( + `h1.product-brand-description`, + )?.innerText; + + // 获取价格 + const price = document + .querySelector(`.screen-reader`) + ?.innerText.replaceAll('\n', ''); + + // 获取评分 + const rate = document.querySelector(`.avgrating`)?.innerText; + + // 获取评价数量 + const reviewCount = Number( + document.querySelector(`[data-testid="rating-trigger"] > div > div > span`) + ?.innerText || '0', + ); + + // 获取图片URL + const mainImageUrl = document.querySelector( + `#mfe-gallery .productImage.tile-img`, + )?.src; + + return { + brandName, + title, + price, + rate, + reviewCount, + mainImageUrl, + itemSeries, + modelSeries, + }; + }); + } +} diff --git a/src/options/App.vue b/src/options/App.vue index 596bea8..3807ddb 100644 --- a/src/options/App.vue +++ b/src/options/App.vue @@ -15,12 +15,14 @@ const theme: GlobalThemeOverrides = { diff --git a/src/options/Options.vue b/src/options/Options.vue index 17182af..4577b25 100644 --- a/src/options/Options.vue +++ b/src/options/Options.vue @@ -3,23 +3,31 @@ import { RouterView } from 'vue-router';