This commit is contained in:
johnathan 2025-06-13 18:35:20 +08:00
parent a240242331
commit 0948413460
22 changed files with 734 additions and 173 deletions

View File

@ -43,6 +43,7 @@
"dayjs": "^1.11.13",
"emittery": "^1.1.0",
"esno": "^4.8.0",
"exceljs": "^4.4.0",
"fs-extra": "^11.2.0",
"husky": "^9.1.7",
"jsdom": "^26.0.0",
@ -61,14 +62,12 @@
"vitest": "^3.1.1",
"vue": "^3.5.13",
"vue-demi": "^0.14.10",
"vue-router": "^4.5.1",
"web-ext": "^8.5.0",
"webext-bridge": "^6.0.1",
"webextension-polyfill": "^0.12.0"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}
}

422
pnpm-lock.yaml generated
View File

@ -6,10 +6,6 @@ settings:
importers:
.:
dependencies:
xlsx:
specifier: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
version: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
devDependencies:
'@ffflorian/jszip-cli':
specifier: ^3.8.2
@ -53,6 +49,9 @@ importers:
esno:
specifier: ^4.8.0
version: 4.8.0
exceljs:
specifier: ^4.4.0
version: 4.4.0
fs-extra:
specifier: ^11.2.0
version: 11.2.0
@ -107,6 +106,9 @@ importers:
vue-demi:
specifier: ^0.14.10
version: 0.14.10(vue@3.5.13(typescript@5.8.2))
vue-router:
specifier: ^4.5.1
version: 4.5.1(vue@3.5.13(typescript@5.8.2))
web-ext:
specifier: ^8.5.0
version: 8.5.0
@ -743,6 +745,18 @@ packages:
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
'@fast-csv/format@4.3.5':
resolution:
{
integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==,
}
'@fast-csv/parse@4.3.6':
resolution:
{
integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==,
}
'@ffflorian/jszip-cli@3.8.2':
resolution:
{
@ -1104,6 +1118,12 @@ packages:
integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==,
}
'@types/node@14.18.63':
resolution:
{
integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==,
}
'@types/node@22.10.5':
resolution:
{
@ -1224,6 +1244,12 @@ packages:
integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==,
}
'@vue/devtools-api@6.6.4':
resolution:
{
integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==,
}
'@vue/reactivity@3.5.13':
resolution:
{
@ -1432,6 +1458,13 @@ packages:
}
engines: { node: '>= 6' }
archiver-utils@3.0.4:
resolution:
{
integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==,
}
engines: { node: '>= 10' }
archiver@3.1.1:
resolution:
{
@ -1439,6 +1472,13 @@ packages:
}
engines: { node: '>= 6' }
archiver@5.3.2:
resolution:
{
integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==,
}
engines: { node: '>= 10' }
argparse@2.0.1:
resolution:
{
@ -1556,12 +1596,24 @@ packages:
}
engines: { node: '>=8' }
binary@0.3.0:
resolution:
{
integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==,
}
bl@4.1.0:
resolution:
{
integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==,
}
bluebird@3.4.7:
resolution:
{
integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==,
}
bluebird@3.7.2:
resolution:
{
@ -1625,6 +1677,13 @@ packages:
integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==,
}
buffer-indexof-polyfill@1.0.2:
resolution:
{
integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==,
}
engines: { node: '>=0.10' }
buffer@5.7.1:
resolution:
{
@ -1637,6 +1696,13 @@ packages:
integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==,
}
buffers@0.1.1:
resolution:
{
integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==,
}
engines: { node: '>=0.2.0' }
bundle-name@3.0.0:
resolution:
{
@ -1693,6 +1759,12 @@ packages:
}
engines: { node: '>=12' }
chainsaw@0.1.0:
resolution:
{
integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==,
}
chalk@2.4.2:
resolution:
{
@ -1897,6 +1969,13 @@ packages:
}
engines: { node: '>= 6' }
compress-commons@4.1.2:
resolution:
{
integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==,
}
engines: { node: '>= 10' }
concat-map@0.0.1:
resolution:
{
@ -1953,6 +2032,14 @@ packages:
typescript:
optional: true
crc-32@1.2.2:
resolution:
{
integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==,
}
engines: { node: '>=0.8' }
hasBin: true
crc32-stream@3.0.1:
resolution:
{
@ -1960,6 +2047,13 @@ packages:
}
engines: { node: '>= 6.9.0' }
crc32-stream@4.0.3:
resolution:
{
integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==,
}
engines: { node: '>= 10' }
crc@3.8.0:
resolution:
{
@ -2257,6 +2351,12 @@ packages:
}
engines: { node: '>= 0.4' }
duplexer2@0.1.4:
resolution:
{
integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==,
}
eastasianwidth@0.2.0:
resolution:
{
@ -2567,6 +2667,13 @@ packages:
integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==,
}
exceljs@4.4.0:
resolution:
{
integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==,
}
engines: { node: '>=8.3.0' }
execa@5.1.1:
resolution:
{
@ -2601,6 +2708,13 @@ packages:
integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==,
}
fast-csv@4.3.6:
resolution:
{
integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==,
}
engines: { node: '>=10.0.0' }
fast-deep-equal@3.1.3:
resolution:
{
@ -2757,6 +2871,14 @@ packages:
engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 }
os: [darwin]
fstream@1.0.12:
resolution:
{
integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==,
}
engines: { node: '>=0.6' }
deprecated: This package is no longer supported.
function-bind@1.1.2:
resolution:
{
@ -3710,6 +3832,12 @@ packages:
engines: { node: '>=18.12.0' }
hasBin: true
listenercount@1.0.1:
resolution:
{
integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==,
}
listr2@8.2.5:
resolution:
{
@ -3756,18 +3884,61 @@ packages:
integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==,
}
lodash.escaperegexp@4.1.2:
resolution:
{
integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==,
}
lodash.flatten@4.4.0:
resolution:
{
integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==,
}
lodash.groupby@4.6.0:
resolution:
{
integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==,
}
lodash.isboolean@3.0.3:
resolution:
{
integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==,
}
lodash.isequal@4.5.0:
resolution:
{
integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==,
}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.isfunction@3.0.9:
resolution:
{
integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==,
}
lodash.isnil@4.0.0:
resolution:
{
integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==,
}
lodash.isplainobject@4.0.6:
resolution:
{
integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==,
}
lodash.isundefined@3.0.1:
resolution:
{
integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==,
}
lodash.merge@4.6.2:
resolution:
{
@ -3780,6 +3951,12 @@ packages:
integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==,
}
lodash.uniq@4.5.0:
resolution:
{
integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==,
}
lodash@4.17.21:
resolution:
{
@ -3917,6 +4094,13 @@ packages:
integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==,
}
minimatch@5.1.6:
resolution:
{
integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==,
}
engines: { node: '>=10' }
minimatch@9.0.1:
resolution:
{
@ -3944,6 +4128,13 @@ packages:
}
engines: { node: '>=16 || 14 >=14.17' }
mkdirp@0.5.6:
resolution:
{
integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==,
}
hasBin: true
mlly@1.7.4:
resolution:
{
@ -4567,6 +4758,12 @@ packages:
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
readdir-glob@1.1.3:
resolution:
{
integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==,
}
readdirp@3.6.0:
resolution:
{
@ -4683,6 +4880,14 @@ packages:
integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==,
}
rimraf@2.7.1:
resolution:
{
integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==,
}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution:
{
@ -4972,6 +5177,13 @@ packages:
integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==,
}
saxes@5.0.1:
resolution:
{
integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==,
}
engines: { node: '>=10' }
saxes@6.0.0:
resolution:
{
@ -5573,6 +5785,12 @@ packages:
}
engines: { node: '>=18' }
traverse@0.3.9:
resolution:
{
integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==,
}
treemate@0.3.11:
resolution:
{
@ -5781,6 +5999,12 @@ packages:
}
engines: { node: '>=8' }
unzipper@0.10.14:
resolution:
{
integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==,
}
upath@2.0.1:
resolution:
{
@ -5944,6 +6168,14 @@ packages:
'@vue/composition-api':
optional: true
vue-router@4.5.1:
resolution:
{
integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==,
}
peerDependencies:
vue: ^3.2.0
vue@3.5.13:
resolution:
{
@ -6197,12 +6429,6 @@ packages:
}
engines: { node: '>=12' }
xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz:
resolution: { tarball: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz }
version: 0.20.3
engines: { node: '>=0.8' }
hasBin: true
xml-name-validator@5.0.0:
resolution:
{
@ -6285,6 +6511,13 @@ packages:
}
engines: { node: '>= 6' }
zip-stream@4.1.1:
resolution:
{
integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==,
}
engines: { node: '>= 10' }
snapshots:
'@antfu/install-pkg@1.0.0':
dependencies:
@ -6542,6 +6775,25 @@ snapshots:
'@eslint/js@8.57.1': {}
'@fast-csv/format@4.3.5':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.isboolean: 3.0.3
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
'@fast-csv/parse@4.3.6':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.groupby: 4.6.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
lodash.isundefined: 3.0.1
lodash.uniq: 4.5.0
'@ffflorian/jszip-cli@3.8.2(typescript@5.8.2)':
dependencies:
commander: 12.1.0
@ -6717,6 +6969,8 @@ snapshots:
'@types/minimatch@3.0.5': {}
'@types/node@14.18.63': {}
'@types/node@22.10.5':
dependencies:
undici-types: 6.20.0
@ -6808,6 +7062,8 @@ snapshots:
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
'@vue/devtools-api@6.6.4': {}
'@vue/reactivity@3.5.13':
dependencies:
'@vue/shared': 3.5.13
@ -6969,6 +7225,19 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 2.3.8
archiver-utils@3.0.4:
dependencies:
glob: 7.2.3
graceful-fs: 4.2.11
lazystream: 1.0.1
lodash.defaults: 4.2.0
lodash.difference: 4.5.0
lodash.flatten: 4.4.0
lodash.isplainobject: 4.0.6
lodash.union: 4.6.0
normalize-path: 3.0.0
readable-stream: 3.6.2
archiver@3.1.1:
dependencies:
archiver-utils: 2.1.0
@ -6979,6 +7248,16 @@ snapshots:
tar-stream: 2.2.0
zip-stream: 2.1.3
archiver@5.3.2:
dependencies:
archiver-utils: 2.1.0
async: 3.2.6
buffer-crc32: 0.2.13
readable-stream: 3.6.2
readdir-glob: 1.1.3
tar-stream: 2.2.0
zip-stream: 4.1.1
argparse@2.0.1: {}
array-buffer-byte-length@1.0.2:
@ -7035,12 +7314,19 @@ snapshots:
binary-extensions@2.3.0: {}
binary@0.3.0:
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
bluebird@3.4.7: {}
bluebird@3.7.2: {}
boolbase@1.0.0: {}
@ -7079,6 +7365,8 @@ snapshots:
buffer-from@1.1.2: {}
buffer-indexof-polyfill@1.0.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@ -7089,6 +7377,8 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
buffers@0.1.1: {}
bundle-name@3.0.0:
dependencies:
run-applescript: 5.0.0
@ -7124,6 +7414,10 @@ snapshots:
loupe: 3.1.2
pathval: 2.0.0
chainsaw@0.1.0:
dependencies:
traverse: 0.3.9
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@ -7250,6 +7544,13 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 2.3.8
compress-commons@4.1.2:
dependencies:
buffer-crc32: 0.2.13
crc32-stream: 4.0.3
normalize-path: 3.0.0
readable-stream: 3.6.2
concat-map@0.0.1: {}
concat-stream@1.6.2:
@ -7286,11 +7587,18 @@ snapshots:
optionalDependencies:
typescript: 5.8.2
crc-32@1.2.2: {}
crc32-stream@3.0.1:
dependencies:
crc: 3.8.0
readable-stream: 3.6.2
crc32-stream@4.0.3:
dependencies:
crc-32: 1.2.2
readable-stream: 3.6.2
crc@3.8.0:
dependencies:
buffer: 5.7.1
@ -7465,6 +7773,10 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexer2@0.1.4:
dependencies:
readable-stream: 2.3.8
eastasianwidth@0.2.0: {}
editorconfig@1.0.4:
@ -7740,6 +8052,18 @@ snapshots:
evtd@0.2.4: {}
exceljs@4.4.0:
dependencies:
archiver: 5.3.2
dayjs: 1.11.13
fast-csv: 4.3.6
jszip: 3.10.1
readable-stream: 3.6.2
saxes: 5.0.1
tmp: 0.2.3
unzipper: 0.10.14
uuid: 8.3.2
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
@ -7780,6 +8104,11 @@ snapshots:
exsolve@1.0.4: {}
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
'@fast-csv/parse': 4.3.6
fast-deep-equal@3.1.3: {}
fast-json-patch@3.1.1: {}
@ -7863,6 +8192,13 @@ snapshots:
fsevents@2.3.3:
optional: true
fstream@1.0.12:
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.7.1
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@ -8402,6 +8738,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
listenercount@1.0.1: {}
listr2@8.2.5:
dependencies:
cli-truncate: 4.0.0
@ -8434,14 +8772,30 @@ snapshots:
lodash.difference@4.5.0: {}
lodash.escaperegexp@4.1.2: {}
lodash.flatten@4.4.0: {}
lodash.groupby@4.6.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
lodash.isfunction@3.0.9: {}
lodash.isnil@4.0.0: {}
lodash.isplainobject@4.0.6: {}
lodash.isundefined@3.0.1: {}
lodash.merge@4.6.2: {}
lodash.union@4.6.0: {}
lodash.uniq@4.5.0: {}
lodash@4.17.21: {}
log-update@6.1.0:
@ -8503,6 +8857,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.11
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.1
minimatch@9.0.1:
dependencies:
brace-expansion: 2.0.1
@ -8515,6 +8873,10 @@ snapshots:
minipass@7.1.2: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
mlly@1.7.4:
dependencies:
acorn: 8.14.1
@ -8908,6 +9270,10 @@ snapshots:
process: 0.11.10
string_decoder: 1.3.0
readdir-glob@1.1.3:
dependencies:
minimatch: 5.1.6
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@ -8973,6 +9339,10 @@ snapshots:
rfdc@1.4.1: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rimraf@3.0.2:
dependencies:
glob: 7.2.3
@ -9143,6 +9513,10 @@ snapshots:
sax@1.4.1: {}
saxes@5.0.1:
dependencies:
xmlchars: 2.2.0
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
@ -9469,6 +9843,8 @@ snapshots:
dependencies:
punycode: 2.3.1
traverse@0.3.9: {}
treemate@0.3.11: {}
tslib@2.8.1: {}
@ -9610,6 +9986,19 @@ snapshots:
untildify@4.0.0: {}
unzipper@0.10.14:
dependencies:
big-integer: 1.6.52
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
upath@2.0.1: {}
update-notifier@7.3.1:
@ -9728,6 +10117,11 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.8.2)
vue-router@4.5.1(vue@3.5.13(typescript@5.8.2)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.8.2)
vue@3.5.13(typescript@5.8.2):
dependencies:
'@vue/compiler-dom': 3.5.13
@ -9924,8 +10318,6 @@ snapshots:
xdg-basedir@5.1.0: {}
xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz: {}
xml-name-validator@5.0.0: {}
xml2js@0.6.2:
@ -9970,3 +10362,9 @@ snapshots:
archiver-utils: 2.1.0
compress-commons: 2.1.1
readable-stream: 3.6.2
zip-stream@4.1.1:
dependencies:
archiver-utils: 3.0.4
compress-commons: 4.1.2
readable-stream: 3.6.2

4
shim.d.ts vendored
View File

@ -6,3 +6,7 @@ declare module 'webext-bridge' {
// see https://github.com/antfu/webext-bridge#type-safe-protocols
}
}
declare global {
type AppContext = 'options' | 'sidepanel';
}

View File

@ -0,0 +1,4 @@
export function useAppContext() {
const appContext = document.location.pathname.split('/')[2] as AppContext;
return { appContext };
}

View File

@ -1,9 +1,9 @@
export function useCurrentUrl() {
const currentUrl = ref('');
const currentUrl = ref<string | undefined>(undefined);
const updateUrl = async () => {
const tab = await browser.tabs.query({ active: true, currentWindow: true }).then((ts) => ts[0]);
currentUrl.value = tab.url || '';
currentUrl.value = tab.url || undefined;
};
onMounted(() => {
@ -17,5 +17,5 @@ export function useCurrentUrl() {
browser.tabs.onHighlighted.removeListener(updateUrl);
});
return { currentUrl };
return currentUrl;
}

View File

@ -1,4 +0,0 @@
export function usePageContext() {
const pageContext = document.location.pathname.split('/')[2] as 'sidepanel' | 'options';
return { pageContext };
}

View File

@ -6,7 +6,7 @@ import { storage } from 'webextension-polyfill';
import type { RemovableRef, StorageLikeAsync, UseStorageAsyncOptions } from '@vueuse/core';
import type { Ref } from 'vue-demi';
import type { Storage } from 'webextension-polyfill';
import { usePageContext } from './usePageContext';
import { useAppContext } from './useAppContext';
export type WebExtensionStorageOptions<T> = UseStorageAsyncOptions<T>;
@ -121,7 +121,7 @@ export function useWebExtensionStorage<T>(
return;
}
if (typeof listenToStorageChanges === 'string') {
const { pageContext: context } = usePageContext();
const { appContext: context } = useAppContext();
if (listenToStorageChanges !== context) {
return;
}

View File

@ -1,12 +1,13 @@
import type { App } from 'vue';
import { usePageContext } from '~/composables/usePageContext';
import { useAppContext } from '~/composables/useAppContext';
import { router } from '~/router';
/**
* Setup Vue app
* @param app Vue app
*/
export function setupApp(app: App) {
const { pageContext: context } = usePageContext();
const { appContext: context } = useAppContext();
// Inject a globally available `$app` object in template
app.config.globalProperties.$app = {
@ -19,4 +20,6 @@ export function setupApp(app: App) {
// Here you can install additional plugins for all contexts: popup, options page and content-script.
// example: app.use(i18n)
// example excluding content-script context: if (context !== 'content-script') app.use(i18n)
app.use(router);
return app;
}

View File

@ -1,97 +1,144 @@
import { utils, read, writeFileXLSX, WorkSheet, WorkBook } from 'xlsx';
import excel from 'exceljs';
class Worksheet {
readonly _raw: WorkSheet;
readonly _ws: excel.Worksheet;
readonly workbook: Workbook;
constructor(ws: WorkSheet) {
this._raw = ws;
constructor(ws: excel.Worksheet, wb: Workbook) {
this._ws = ws;
this.workbook = wb;
}
static fromJson(data: Record<string, unknown>[], options: { headers?: Header[] } = {}) {
async readJson(data: Record<string, unknown>[], options: { headers?: Header[] } = {}) {
const {
headers = data.length > 0
? Object.keys(data[0]).map((k) => ({ label: k, prop: k }) as Header)
: [],
} = options;
const rows = data.map((item) => {
const row: Record<string, unknown> = {};
headers.forEach((header) => {
const value = getAttribute(item, header.prop);
if (header.formatOutputValue) {
row[header.label] = header.formatOutputValue(value);
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
row[header.label] = value;
} else {
row[header.label] = JSON.stringify(value);
const rows = await Promise.all(
data.map(async (item, i) => {
const record: Record<string, unknown> = {};
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);
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
record[header.label] = value;
} else {
record[header.label] = JSON.stringify(value);
}
}
});
return row;
return record;
}),
);
this._ws.columns = headers.map((e) => {
return { header: e.label, key: e.label };
});
const ws = utils.json_to_sheet(rows, {
header: headers.map((h) => h.label),
});
ws['!autofilter'] = {
ref: utils.encode_range({ c: 0, r: 0 }, { c: headers.length - 1, r: rows.length }),
}; // Use Auto Filter https://github.com/SheetJS/sheetjs/issues/472#issuecomment-292852308
return new Worksheet(ws);
this._ws.addRows(rows);
this._ws.autoFilter = {
from: {
row: 1,
column: 1,
},
to: {
row: rows.length + 1,
column: headers.length,
},
};
}
toJson<T>(options: { headers?: Header[] } = {}) {
async toJson<T>(options: { headers?: Header[] } = {}) {
const { headers } = options;
let jsonData = utils.sheet_to_json<Record<string, unknown>>(this._raw);
if (headers) {
jsonData = jsonData.map((item) => {
const mappedItem: Record<string, unknown> = {};
headers.forEach((header) => {
const value = header.parseImportValue
? header.parseImportValue(item[header.label])
: item[header.label];
setAttribute(mappedItem, header.prop, value);
});
return mappedItem;
let jsonData: Record<string, unknown>[] = [];
this._ws.eachRow((row) => {
const rowData: Record<string, unknown> = {};
row.eachCell((cell, colNumber) => {
const header = this._ws.getRow(1).getCell(colNumber).value?.toString()!;
rowData[header] = cell.value;
});
jsonData.push(rowData);
});
jsonData = jsonData.slice(1); // Remove Headers
if (headers) {
jsonData = await Promise.all(
jsonData.map(async (item, i) => {
const mappedItem: Record<string, unknown> = {};
for (const header of headers) {
if (header.ignore?.in) {
continue;
}
const value = header.parseImportValue
? await header.parseImportValue(item[header.label], i)
: item[header.label];
setAttribute(mappedItem, header.prop, value);
}
return mappedItem;
}),
);
}
return jsonData as T[];
}
toWorkbook(sheetName?: string) {
const wb = new Workbook(utils.book_new());
wb.addSheet(sheetName || 'Sheet1', this);
return wb;
async addImage(img: { data: ArrayBuffer; ext: 'jpeg' | 'png' | 'gif' }) {
const imgId = this.workbook._wb.addImage({
buffer: img.data,
extension: img.ext,
});
return imgId;
}
}
class Workbook {
readonly _raw: WorkBook;
_wb: excel.Workbook;
constructor(wb: WorkBook) {
this._raw = wb;
constructor(wb: excel.Workbook) {
this._wb = wb;
}
get sheetCount() {
return this._raw.SheetNames.length;
return this._wb.worksheets.length;
}
static fromArrayBuffer(bf: ArrayBuffer) {
const data = new Uint8Array(bf);
const wb = read(data, { type: 'array' });
return new Workbook(wb);
static createWorkbook() {
return new Workbook(new excel.Workbook());
}
getSheet(index: number) {
const sheetName = this._raw.SheetNames[index];
return new Worksheet(this._raw.Sheets[sheetName]);
async loadArrayBuffer(bf: ArrayBuffer) {
this._wb = await this._wb.xlsx.load(bf);
}
addSheet(name: string, sheet: Worksheet) {
utils.book_append_sheet(this._raw, sheet._raw, name);
getSheet(index: number): Worksheet | undefined {
const ws = this._wb.getWorksheet(index + 1); // Align the index
return ws ? new Worksheet(ws, this) : undefined;
}
exportFile(fileName: string) {
writeFileXLSX(this._raw, fileName, { bookType: 'xlsx', type: 'binary', compression: true });
addSheet(name?: string) {
const ws = this._wb.addWorksheet(name);
return new Worksheet(ws, this);
}
async exportFile(fileName: string) {
const bf = await this._wb.xlsx.writeBuffer();
const blob = new Blob([bf], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
@ -131,12 +178,17 @@ function setAttribute(obj: Record<string, unknown>, path: string, value: unknown
export type Header = {
label: string;
prop: string;
parseImportValue?: (val: any) => any;
formatOutputValue?: (val: any) => any;
parseImportValue?: (val: any, index: number) => any;
formatOutputValue?: (val: any, index: number) => any;
ignore?: {
in?: boolean;
out?: boolean;
};
};
export type ExportBaseOptions = {
fileName?: string;
sheetName?: string;
headers?: Header[];
};
@ -149,31 +201,20 @@ export type ImportBaseOptions = {
* @param data
* @param options
*/
export function exportToXLSX(
export async function exportToXLSX(
data: Record<string, unknown>[],
options?: ExportBaseOptions & { asWorkSheet?: false },
): void;
export function exportToXLSX(
data: Record<string, unknown>[],
options: Omit<ExportBaseOptions, 'fileName'> & { asWorkSheet: true },
): Worksheet;
export function exportToXLSX(
data: Record<string, unknown>[],
options: ExportBaseOptions & { asWorkSheet?: boolean } = {},
options: ExportBaseOptions = {},
) {
const {
headers,
sheetName,
fileName = `export_${new Date().toISOString().slice(0, 10)}.xlsx`,
asWorkSheet,
} = options;
const worksheet = Worksheet.fromJson(data, { headers: headers });
if (asWorkSheet) {
return worksheet;
}
const workbook = worksheet.toWorkbook();
workbook.exportFile(fileName);
const workbook = Workbook.createWorkbook();
const worksheet = workbook.addSheet(sheetName);
await worksheet.readJson(data, { headers });
await workbook.exportFile(fileName);
}
/**
@ -198,13 +239,15 @@ export async function importFromXLSX<T extends Record<string, unknown>>(
reader.onload = (event) => {
try {
const wb = Workbook.fromArrayBuffer(event.target?.result as ArrayBuffer);
if (asWorkBook) {
resolve(wb);
}
const ws = wb.getSheet(0); // 默认读取第一个工作表
const jsonData = ws.toJson<T>({ headers });
resolve(jsonData);
const wb = Workbook.createWorkbook();
wb.loadArrayBuffer(event.target?.result as ArrayBuffer).then(() => {
if (asWorkBook) {
resolve(wb);
} else {
const ws = wb.getSheet(0)!; // 默认读取第一个工作表
resolve(ws.toJson<T>({ headers }));
}
});
} catch (error) {
reject(error);
}
@ -215,3 +258,10 @@ export async function importFromXLSX<T extends Record<string, unknown>>(
reader.readAsArrayBuffer(file);
});
}
/**
* Excel文件对象
*/
export function createWorkbook() {
return Workbook.createWorkbook();
}

View File

@ -21,20 +21,29 @@
* console.log(result); // Outputs: 42
* ```
*/
export async function exec<T>(tabId: number, func: () => Promise<T>): Promise<T>;
type ExecOptions = {
timeout?: number;
};
export async function exec<T>(
tabId: number,
func: () => Promise<T>,
payload?: undefined,
options?: ExecOptions,
): Promise<T>;
export async function exec<T, P extends Record<string, unknown>>(
tabId: number,
func: (payload: P) => Promise<T>,
payload: P,
options?: ExecOptions,
): Promise<T>;
export async function exec<T, P extends Record<string, unknown>>(
tabId: number,
func: (payload?: P) => Promise<T>,
payload?: P,
options: ExecOptions = {},
): Promise<T> {
const { timeout } = {
timeout: 30000,
};
const { timeout = 30000 } = options;
return new Promise<T>(async (resolve, reject) => {
setTimeout(() => reject('脚本运行超时'), timeout);
try {

View File

@ -1,7 +1,6 @@
import Emittery from 'emittery';
import { HomedepotEvents, HomedepotWorker } from './types';
import { Tabs } from 'webextension-polyfill';
import { isForbiddenUrl } from '~/env';
import { withErrorHandling } from '../error-handler';
import { HomedepotDetailPageInjector } from '../web-injectors';
@ -19,13 +18,6 @@ class HomedepotWorkerImpl implements HomedepotWorker {
private readonly _controlChannel = new Emittery<{ interrupt: undefined }>();
private async getCurrentTab(): Promise<Tabs.Tab> {
const tab = await browser.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => tabs[0]);
return tab;
}
private async createNewTab(url?: string): Promise<Tabs.Tab> {
const tab = await browser.tabs.create({ url, active: true });
return tab;
@ -34,16 +26,14 @@ class HomedepotWorkerImpl implements HomedepotWorker {
@withErrorHandling
private async wanderingDetailPage(OSMID: string) {
const url = `https://www.homedepot.com/p/${OSMID}`;
let tab = await this.getCurrentTab();
if (!tab.url || isForbiddenUrl(tab.url)) {
tab = await this.createNewTab(url);
} else {
await browser.tabs.update(tab.id!, { url });
}
const tab = await this.createNewTab(url);
const injector = new HomedepotDetailPageInjector(tab);
await injector.waitForPageLoad();
const info = await injector.getInfo();
this.channel.emit('detail-item-collected', { item: { OSMID, ...info } });
setTimeout(() => {
browser.tabs.remove(tab.id!);
}, 1000);
}
async runDetailPageTask(

View File

@ -42,12 +42,12 @@ type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
type HomedepotDetailItem = {
OSMID: string;
link: string;
brandName: string;
brandName?: string;
title: string;
price: string;
rate: string;
rate?: string;
innerText: string;
reviewCount: number;
reviewCount?: number;
mainImageUrl: string;
};

View File

@ -5,15 +5,18 @@ import type { AmazonReview, AmazonSearchItem, HomedepotDetailItem } from './page
class BaseInjector {
readonly _tab: Tabs.Tab;
constructor(tab: Tabs.Tab) {
readonly _timeout: number;
constructor(tab: Tabs.Tab, timeout: number = 30000) {
this._tab = tab;
this._timeout = timeout;
}
run<T, P extends Record<string, unknown>>(
func: (payload: P) => Promise<T>,
payload?: P,
): Promise<T> {
return exec(this._tab.id!, func, payload as P);
return exec(this._tab.id!, func, payload as P, { timeout: this._timeout });
}
}
@ -443,23 +446,34 @@ 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<HTMLElement>(
`#product-section-overview div[role='button'][aria-expanded='false']`,
`#product-section-rr div[role='button'][aria-expanded='false'], #product-section-overview div[role='button'][aria-expanded='false']`,
)
?.click();
const reviewPlaceholderEl = document.querySelector(
`[data-component^="ratings-and-reviews"] [class^="placeholder"]`,
);
reviewPlaceholderEl?.scrollIntoView({ behavior: 'smooth' });
if (document.readyState === 'complete' && !reviewPlaceholderEl) {
await new Promise((resolve) => setTimeout(resolve, 1000));
document
.querySelector(`#product-section-rr`)
?.scrollIntoView({ behavior: 'smooth', block: 'center' });
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;
}
}
@ -471,22 +485,26 @@ export class HomedepotDetailPageInjector extends BaseInjector {
const link = document.location.toString();
const brandName = document.querySelector<HTMLDivElement>(
`[data-component^="product-details:ProductDetailsBrandCollection"]`,
)!.innerText;
)?.innerText;
const title = document.querySelector<HTMLDivElement>(
`[data-component^="product-details:ProductDetailsTitle"]`,
)!.innerText;
const price = document.querySelector<HTMLDivElement>(`#standard-price`)!.innerText;
const rate = /\d\.\d/.exec(
document.querySelector<HTMLDivElement>(`[data-component^="ratings-and-reviews"] .sui-mr-1`)!
.innerText,
)![0];
const reviewCount = Number(
/[\d]+/.exec(
document.querySelector<HTMLDivElement>(
`[data-component^="ratings-and-reviews"] button > span:last-child`,
)!.innerText,
)![0],
const price = document
.querySelector<HTMLDivElement>(`#standard-price`)!
.innerText.replaceAll('\n', '');
const rateEl = document.querySelector<HTMLDivElement>(
`[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<HTMLDivElement>(
`[data-component^="ratings-and-reviews"] [name="simple-rating"] + span`,
)!.innerText,
)![0],
)
: undefined;
const mainImageUrl = document.querySelector<HTMLImageElement>(
`.mediagallery__mainimage img`,
)!.src;

View File

@ -1,9 +1,12 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
<template>
<main>
<h1>采集结果</h1>
<result-table class="result-table" />
<h1 class="header-title">采集结果</h1>
<!-- <result-table class="result-table" /> -->
<router-view />
</main>
</template>
@ -13,6 +16,10 @@ main {
flex-direction: column;
align-items: center;
.header-title {
cursor: default;
}
.result-table {
height: 90vh;
width: 95vw;

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import { NButton, NSpace } from 'naive-ui';
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
import { createWorkbook, Header, importFromXLSX } from '~/logic/data-io';
import type { AmazonDetailItem, AmazonItem, AmazonReview } from '~/logic/page-worker/types';
import { allItems, reviewItems } from '~/logic/storage';
import DetailDescription from './DetailDescription.vue';
import ReviewPreview from './ReviewPreview.vue';
import DetailDescription from '~/components/DetailDescription.vue';
import ReviewPreview from '~/components/ReviewPreview.vue';
const message = useMessage();
const modal = useModal();
@ -243,11 +243,12 @@ const handleExport = async () => {
return a;
}, []);
const sheet1 = exportToXLSX(items, { headers: itemHeaders, asWorkSheet: true });
const wb = sheet1.toWorkbook('items');
const sheet2 = exportToXLSX(reviews, { headers: reviewHeaders, asWorkSheet: true });
wb.addSheet('reviews', sheet2);
wb.exportFile(`Items ${dayjs().format('YYYY-MM-DD')}.xlsx`);
const wb = createWorkbook();
const sheet1 = wb.addSheet('items');
await sheet1.readJson(items, { headers: itemHeaders });
const sheet2 = wb.addSheet('reviews');
await sheet2.readJson(reviews, { headers: reviewHeaders });
await wb.exportFile(`Items ${dayjs().format('YYYY-MM-DD')}.xlsx`);
message.info('导出完成');
};
@ -255,13 +256,15 @@ const handleImport = async (file: File) => {
const itemHeaders = getItemHeaders();
const wb = await importFromXLSX(file, { asWorkBook: true });
const sheet1 = wb.getSheet(0);
const items = sheet1.toJson<AmazonItem>({ headers: itemHeaders });
const sheet1 = wb.getSheet(0)!;
const items = await sheet1.toJson<AmazonItem>({ headers: itemHeaders });
allItems.value = items;
if (wb.sheetCount > 1) {
const sheet2 = wb.getSheet(1);
const reviews = sheet2.toJson<AmazonReview & { asin?: string }>({ headers: reviewHeaders });
const sheet2 = wb.getSheet(1)!;
const reviews = await sheet2.toJson<AmazonReview & { asin?: string }>({
headers: reviewHeaders,
});
reviewItems.value = reviews.reduce((m, r) => {
const asin = r.asin!;
delete r.asin;

27
src/router/index.ts Normal file
View File

@ -0,0 +1,27 @@
import { Plugin } from 'vue';
import { createRouter, createMemoryHistory, RouteRecordRaw } from 'vue-router';
import { useAppContext } from '~/composables/useAppContext';
const routeObj: Record<AppContext, RouteRecordRaw[]> = {
options: [
{ path: '/', redirect: '/amazon' },
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
],
sidepanel: [
{ path: '/', redirect: '/amazon' },
{ path: '/amazon', component: () => import('~/sidepanel/views/AmazonSidepanel.vue') },
{ path: '/homedepot', component: () => import('~/sidepanel/views/HomedepotSidepanel.vue') },
],
};
export const router: Plugin = {
install(app) {
const { appContext: context } = useAppContext();
const routes = routeObj[context];
const router = createRouter({
history: createMemoryHistory(),
routes,
});
app.use(router);
},
};

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { GlobalThemeOverrides } from 'naive-ui';
import SidePanel from './Sidepanel.vue';
import { useRouter } from 'vue-router';
import { useCurrentUrl } from '~/composables/useCurrentUrl';
const theme: GlobalThemeOverrides = {
common: {
@ -10,6 +11,25 @@ const theme: GlobalThemeOverrides = {
primaryColorSuppl: '#003366',
},
};
const currentUrl = useCurrentUrl();
const router = useRouter();
watch(currentUrl, (newVal) => {
if (newVal) {
const url = new URL(newVal);
switch (url.hostname) {
case 'www.amazon.com':
router.push('/amazon');
break;
case 'www.homedepot.com':
router.push('/homedepot');
break;
default:
break;
}
}
});
</script>
<template>
@ -18,7 +38,7 @@ const theme: GlobalThemeOverrides = {
<n-message-provider>
<n-dialog-provider>
<n-modal-provider>
<side-panel />
<router-view />
</n-modal-provider>
</n-dialog-provider>
</n-message-provider>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import DetailPageEntry from './DetailPageEntry.vue';
import SearchPageEntry from './SearchPageEntry.vue';
import ReviewPageEntry from './ReviewPageEntry.vue';
import DetailPageEntry from './AmazonEntries/DetailPageEntry.vue';
import SearchPageEntry from './AmazonEntries/SearchPageEntry.vue';
import ReviewPageEntry from './AmazonEntries/ReviewPageEntry.vue';
const tabs = [
{
@ -17,6 +17,7 @@ const tabs = [
component: ReviewPageEntry,
},
];
const selectedTab = ref(tabs[0].name);
const currentComponent = computed(() => {
const tab = tabs.find((tab) => tab.name === selectedTab.value);

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { homedepot } from '~/logic/page-worker';
const inputText = ref('');
const output = ref(undefined);
const worker = homedepot.useHomedepotWorker();
worker.channel.on('detail-item-collected', ({ item }) => {
output.value = item;
});
const handleStart = () => {
worker.runDetailPageTask(inputText.value.split('\n').filter((id) => /\d+/.exec(id)));
};
</script>
<template>
<div class="homedepot-sidepanel">
<h3>Hello World!</h3>
<n-input type="textarea" v-model:value="inputText" />
<n-button @click="handleStart">Test!</n-button>
<n-code word-wrap :code="JSON.stringify(output)" />
</div>
</template>
<style lang="scss" scoped>
.homedepot-sidepanel {
display: flex;
flex-direction: column;
align-items: center;
}
</style>