Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea4bfcdbc | ||
|
|
dfee73b3f9 | ||
|
|
c9af72dc85 | ||
|
|
dc39e468bb |
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/asserts/favicon.ico" />
|
<link rel="icon" href="/asserts/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||||
<title>LionWebsite</title>
|
<title>LionWebsite</title>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Generated
+86
-78
@@ -10,15 +10,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.0.0",
|
"axios": "^1.0.0",
|
||||||
"element-plus": "^2.2.15",
|
"element-plus": "^2.2.15",
|
||||||
|
"lionwebsite-frontend-shared": "file:../lionwebsite-frontend-shared",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "6.0.9",
|
"@vitejs/plugin-vue": "6.0.9",
|
||||||
"vite": "8.3.0"
|
"vite": "8.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../lionwebsite-frontend-shared": {
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
@@ -115,9 +119,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.150.0",
|
"version": "0.155.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
|
||||||
"integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==",
|
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -136,9 +140,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
|
||||||
"integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==",
|
"integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -153,9 +157,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
|
||||||
"integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==",
|
"integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -170,9 +174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
|
||||||
"integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==",
|
"integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -187,9 +191,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
|
||||||
"integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==",
|
"integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -204,9 +208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
|
||||||
"integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==",
|
"integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -221,9 +225,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
|
||||||
"integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==",
|
"integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -238,9 +242,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
|
||||||
"integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==",
|
"integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -258,9 +262,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
|
||||||
"integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==",
|
"integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -278,9 +282,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
|
||||||
"integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==",
|
"integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -298,9 +302,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
|
||||||
"integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==",
|
"integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -318,9 +322,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
|
||||||
"integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==",
|
"integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -338,9 +342,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
|
||||||
"integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==",
|
"integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -358,9 +362,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
|
||||||
"integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==",
|
"integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -375,9 +379,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
|
||||||
"integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==",
|
"integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -392,9 +396,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
|
||||||
"integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==",
|
"integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -848,9 +852,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.16.0",
|
"version": "1.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
||||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -1281,6 +1285,10 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lionwebsite-frontend-shared": {
|
||||||
|
"resolved": "../lionwebsite-frontend-shared",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.18.1",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
@@ -1461,13 +1469,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
|
||||||
"integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==",
|
"integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.150.0",
|
"@oxc-project/types": "=0.155.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1477,21 +1485,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm-eabi": "1.2.9",
|
"@rolldown/binding-android-arm-eabi": "1.2.15",
|
||||||
"@rolldown/binding-android-arm64": "1.2.9",
|
"@rolldown/binding-android-arm64": "1.2.15",
|
||||||
"@rolldown/binding-darwin-arm64": "1.2.9",
|
"@rolldown/binding-darwin-arm64": "1.2.15",
|
||||||
"@rolldown/binding-darwin-x64": "1.2.9",
|
"@rolldown/binding-darwin-x64": "1.2.15",
|
||||||
"@rolldown/binding-freebsd-x64": "1.2.9",
|
"@rolldown/binding-freebsd-x64": "1.2.15",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.9",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.2.9",
|
"@rolldown/binding-linux-arm64-gnu": "1.2.15",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.2.9",
|
"@rolldown/binding-linux-arm64-musl": "1.2.15",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.9",
|
"@rolldown/binding-linux-ppc64-gnu": "1.2.15",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.2.9",
|
"@rolldown/binding-linux-s390x-gnu": "1.2.15",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.2.9",
|
"@rolldown/binding-linux-x64-gnu": "1.2.15",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.2.9",
|
"@rolldown/binding-linux-x64-musl": "1.2.15",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.2.9",
|
"@rolldown/binding-openharmony-arm64": "1.2.15",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.2.9",
|
"@rolldown/binding-win32-arm64-msvc": "1.2.15",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.2.9"
|
"@rolldown/binding-win32-x64-msvc": "1.2.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
@@ -1567,9 +1575,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -1593,16 +1601,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz",
|
||||||
"integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
|
"integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.33.0",
|
"lightningcss": "^1.33.0",
|
||||||
"picomatch": "^4.0.7",
|
"picomatch": "^4.0.7",
|
||||||
"postcss": "^8.5.28",
|
"postcss": "^8.5.28",
|
||||||
"rolldown": "~1.2.6",
|
"rolldown": "~1.2.11",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+3
-2
@@ -14,10 +14,11 @@
|
|||||||
"element-plus": "^2.2.15",
|
"element-plus": "^2.2.15",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2",
|
||||||
|
"lionwebsite-frontend-shared": "file:../lionwebsite-frontend-shared"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "6.0.9",
|
"@vitejs/plugin-vue": "6.0.9",
|
||||||
"vite": "8.3.0"
|
"vite": "8.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, ref, watch, onBeforeUnmount} from "vue";
|
import {computed, ref, watch, onBeforeUnmount} from "vue";
|
||||||
import {startImages, settleImage, retryImage} from "../utils/progressiveImages.js";
|
import {startImages, settleImage, retryImage} from "lionwebsite-frontend-shared";
|
||||||
import store from "../store/index.js";
|
import store from "../store/index.js";
|
||||||
let onlineReadingScrollbar = ref()
|
let onlineReadingScrollbar = ref()
|
||||||
let links = ref()
|
let links = ref()
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import axios from "axios";
|
|||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import store from "../store/index.js";
|
import store from "../store/index.js";
|
||||||
import TaskRow from "./TaskRow.vue";
|
import TaskRow from "./TaskRow.vue";
|
||||||
import {validateLink} from "../utils/validate.js";
|
import {fetchGalleries, validateLink} from "lionwebsite-frontend-shared";
|
||||||
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
import {ehThumbnailUrl} from "lionwebsite-frontend-shared";
|
||||||
|
|
||||||
const mode = ref("local");
|
const mode = ref("local");
|
||||||
const localMode = ref("keyword");
|
const localMode = ref("keyword");
|
||||||
@@ -56,32 +56,20 @@ async function retryGallery(gallery) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryGalleries(link) {
|
// direction 为 "next"/"prev" 时使用后端返回的翻页游标;href 为空表示新关键词。
|
||||||
let keyword
|
function queryGalleries(href, direction = null) {
|
||||||
if (link != null) {
|
if (href == null && remoteKeyword.value.trim() === "") {
|
||||||
keyword = new URL(link).search.replace("?f_search=", "")
|
|
||||||
} else {
|
|
||||||
keyword = remoteKeyword.value
|
|
||||||
if (keyword.trim() === "") {
|
|
||||||
ElMessage("请输入搜索关键词")
|
ElMessage("请输入搜索关键词")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
remoteLoading.value = true
|
remoteLoading.value = true
|
||||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + keyword.replace(" ", "+"))
|
fetchGalleries(axios, {keyword: remoteKeyword.value, direction, href: href || ""})
|
||||||
.then((res) => {
|
.then(({galleries, pagination}) => {
|
||||||
if (res.data.result === "success") {
|
remotePage.value = pagination
|
||||||
remotePage.value = {
|
remoteGalleries.value.splice(0, remoteGalleries.value.length, ...galleries)
|
||||||
first: res.data.first,
|
})
|
||||||
previous: res.data.previous,
|
.catch(error => {
|
||||||
next: res.data.next,
|
ElMessage({message: error.payload || error.message || "搜索失败", type: "error"})
|
||||||
last: res.data.last,
|
|
||||||
}
|
|
||||||
remoteGalleries.value.splice(0)
|
|
||||||
JSON.parse(res.data.data).forEach(gallery => remoteGalleries.value.push(gallery))
|
|
||||||
} else {
|
|
||||||
ElMessage({message: res.data.data, type: "error"})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
remoteLoading.value = false
|
remoteLoading.value = false
|
||||||
@@ -186,8 +174,8 @@ function parseLink() {
|
|||||||
|
|
||||||
<div class="pager-row" v-if="remoteGalleries.length">
|
<div class="pager-row" v-if="remoteGalleries.length">
|
||||||
<button type="button" :disabled="remotePage.first === undefined" @click="queryGalleries(remotePage.first)">首页</button>
|
<button type="button" :disabled="remotePage.first === undefined" @click="queryGalleries(remotePage.first)">首页</button>
|
||||||
<button type="button" :disabled="remotePage.previous === undefined" @click="queryGalleries(remotePage.previous)">上一页</button>
|
<button type="button" :disabled="remotePage.previous === undefined" @click="queryGalleries(remotePage.previous, 'prev')">上一页</button>
|
||||||
<button type="button" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next)">下一页</button>
|
<button type="button" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next, 'next')">下一页</button>
|
||||||
<button type="button" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</button>
|
<button type="button" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {computed, ref, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
import store from "../store/index.js";
|
import store from "../store/index.js";
|
||||||
import {ehThumbnailUrl, pickDefaultResolution} from "../utils/thumbnail.js";
|
import {ehThumbnailUrl, pickDefaultResolution} from "lionwebsite-frontend-shared";
|
||||||
|
|
||||||
// Two flows share this sheet: a task already in the list, and a remote gallery
|
// Two flows share this sheet: a task already in the list, and a remote gallery
|
||||||
// that was resolved from a link or from the online search.
|
// that was resolved from a link or from the online search.
|
||||||
@@ -19,6 +19,11 @@ watch(chosenGallery, (gallery) => {
|
|||||||
mode.value = "remote";
|
mode.value = "remote";
|
||||||
targetResolution.value = pickDefaultResolution(gallery.availableResolution);
|
targetResolution.value = pickDefaultResolution(gallery.availableResolution);
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
|
} else if (mode.value === "remote") {
|
||||||
|
// The remote decision was withdrawn elsewhere: close instead of leaving a
|
||||||
|
// stale task/remote mix behind the same sheet.
|
||||||
|
mode.value = "task";
|
||||||
|
isOpen.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,6 +32,11 @@ watch(detailGallery, (gallery) => {
|
|||||||
mode.value = "task";
|
mode.value = "task";
|
||||||
task.value = gallery;
|
task.value = gallery;
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
|
} else if (mode.value === "task") {
|
||||||
|
// Only the task flow closes on an empty detail; clearing the detail while a
|
||||||
|
// remote gallery is on screen must not tear the remote sheet down.
|
||||||
|
task.value = null;
|
||||||
|
isOpen.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,11 +54,12 @@ const resolutions = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// A resolved gallery still carries a raw path, so the proxy URL is built here.
|
// A gallery resolved through queryGalleryTask already has its thumb_link proxied
|
||||||
|
// by normalizeTask; only a raw upstream path (thumbnailUrl) needs the proxy here.
|
||||||
const remoteThumb = computed(() => {
|
const remoteThumb = computed(() => {
|
||||||
const gallery = chosenGallery.value;
|
const gallery = chosenGallery.value;
|
||||||
if (!gallery) return "";
|
if (!gallery) return "";
|
||||||
return ehThumbnailUrl(gallery.thumb_link || gallery.thumbnailUrl, store.state.AuthCode);
|
return gallery.thumb_link || ehThumbnailUrl(gallery.thumbnailUrl, store.state.AuthCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const downloaderFilter = computed({
|
|||||||
// downloader that would filter the visible list down to nothing.
|
// downloader that would filter the visible list down to nothing.
|
||||||
const downloaderOptions = computed(() => {
|
const downloaderOptions = computed(() => {
|
||||||
const seen = new Map();
|
const seen = new Map();
|
||||||
;(store.state.currentTasks || []).forEach(task => {
|
;(store.getters.categoryGallery || []).forEach(task => {
|
||||||
if (task.downloaderName && !seen.has(task.downloader))
|
if (task.downloaderName && !seen.has(task.downloader))
|
||||||
seen.set(task.downloader, task.downloaderName);
|
seen.set(task.downloader, task.downloaderName);
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-2
@@ -1,9 +1,17 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import {
|
||||||
|
ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage, ElInput,
|
||||||
|
ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch,
|
||||||
|
} from "element-plus"
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import element from "element-plus"
|
|
||||||
import "./reset.css"
|
import "./reset.css"
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import "./styles/theme.css"
|
import "./styles/theme.css"
|
||||||
|
|
||||||
createApp(App).use(element).mount('#app')
|
// 只注册本仓库用到的组件,避免把整个 Element Plus 打进入口包。
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const component of [ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage,
|
||||||
|
ElInput, ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch])
|
||||||
|
app.component(component.name, component)
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
+8
-604
@@ -2,11 +2,7 @@ import vuex from "vuex"
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {ElMessage} from "element-plus"
|
import {ElMessage} from "element-plus"
|
||||||
import qs from "qs"
|
import qs from "qs"
|
||||||
import {createTaskSocket} from "../utils/taskSocket.js"
|
import {createGalleryStore} from "lionwebsite-frontend-shared"
|
||||||
let taskSocket
|
|
||||||
let taskRefreshVersion = 0
|
|
||||||
const BaseUrl = "https://downloader.lionwebsite.xyz/"
|
|
||||||
const GalleryManageUrl = BaseUrl + "GalleryManage"
|
|
||||||
|
|
||||||
// Axios 全局错误处理
|
// Axios 全局错误处理
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
@@ -17,602 +13,10 @@ axios.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const actions = {
|
// 移动端与桌面端共用同一份任务业务核心,移动端用“继续加载”代替分页条。
|
||||||
reconnect(context) {
|
export default new vuex.Store(createGalleryStore({
|
||||||
axios.post(GalleryManageUrl + "/reconnect", null, {
|
variant: "mobile",
|
||||||
params: {
|
axios,
|
||||||
AuthCode: context.state.AuthCode
|
qs,
|
||||||
}
|
Message: ElMessage,
|
||||||
}).then(res => {
|
}))
|
||||||
if (res.data.result === "success") {
|
|
||||||
ElMessage("重连成功")
|
|
||||||
} else {
|
|
||||||
ElMessage("重连失败")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateGalleryTasks(context){
|
|
||||||
const version = ++taskRefreshVersion
|
|
||||||
return axios.get(GalleryManageUrl, {
|
|
||||||
params:{
|
|
||||||
AuthCode: context.state.AuthCode,
|
|
||||||
type: 'all'
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
if(res.data.result === "success" && version === taskRefreshVersion)
|
|
||||||
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
postGalleryTask(context, data){
|
|
||||||
return axios.post(GalleryManageUrl + '?' + qs.stringify({
|
|
||||||
AuthCode: context.state.AuthCode,
|
|
||||||
...data
|
|
||||||
}, {indices:false})).then((res) => {
|
|
||||||
if(res.data.result === "success") {
|
|
||||||
ElMessage("提交成功")
|
|
||||||
context.commit("_setChosenGallery", {gallery: false})
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if(res.data.data)
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
else
|
|
||||||
ElMessage("提交失败")
|
|
||||||
// A node timeout can still leave a persisted task; always reload its actual state.
|
|
||||||
return Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")])
|
|
||||||
})
|
|
||||||
},
|
|
||||||
queryGalleryTask(context, link){
|
|
||||||
axios.get(GalleryManageUrl, {
|
|
||||||
params:{
|
|
||||||
param: link,
|
|
||||||
type: 'link',
|
|
||||||
AuthCode: context.state.AuthCode
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
if(res.data.result === 'success'){
|
|
||||||
let gallery = JSON.parse(res.data.data)
|
|
||||||
if(gallery.status === "下载完成")
|
|
||||||
gallery.download = buildGalleryDownloadUrl(gallery.gid, context.state.AuthCode)
|
|
||||||
context.commit("_setChosenGallery", {gallery})
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ElMessage("查询失败")
|
|
||||||
})
|
|
||||||
},
|
|
||||||
validate(context, AuthCode){
|
|
||||||
axios.post(BaseUrl + "validate?AuthCode=" + AuthCode).then((res)=>{
|
|
||||||
if(res.data.result === 'success'){
|
|
||||||
let data = JSON.parse(res.data.data);
|
|
||||||
if(!data.isAvailable)
|
|
||||||
ElMessage({duration:0, message:"节点挂了,不能下也不能看,找狮子处理", type: "error"})
|
|
||||||
context.commit("_authed", {AuthCode, ...data})
|
|
||||||
//初始化
|
|
||||||
context.dispatch("loadWeekUsedAmount").then()
|
|
||||||
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
|
|
||||||
context.dispatch("initWebsocket").then()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
context.dispatch("disconnectWebsocket")
|
|
||||||
context.commit("_unAuthed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initWebsocket(context){
|
|
||||||
taskSocket?.stop()
|
|
||||||
taskSocket = createTaskSocket({
|
|
||||||
url: "wss://downloader.lionwebsite.xyz/ws/",
|
|
||||||
onState: status => context.commit("_setConnectionStatus", status),
|
|
||||||
onOpen: () => {
|
|
||||||
// Catch up on every connection, including events missed during an outage.
|
|
||||||
Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
|
|
||||||
},
|
|
||||||
onMessage: event => {
|
|
||||||
let message
|
|
||||||
try { message = JSON.parse(event.data) }
|
|
||||||
catch { return }
|
|
||||||
if(message.type === "updateTasks" && Array.isArray(message.data))
|
|
||||||
context.commit("_updateGalleryTaskProceeding", message.data)
|
|
||||||
else if(message.type === "fullUpdate")
|
|
||||||
context.dispatch("updateGalleryTasks").catch(() => {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
taskSocket.start()
|
|
||||||
},
|
|
||||||
disconnectWebsocket(context){
|
|
||||||
taskSocket?.stop()
|
|
||||||
taskSocket = undefined
|
|
||||||
context.commit("_setConnectionStatus", "disconnected")
|
|
||||||
},
|
|
||||||
loadWeekUsedAmount(context){
|
|
||||||
return axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
|
||||||
params: {
|
|
||||||
AuthCode: context.state.AuthCode
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
if(res.data.result === "success"){
|
|
||||||
context.state.weekUsed = JSON.parse(res.data.data)
|
|
||||||
}else
|
|
||||||
ElMessage("查询用量失败")
|
|
||||||
})
|
|
||||||
},
|
|
||||||
collectGallery(context, gid){
|
|
||||||
axios.post(GalleryManageUrl + "/collect?" +qs.stringify( {
|
|
||||||
gid, AuthCode:context.state.AuthCode
|
|
||||||
})).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_collectGallery", gid)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
disCollectGallery(context, gid){
|
|
||||||
axios.post(GalleryManageUrl + "/disCollect?" + qs.stringify({
|
|
||||||
gid, AuthCode:context.state.AuthCode
|
|
||||||
})).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_disCollectGallery", gid)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteGallery(context, gid){
|
|
||||||
return axios.delete(GalleryManageUrl, {
|
|
||||||
params:{
|
|
||||||
AuthCode:context.state.AuthCode, gid
|
|
||||||
}}).then((res) => {
|
|
||||||
if(res.data.result === "success"){
|
|
||||||
ElMessage("删除成功")
|
|
||||||
context.commit("_deleteGallery", gid)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
retryGallery(context, gid){
|
|
||||||
return axios.post(GalleryManageUrl + "/retry?" + qs.stringify({
|
|
||||||
gid, AuthCode: context.state.AuthCode
|
|
||||||
})).then((res) => {
|
|
||||||
if(res.data.result === "success"){
|
|
||||||
ElMessage({message: "重试结果:" + res.data.data, type: "success"})
|
|
||||||
return context.dispatch("updateGalleryTasks")
|
|
||||||
}
|
|
||||||
ElMessage({message: res.data.data || "重试失败", type: "error"})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
readOnlineGallery(context, gallery){
|
|
||||||
if(gallery.images !== undefined && gallery.images.length !== 0)
|
|
||||||
context.commit("_setReadingGallery", gallery)
|
|
||||||
else
|
|
||||||
return axios.post(GalleryManageUrl + "/cache", null, {
|
|
||||||
params: {url: gallery.link, AuthCode: context.state.AuthCode}
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.data.result === 'success') {
|
|
||||||
gallery.pages = res.data.data.pages
|
|
||||||
setTimeout(() => {
|
|
||||||
context.commit("_setReadingGallery", gallery)
|
|
||||||
}, 100)
|
|
||||||
} else
|
|
||||||
ElMessage.error(res.data.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
alterAuthCode(context, AuthCode){
|
|
||||||
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': context.state.AuthCode, 'newAuthCode': AuthCode}))
|
|
||||||
.then((res) => {
|
|
||||||
if(res.data.result === 'success') {
|
|
||||||
ElMessage("修改成功")
|
|
||||||
if(localStorage.getItem("auth") === context.state.AuthCode)
|
|
||||||
localStorage.setItem("auth", AuthCode)
|
|
||||||
context.state.AuthCode = AuthCode
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resetUndone(context){
|
|
||||||
axios.post(GalleryManageUrl + "/reset?AuthCode=" + context.state.AuthCode).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
_setConnectionStatus(state, status){
|
|
||||||
state.connectionStatus = status
|
|
||||||
},
|
|
||||||
_collectGallery(state, gid){
|
|
||||||
let tasks = state.totalGalleryTask
|
|
||||||
for(let i=0; i< tasks.length; i++){
|
|
||||||
if(!tasks[i].isCollect && tasks[i].gid === gid){
|
|
||||||
tasks[i].isCollect = true
|
|
||||||
state.collectGallery.push(tasks[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_disCollectGallery(state, gid){
|
|
||||||
let index
|
|
||||||
for(let i=0; i < state.collectGallery.length; i++)
|
|
||||||
if(state.collectGallery[i].gid === gid){
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
state.collectGallery[index].isCollect = false
|
|
||||||
state.collectGallery.splice(index, 1)
|
|
||||||
},
|
|
||||||
_updateGalleryTasks(state, tasks){
|
|
||||||
state.totalGalleryTask.splice(0)
|
|
||||||
state.collectGallery.splice(0)
|
|
||||||
state.downloadGallery.splice(0)
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
//处理名字
|
|
||||||
task.shortName = getShortname(task.name)
|
|
||||||
if(task.thumb_link.trim() !== '')
|
|
||||||
task.thumb_link = buildGalleryManageUrl("/ehThumbnail", {
|
|
||||||
path: task.thumb_link,
|
|
||||||
AuthCode: state.AuthCode
|
|
||||||
})
|
|
||||||
else
|
|
||||||
delete task.thumb_link
|
|
||||||
|
|
||||||
//处理进度相关
|
|
||||||
switch (task.status) {
|
|
||||||
case "已提交":
|
|
||||||
case "等待压缩":
|
|
||||||
case "压缩中":
|
|
||||||
task.progress = task.status
|
|
||||||
break;
|
|
||||||
case "下载中":
|
|
||||||
task.progress = (Math.round((task.proceeding / task.pages) * 100)).toString() + "%"
|
|
||||||
break;
|
|
||||||
case "下载完成":
|
|
||||||
task.progress = task.status
|
|
||||||
task.download = buildGalleryDownloadUrl(task.gid, state.AuthCode)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
//处理时间戳
|
|
||||||
task.createTimeDisplay = new Date(task.createTime * 1000).toLocaleString("zh")
|
|
||||||
|
|
||||||
//处理是否收藏
|
|
||||||
if('isCollect' in task)
|
|
||||||
state.collectGallery.push(task)
|
|
||||||
else
|
|
||||||
task.isCollect = false
|
|
||||||
|
|
||||||
//处理是否下载
|
|
||||||
if(task.downloader === state.userId)
|
|
||||||
state.downloadGallery.push(task)
|
|
||||||
|
|
||||||
state.totalGalleryTask.push(task)
|
|
||||||
})
|
|
||||||
|
|
||||||
sortTasks(state)
|
|
||||||
if(state.isAuth && !state.loadComplete){
|
|
||||||
state.loadComplete = true
|
|
||||||
ElMessage("加载完成")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_updateGalleryTaskProceeding(state, tasks){
|
|
||||||
let galleries = Array.from(state.totalGalleryTask)
|
|
||||||
state.totalGalleryTask.splice(0)
|
|
||||||
galleries.forEach((gallery) => {
|
|
||||||
if(gallery.status !== '下载完成')
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
if(task.gid === gallery.gid){
|
|
||||||
gallery.status = status[task.status]
|
|
||||||
gallery.proceeding = task.proceeding
|
|
||||||
if(gallery.status === '下载中')
|
|
||||||
gallery.progress = (Math.round((gallery.proceeding / gallery.pages) * 100)).toString() + "%"
|
|
||||||
else
|
|
||||||
gallery.progress = gallery.status
|
|
||||||
}
|
|
||||||
})
|
|
||||||
state.totalGalleryTask.push(gallery)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
_changePage(state, targetPage){
|
|
||||||
state.page = targetPage
|
|
||||||
},
|
|
||||||
_setActiveTab(state, tab){
|
|
||||||
state.activeTab = tab
|
|
||||||
},
|
|
||||||
_setDetailGallery(state, gallery){
|
|
||||||
state.detailGallery = gallery
|
|
||||||
},
|
|
||||||
// The mobile list grows in place instead of paging through a footer control.
|
|
||||||
_showMoreTasks(state){
|
|
||||||
state.visiblePages += 1
|
|
||||||
},
|
|
||||||
_authed(state, data){
|
|
||||||
state.AuthCode = data.AuthCode
|
|
||||||
state.userId = data.userId
|
|
||||||
state.username = data.username
|
|
||||||
state.isAdmin = Boolean(data.isAdmin)
|
|
||||||
state.downloaderFilter = null
|
|
||||||
state.isAuth = true
|
|
||||||
ElMessage("验证成功,加载中")
|
|
||||||
},
|
|
||||||
_unAuthed(state){
|
|
||||||
state.isAuth = false
|
|
||||||
state.AuthCode = ""
|
|
||||||
state.userId = -1
|
|
||||||
state.username = ""
|
|
||||||
ElMessage("授权码错误")
|
|
||||||
localStorage.removeItem("auth")
|
|
||||||
},
|
|
||||||
_searchLocalByLink(state, link){
|
|
||||||
let tasks = state.currentTasks
|
|
||||||
let gid = link.split("/")[4]
|
|
||||||
let matched = null
|
|
||||||
let name = null
|
|
||||||
|
|
||||||
if(gid === undefined)
|
|
||||||
for (let i = 0; i < tasks.length; i++) {
|
|
||||||
if (tasks[i].link === link) {
|
|
||||||
state.page = Math.floor(i / state.length) + 1
|
|
||||||
matched = tasks[i]
|
|
||||||
name = tasks[i].name
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else for (let i = 0; i < tasks.length; i++)
|
|
||||||
if (String(tasks[i].gid) === gid) {
|
|
||||||
state.page = Math.floor(i / state.length) + 1
|
|
||||||
matched = tasks[i]
|
|
||||||
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!name)
|
|
||||||
ElMessage("未找到此任务")
|
|
||||||
else {
|
|
||||||
state.searchTask.splice(0)
|
|
||||||
if(matched)
|
|
||||||
state.searchTask.push(matched)
|
|
||||||
state.isSearch = true
|
|
||||||
state.visiblePages = 1
|
|
||||||
ElMessage("已跳转到该任务所在页数,任务名:" + name)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_searchLocalByKeyword(state, keyword){
|
|
||||||
state.searchTask.splice(0)
|
|
||||||
if(keyword.trim() !== '') {
|
|
||||||
state.page = 1
|
|
||||||
state.visiblePages = 1
|
|
||||||
let tasks = state.currentTasks
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
if (task.name.includes(keyword))
|
|
||||||
state.searchTask.push(task)
|
|
||||||
})
|
|
||||||
if (state.searchTask.length === 0) {
|
|
||||||
ElMessage("未找到该关键字的任务")
|
|
||||||
state.isSearch = false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.isSearch = true
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
state.isSearch = false
|
|
||||||
confirmCurrentTask(state)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_deleteGallery(state, gid){
|
|
||||||
let tasks = [state.totalGalleryTask, state.downloadGallery, state.collectGallery]
|
|
||||||
deleteTask(tasks, 'gid', gid)
|
|
||||||
},
|
|
||||||
_setChosenGallery(state,data){
|
|
||||||
state.chosenGallery = data.gallery
|
|
||||||
},
|
|
||||||
_setCategory(state, category){
|
|
||||||
state.category = category
|
|
||||||
state.visiblePages = 1
|
|
||||||
// 换分类后旧的下载人筛选可能在新分类里根本不存在,先清掉。
|
|
||||||
state.downloaderFilter = null
|
|
||||||
confirmCurrentTask(state)
|
|
||||||
sortTasks(state)
|
|
||||||
},
|
|
||||||
_setSortType(state, sortType){
|
|
||||||
state.sortType = sortType
|
|
||||||
state.visiblePages = 1
|
|
||||||
sortTasks(state)
|
|
||||||
},
|
|
||||||
_setGalleryNameType(state, galleryNameType){
|
|
||||||
state.galleryNameType = galleryNameType
|
|
||||||
},
|
|
||||||
_setDownloaderFilter(state, downloader){
|
|
||||||
state.downloaderFilter = downloader === null || downloader === undefined
|
|
||||||
? null
|
|
||||||
: Number(downloader)
|
|
||||||
state.visiblePages = 1
|
|
||||||
},
|
|
||||||
_setShowNameType(state, type){
|
|
||||||
if(type === "shortName")
|
|
||||||
state.length = state.shortLength
|
|
||||||
else
|
|
||||||
state.length = state.defaultLength
|
|
||||||
},
|
|
||||||
_setReadingGallery(state, gallery){
|
|
||||||
if(gallery.images === undefined) {
|
|
||||||
gallery.images = []
|
|
||||||
for(let i=1; i<=gallery.pages; i++)
|
|
||||||
gallery.images.push(buildGalleryManageUrl("/onlineImage/" + i, {
|
|
||||||
gid: gallery.gid,
|
|
||||||
AuthCode: state.AuthCode
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
state.readingGallery = gallery
|
|
||||||
state.isReading = true;
|
|
||||||
},
|
|
||||||
_closeReader(state){
|
|
||||||
state.isReading = false;
|
|
||||||
},
|
|
||||||
_openHistoryPanel(state){
|
|
||||||
state.isShowHistory = true
|
|
||||||
},
|
|
||||||
_closeHistoryPanel(state){
|
|
||||||
state.isShowHistory = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
connectionStatus: "disconnected", // live task updates
|
|
||||||
websocket: null, //websocket
|
|
||||||
|
|
||||||
totalGalleryTask: [], //存放数据的数组
|
|
||||||
chosenGallery: false, //准备下载
|
|
||||||
collectGallery: [], //收藏
|
|
||||||
downloadGallery: [], //下载
|
|
||||||
isSearch: false, //用于决定是否显示搜索结果
|
|
||||||
|
|
||||||
readingGallery: {'name': '', 'images': []}, //在线看
|
|
||||||
isReading: false, //是否正在看
|
|
||||||
|
|
||||||
currentGid: "", //当前GID
|
|
||||||
lengthPerPage: 0, //在线预览每页图片数量
|
|
||||||
|
|
||||||
page: 1, //当前页数
|
|
||||||
length: 5, //每页能有多少个链接
|
|
||||||
defaultLength: 4, //默认个数
|
|
||||||
shortLength: 5, //简洁个数
|
|
||||||
|
|
||||||
userId: -1, //用户id
|
|
||||||
username: "", //用户名
|
|
||||||
isAuth: false, //是否授权
|
|
||||||
AuthCode: '', //授权码
|
|
||||||
loadComplete: false, //是否加载完成
|
|
||||||
galleryRefreshTimer: 0, //更新计时器id
|
|
||||||
|
|
||||||
isInclude: false, //是否搜索到任务
|
|
||||||
searchTask: [], //搜索到的任务
|
|
||||||
isShowHistory: false, //是否打开面板
|
|
||||||
activeTab: 'tasks', //移动端底部标签 tasks search settings
|
|
||||||
detailGallery: null, //详情抽屉里正在看的任务
|
|
||||||
visiblePages: 1, //列表已展开的页数,用于“继续加载”
|
|
||||||
galleryNameType: 'shortName', //名字类型 shortName name
|
|
||||||
category: 'myDownload', //分类 myDownload myCollect total
|
|
||||||
sortType:'shortName', //排序类型 shortName name createTime
|
|
||||||
currentTasks: [], //当前任务
|
|
||||||
weekUsed: {}, //每周用量
|
|
||||||
isAdmin: false, //管理员(后端 validate 下发)
|
|
||||||
downloaderFilter: null, //管理员按下载人筛选,null 表示全部
|
|
||||||
}
|
|
||||||
|
|
||||||
const getters = {
|
|
||||||
currentTasks(state){
|
|
||||||
const tasks = activeTaskList(state)
|
|
||||||
return tasks.slice((state.page - 1) * state.length, state.page * state.length)
|
|
||||||
},
|
|
||||||
min(){
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
visibleTasks(state){
|
|
||||||
return activeTaskList(state).slice(0, state.visiblePages * state.length)
|
|
||||||
},
|
|
||||||
taskTotal(state){
|
|
||||||
return activeTaskList(state).length
|
|
||||||
},
|
|
||||||
taskRemaining(state, getters){
|
|
||||||
return Math.max(0, getters.taskTotal - state.visiblePages * state.length)
|
|
||||||
},
|
|
||||||
searchResults(state){
|
|
||||||
return (state.searchTask || []).slice(0, state.visiblePages * state.length)
|
|
||||||
},
|
|
||||||
max(state){
|
|
||||||
let max = 0
|
|
||||||
const tasks = activeTaskList(state)
|
|
||||||
|
|
||||||
if(!tasks)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
max = Math.floor(tasks.length/state.length)
|
|
||||||
if(tasks.length % state.length !== 0 || max === 0)
|
|
||||||
max += 1
|
|
||||||
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new vuex.Store({
|
|
||||||
actions,
|
|
||||||
mutations,
|
|
||||||
state,
|
|
||||||
getters
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前视图实际要展示的任务。
|
|
||||||
* 下载人筛选只在管理员生效,作用于「我的下载/收藏/全部」三种分类的结果。
|
|
||||||
*/
|
|
||||||
function activeTaskList(state){
|
|
||||||
if(state.isSearch)
|
|
||||||
return state.searchTask
|
|
||||||
if(state.isAdmin && state.downloaderFilter !== null)
|
|
||||||
return state.currentTasks.filter(task => task.downloader === state.downloaderFilter)
|
|
||||||
return state.currentTasks
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShortname(name){
|
|
||||||
if(name === null)
|
|
||||||
return null
|
|
||||||
if (!name.includes("[")) return name
|
|
||||||
|
|
||||||
// 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签
|
|
||||||
const trimmed = name.substring(0, name.lastIndexOf("["))
|
|
||||||
.replace(/\s*\[[^\]]*\]\s*/g, '')
|
|
||||||
.replace(/\s*\([^\)]*\)\s*/g, '')
|
|
||||||
.trim()
|
|
||||||
// 名字以 [...] 开头时截取结果为空,退回原名避免整列空白
|
|
||||||
return trimmed === '' ? name.trim() : trimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGalleryDownloadUrl(gid, AuthCode){
|
|
||||||
let params = new URLSearchParams({AuthCode, gid: gid.toString()})
|
|
||||||
return GalleryManageUrl + "/file/" + gid + ".zip?" + params.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGalleryManageUrl(path, params){
|
|
||||||
return GalleryManageUrl + path + "?" + new URLSearchParams(params).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmCurrentTask(state){
|
|
||||||
switch (state.category){
|
|
||||||
case 'myDownload':
|
|
||||||
state.currentTasks = state.downloadGallery
|
|
||||||
break
|
|
||||||
case 'myCollect':
|
|
||||||
state.currentTasks = state.collectGallery
|
|
||||||
break
|
|
||||||
case 'total':
|
|
||||||
state.currentTasks = state.totalGalleryTask
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortTasks(state){
|
|
||||||
switch (state.sortType) {
|
|
||||||
case "name":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.name > after.name ? 1: -1
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "shortName":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.shortName > after.shortName ? 1: -1
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "createTime":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.createTime - after.createTime
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTask(tasks, key, value){
|
|
||||||
for(let j=0; j < tasks.length; j++)
|
|
||||||
for(let i=0; i<tasks[j].length; i++)
|
|
||||||
if(tasks[j][i][key] === value){
|
|
||||||
tasks[j].splice(i, 1)
|
|
||||||
i--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = ['已提交', '下载中', '等待压缩', '压缩中', '下载完成']
|
|
||||||
|
|||||||
@@ -1073,3 +1073,21 @@ body {
|
|||||||
padding: 10px 12px 8px;
|
padding: 10px 12px 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== iOS focus-zoom guard ==========
|
||||||
|
iOS Safari 会在聚焦字号小于 16px 的输入控件时放大整个页面,而且会忽略
|
||||||
|
viewport 里的 user-scalable=no / maximum-scale。把可聚焦控件的字号固定到
|
||||||
|
16px 才是可靠解法;按钮、标签等展示型元素不受影响。
|
||||||
|
|
||||||
|
这里用 !important 是必要的:Element Plus 的 .el-select--small / .el-input--small
|
||||||
|
等变体自带 13~14px 规则,优先级高于普通选择器。 */
|
||||||
|
input:not([type="checkbox"]):not([type="radio"]),
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
.el-input__inner,
|
||||||
|
.el-textarea__inner,
|
||||||
|
.el-select__input,
|
||||||
|
.el-select__wrapper,
|
||||||
|
.el-input-number .el-input__inner {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
function imageItem(url, index) {
|
|
||||||
return {url, src: url, index, attempt: 0, failed: false, settled: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startImages(urls) {
|
|
||||||
return urls.length ? [imageItem(urls[0], 0)] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
export function settleImage(items, urls, item, failed = false) {
|
|
||||||
// Ignore events from images removed by a page change or dialog close.
|
|
||||||
if (!items.includes(item)) return
|
|
||||||
item.failed = failed
|
|
||||||
if (item.settled) return
|
|
||||||
item.settled = true
|
|
||||||
if (items.length < urls.length)
|
|
||||||
items.push(imageItem(urls[items.length], items.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function retryImage(item) {
|
|
||||||
item.failed = false
|
|
||||||
item.attempt++
|
|
||||||
item.src = item.url + (item.url.includes('?') ? '&' : '?') + 'retry=' + item.attempt
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
export function createTaskSocket({url, onOpen, onMessage, onState,
|
|
||||||
createSocket = address => new WebSocket(address),
|
|
||||||
setTimer = setTimeout, clearTimer = clearTimeout}) {
|
|
||||||
let socket = null
|
|
||||||
let retryTimer
|
|
||||||
let connectTimer
|
|
||||||
let attempts = 0
|
|
||||||
let stopped = true
|
|
||||||
|
|
||||||
function detach() {
|
|
||||||
clearTimer(connectTimer)
|
|
||||||
connectTimer = undefined
|
|
||||||
if (socket) {
|
|
||||||
const previous = socket
|
|
||||||
socket = null
|
|
||||||
previous.onopen = previous.onclose = previous.onerror = previous.onmessage = null
|
|
||||||
try { previous.close() } catch { /* already closed */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function schedule() {
|
|
||||||
if (stopped) return
|
|
||||||
onState('reconnecting')
|
|
||||||
const delay = Math.min(1000 * 2 ** attempts, 30000)
|
|
||||||
attempts = Math.min(attempts + 1, 5)
|
|
||||||
clearTimer(retryTimer)
|
|
||||||
retryTimer = setTimer(connect, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
if (stopped) return
|
|
||||||
clearTimer(retryTimer)
|
|
||||||
retryTimer = undefined
|
|
||||||
onState(attempts ? 'reconnecting' : 'connecting')
|
|
||||||
let current
|
|
||||||
try { current = socket = createSocket(url) }
|
|
||||||
catch { schedule(); return }
|
|
||||||
const retry = () => {
|
|
||||||
if (stopped || socket !== current) return
|
|
||||||
detach()
|
|
||||||
schedule()
|
|
||||||
}
|
|
||||||
current.onopen = () => {
|
|
||||||
if (stopped || socket !== current) return
|
|
||||||
clearTimer(connectTimer)
|
|
||||||
connectTimer = undefined
|
|
||||||
attempts = 0
|
|
||||||
try { current.send('DownloaderWebsocket') }
|
|
||||||
catch { retry(); return }
|
|
||||||
onState('connected')
|
|
||||||
onOpen()
|
|
||||||
}
|
|
||||||
current.onmessage = event => {
|
|
||||||
if (!stopped && socket === current) onMessage(event)
|
|
||||||
}
|
|
||||||
current.onclose = retry
|
|
||||||
current.onerror = retry
|
|
||||||
connectTimer = setTimer(retry, 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start() {
|
|
||||||
if (!stopped) return
|
|
||||||
stopped = false
|
|
||||||
attempts = 0
|
|
||||||
connect()
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
stopped = true
|
|
||||||
clearTimer(retryTimer)
|
|
||||||
retryTimer = undefined
|
|
||||||
detach()
|
|
||||||
onState('disconnected')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const BaseUrl = "https://downloader.lionwebsite.xyz/";
|
|
||||||
|
|
||||||
// Remote galleries carry a raw thumbnail path; it only resolves through the
|
|
||||||
// backend proxy once the active auth code is attached.
|
|
||||||
export function ehThumbnailUrl(path, AuthCode) {
|
|
||||||
if (!path) return "";
|
|
||||||
return BaseUrl + "GalleryManage/ehThumbnail?" + new URLSearchParams({path, AuthCode}).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNIT_SCALE = {b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4};
|
|
||||||
|
|
||||||
function sizeInBytes(text) {
|
|
||||||
const match = String(text).match(/([\d.]+)\s*([kmgt]?)(?:i?b)/i);
|
|
||||||
if (!match) return 0;
|
|
||||||
const scale = UNIT_SCALE[match[2].toLowerCase()] ?? 1;
|
|
||||||
return Number(match[1]) * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the original file; otherwise take whichever entry reports the most bytes.
|
|
||||||
export function pickDefaultResolution(availableResolution) {
|
|
||||||
const entries = Object.entries(availableResolution || {});
|
|
||||||
if (entries.length === 0) return "";
|
|
||||||
const original = entries.find(([resolution]) => /original/i.test(resolution));
|
|
||||||
if (original) return original[0];
|
|
||||||
return entries.reduce((best, current) =>
|
|
||||||
sizeInBytes(current[1]) > sizeInBytes(best[1]) ? current : best)[0];
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export function validateLink(rawLink){
|
|
||||||
if(rawLink.trim() === "")
|
|
||||||
return false
|
|
||||||
if(rawLink.includes("hentai"))
|
|
||||||
return rawLink.includes("/g/")
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
import {startImages, settleImage, retryImage} from '../src/utils/progressiveImages.js'
|
|
||||||
|
|
||||||
test('a failed image does not prevent later pages from loading', () => {
|
|
||||||
const urls = ['first', 'second', 'third']
|
|
||||||
const items = startImages(urls)
|
|
||||||
settleImage(items, urls, items[0], true)
|
|
||||||
assert.equal(items[0].failed, true)
|
|
||||||
assert.equal(items[1].url, 'second')
|
|
||||||
settleImage(items, urls, items[1])
|
|
||||||
assert.equal(items[2].url, 'third')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('retry does not append a duplicate page and bypasses a failed URL cache', () => {
|
|
||||||
const urls = ['first?gid=1', 'second', 'third']
|
|
||||||
const items = startImages(urls)
|
|
||||||
settleImage(items, urls, items[0], true)
|
|
||||||
retryImage(items[0])
|
|
||||||
assert.equal(items[0].src, 'first?gid=1&retry=1')
|
|
||||||
settleImage(items, urls, items[0])
|
|
||||||
assert.equal(items.length, 2)
|
|
||||||
assert.equal(items[0].failed, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('late image events cannot append content after changing pages', () => {
|
|
||||||
const old = startImages(['old'])[0]
|
|
||||||
const urls = ['new', 'next']
|
|
||||||
const items = startImages(urls)
|
|
||||||
settleImage(items, urls, old)
|
|
||||||
assert.equal(items.length, 1)
|
|
||||||
assert.deepEqual(startImages([]), [])
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import test from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import {buildSearchParams, fetchGalleries, readResponse} from "lionwebsite-frontend-shared"
|
||||||
|
|
||||||
|
test("keyword request never hand-builds the query string", () => {
|
||||||
|
assert.deepEqual(buildSearchParams({keyword: "cat girl"}), {keyword: "cat girl"})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("next page forwards the upstream cursor instead of the keyword", () => {
|
||||||
|
const params = buildSearchParams({
|
||||||
|
keyword: "cat girl", direction: "next",
|
||||||
|
href: "https://exhentai.org/?f_search=cat+girl&next=777",
|
||||||
|
})
|
||||||
|
assert.equal(params.next, "777")
|
||||||
|
assert.equal(params.keyword, "cat girl")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fetchGalleries decodes object and JSON-string payloads identically", async () => {
|
||||||
|
const asObject = {get: async () => ({data: {result: "success", data: [{gid: 1}], next: "n1"}})}
|
||||||
|
const asString = {get: async () => ({data: {result: "success", data: '[{"gid":1}]', next: "n1"}})}
|
||||||
|
const one = await fetchGalleries(asObject, {keyword: "k"})
|
||||||
|
const two = await fetchGalleries(asString, {keyword: "k"})
|
||||||
|
assert.deepEqual(one.galleries, two.galleries)
|
||||||
|
assert.equal(one.pagination.next, "n1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fetchGalleries surfaces the backend message on failure", async () => {
|
||||||
|
const http = {get: async () => ({data: {result: "failure", data: "没有搜索到结果"}})}
|
||||||
|
await assert.rejects(() => fetchGalleries(http, {keyword: "k"}),
|
||||||
|
error => error.payload === "没有搜索到结果")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("readResponse keeps all four pagination cursors", () => {
|
||||||
|
const parsed = readResponse({data: {result: "success", data: "[]",
|
||||||
|
first: "f", previous: "p", next: "n", last: "l"}})
|
||||||
|
assert.deepEqual(parsed.pagination, {first: "f", previous: "p", next: "n", last: "l"})
|
||||||
|
})
|
||||||
|
|
||||||
+79
-83
@@ -1,95 +1,91 @@
|
|||||||
import test from 'node:test'
|
import test from "node:test"
|
||||||
import assert from 'node:assert/strict'
|
import assert from "node:assert/strict"
|
||||||
import fs from 'node:fs'
|
|
||||||
import vm from 'node:vm'
|
|
||||||
|
|
||||||
function fixture() {
|
// 直接加载真实的移动端 store;在 Node 里 import 到的就是下发到浏览器的那份配置。
|
||||||
let code = fs.readFileSync(new URL('../src/store/index.js', import.meta.url), 'utf8')
|
const store = (await import("../src/store/index.js")).default
|
||||||
.replace(/^import .*$/gm, '')
|
|
||||||
.replace('export default new vuex.Store(', 'globalThis.store = new vuex.Store(')
|
|
||||||
const messages = []
|
|
||||||
const showMessage = message => messages.push(message)
|
|
||||||
showMessage.error = message => messages.push(message)
|
|
||||||
const axios = {interceptors: {response: {use() {}}}}
|
|
||||||
const context = vm.createContext({axios, vuex: {Store: function (config) {Object.assign(this, config)}},
|
|
||||||
ElMessage: showMessage, URLSearchParams, console,
|
|
||||||
qs: {stringify: () => ''}})
|
|
||||||
vm.runInContext(code, context)
|
|
||||||
const store = context.store
|
|
||||||
const api = {state: store.state,
|
|
||||||
commit: (name, data) => store.mutations[name](store.state, data),
|
|
||||||
dispatch: (name, data) => Promise.resolve(store.actions[name](api, data))}
|
|
||||||
return {store, messages, axios, api}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('link search matches a numeric gid and jumps to the right page', () => {
|
const tasks = () => [
|
||||||
const {store, messages} = fixture()
|
{gid: 1, name: "beta [tag]", status: "下载完成", pages: 10, proceeding: 10,
|
||||||
store.state.currentTasks = [{gid: 1}, {gid: 2}, {gid: 12345, name: 'sample', shortName: 'sample'}]
|
thumb_link: "", createTime: 2, downloader: 7, isCollect: true},
|
||||||
|
{gid: 2, name: "alpha", status: "下载中", pages: 10, proceeding: 5,
|
||||||
|
thumb_link: "", createTime: 1, downloader: 7, isCollect: false},
|
||||||
|
{gid: 3, name: "gamma", status: "已提交", pages: 10, proceeding: 0,
|
||||||
|
thumb_link: "", createTime: 3, downloader: 8, isCollect: false},
|
||||||
|
]
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
store.state.AuthCode = "test-auth"
|
||||||
|
store.state.userId = 7
|
||||||
|
store.state.category = "total"
|
||||||
|
store.state.sortType = "createTime"
|
||||||
store.state.length = 2
|
store.state.length = 2
|
||||||
store.mutations._searchLocalByLink(store.state, 'https://example.org/g/12345/key/')
|
store.state.visiblePages = 1
|
||||||
assert.equal(store.state.page, 2)
|
store.state.isAdmin = false
|
||||||
assert.match(messages[0], /已跳转/)
|
store.state.downloaderFilter = null
|
||||||
})
|
store.commit("_updateGalleryTasks", tasks())
|
||||||
|
|
||||||
test('missing gid still reports no match', () => {
|
|
||||||
const {store, messages} = fixture()
|
|
||||||
store.state.currentTasks = [{gid: 12345}]
|
|
||||||
store.mutations._searchLocalByLink(store.state, 'https://example.org/g/999/key/')
|
|
||||||
assert.equal(messages[0], '未找到此任务')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('submission reloads persisted status without duplicating a completed task', async () => {
|
|
||||||
const {store, axios, api} = fixture()
|
|
||||||
const gallery = {gid: 123, name: 'sample', status: '下载完成', thumb_link: '', createTime: 1, downloader: 7}
|
|
||||||
store.state.chosenGallery = {...gallery, status: '已提交'}
|
|
||||||
store.state.totalGalleryTask.push(gallery)
|
|
||||||
axios.post = async () => ({data: {result: 'success'}})
|
|
||||||
axios.get = async url => ({data: {result: 'success', data: JSON.stringify(url.endsWith('weekUsedAmount') ? {} : [gallery])}})
|
|
||||||
await store.actions.postGalleryTask(api, {targetResolution: 'original'})
|
|
||||||
assert.equal(store.state.totalGalleryTask.length, 1)
|
|
||||||
assert.equal(store.state.totalGalleryTask[0].status, '下载完成')
|
|
||||||
assert.equal(store.state.chosenGallery, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('gallery management subrequests carry the active auth code', async () => {
|
|
||||||
const {store, axios, api} = fixture()
|
|
||||||
store.state.AuthCode = 'test-auth'
|
|
||||||
let call
|
|
||||||
axios.post = async (...args) => {
|
|
||||||
call = args
|
|
||||||
return {data: {result: args[0].endsWith('/cache') ? 'failure' : 'success', data: 'expected test response'}}
|
|
||||||
}
|
}
|
||||||
await store.actions.reconnect(api)
|
|
||||||
assert.equal(call[0], 'https://downloader.lionwebsite.xyz/GalleryManage/reconnect')
|
|
||||||
assert.equal(call[1], null)
|
|
||||||
assert.equal(call[2].params.AuthCode, 'test-auth')
|
|
||||||
|
|
||||||
await store.actions.readOnlineGallery(api, {link: 'https://example.org/g/1/key/', images: []})
|
test("mobile variant shares one task list with the desktop core", () => {
|
||||||
assert.equal(call[0], 'https://downloader.lionwebsite.xyz/GalleryManage/cache')
|
reset()
|
||||||
assert.equal(call[1], null)
|
assert.equal(store.state.totalGalleryTask.length, 3)
|
||||||
assert.deepEqual({...call[2].params}, {
|
assert.equal(store.getters.downloadGallery.length, 2)
|
||||||
url: 'https://example.org/g/1/key/',
|
assert.equal(store.getters.collectGallery.length, 1)
|
||||||
AuthCode: 'test-auth'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('thumbnail and online image URLs carry the active auth code', () => {
|
test("continue-loading reveals every task beyond the first page", () => {
|
||||||
const {store} = fixture()
|
reset()
|
||||||
store.state.AuthCode = 'code with spaces'
|
assert.equal(store.getters.visibleTasks.length, 2)
|
||||||
store.mutations._updateGalleryTasks(store.state, [{
|
assert.equal(store.getters.taskRemaining, 1)
|
||||||
gid: 123,
|
store.commit("_showMoreTasks")
|
||||||
name: 'sample',
|
assert.equal(store.getters.visibleTasks.length, 3)
|
||||||
status: '已提交',
|
assert.equal(store.getters.taskRemaining, 0)
|
||||||
thumb_link: 'https://example.org/thumb?a=1&b=2',
|
})
|
||||||
createTime: 1,
|
|
||||||
downloader: 7
|
test("json-string and object payloads decode to the same store result", () => {
|
||||||
|
reset()
|
||||||
|
const fromObjects = store.state.totalGalleryTask.map(task => task.gid)
|
||||||
|
store.state.totalGalleryTask = []
|
||||||
|
store.commit("_updateGalleryTasks", JSON.parse(JSON.stringify(tasks())))
|
||||||
|
assert.deepEqual(store.state.totalGalleryTask.map(task => task.gid), fromObjects)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("cancelling a collect for a task missing from the list never throws", () => {
|
||||||
|
reset()
|
||||||
|
// 旧实现会在 collectGallery 里找不到 gid 时抛 TypeError。
|
||||||
|
assert.doesNotThrow(() => store.commit("_setCollect", {gid: 999, collect: false}))
|
||||||
|
store.commit("_setCollect", {gid: 1, collect: false})
|
||||||
|
assert.equal(store.getters.collectGallery.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sorting never reorders the single source array", () => {
|
||||||
|
reset()
|
||||||
|
const before = store.state.totalGalleryTask.map(task => task.gid)
|
||||||
|
store.commit("_setSortType", "name")
|
||||||
|
assert.deepEqual(store.state.totalGalleryTask.map(task => task.gid), before)
|
||||||
|
assert.deepEqual(store.getters.visibleTasks.map(task => task.name), ["alpha", "beta [tag]"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("link search surfaces the match as a search result on mobile", () => {
|
||||||
|
const messages = []
|
||||||
|
reset()
|
||||||
|
store.commit("_searchLocalByLink", "https://example.org/g/3/key/")
|
||||||
|
assert.equal(store.state.isSearch, true)
|
||||||
|
assert.equal(store.getters.searchResults.length, 1)
|
||||||
|
assert.equal(store.getters.searchResults[0].gid, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("thumbnail and online image urls carry the active auth code", () => {
|
||||||
|
store.state.AuthCode = "code with spaces"
|
||||||
|
store.commit("_updateGalleryTasks", [{
|
||||||
|
gid: 123, name: "sample", status: "已提交",
|
||||||
|
thumb_link: "https://example.org/thumb?a=1&b=2", createTime: 1, downloader: 7,
|
||||||
}])
|
}])
|
||||||
const thumbnail = new URL(store.state.totalGalleryTask[0].thumb_link)
|
const thumbnail = new URL(store.state.totalGalleryTask[0].thumb_link)
|
||||||
assert.equal(thumbnail.searchParams.get('AuthCode'), 'code with spaces')
|
assert.equal(thumbnail.searchParams.get("AuthCode"), "code with spaces")
|
||||||
assert.equal(thumbnail.searchParams.get('path'), 'https://example.org/thumb?a=1&b=2')
|
assert.equal(thumbnail.searchParams.get("path"), "https://example.org/thumb?a=1&b=2")
|
||||||
|
|
||||||
store.mutations._setReadingGallery(store.state, {gid: 123, pages: 1})
|
store.commit("_setReadingGallery", {gid: 123, pages: 1})
|
||||||
const image = new URL(store.state.readingGallery.images[0])
|
const image = new URL(store.state.readingGallery.images[0])
|
||||||
assert.equal(image.searchParams.get('gid'), '123')
|
assert.equal(image.searchParams.get("gid"), "123")
|
||||||
assert.equal(image.searchParams.get('AuthCode'), 'code with spaces')
|
assert.equal(image.searchParams.get("AuthCode"), "code with spaces")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import test from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import fs from "node:fs"
|
||||||
|
import {registerHooks} from "node:module"
|
||||||
|
import {compileScript, parse} from "@vue/compiler-sfc"
|
||||||
|
import {createRenderer, nextTick} from "vue"
|
||||||
|
import {ehThumbnailUrl} from "lionwebsite-frontend-shared"
|
||||||
|
|
||||||
|
// 移动端详情抽屉的“打开/模式”状态只存在于真实组件里,store 单测覆盖不到。
|
||||||
|
// 这里用编译期 loader hook 直接执行仓库中的 .vue 源码,再挂到最小渲染器上;
|
||||||
|
// 开关与模式都落在普通 DOM 属性上,因此不需要 jsdom/happy-dom。
|
||||||
|
registerHooks({
|
||||||
|
load(url, context, nextLoad) {
|
||||||
|
if (!url.endsWith("/TaskDetailSheet.vue")) return nextLoad(url, context)
|
||||||
|
const source = fs.readFileSync(new URL(url), "utf8")
|
||||||
|
const {descriptor, errors} = parse(source, {filename: url})
|
||||||
|
assert.deepEqual(errors, [], "SFC 解析必须无错")
|
||||||
|
return {
|
||||||
|
format: "module",
|
||||||
|
shortCircuit: true,
|
||||||
|
source: compileScript(descriptor, {id: "sheet-test", inlineTemplate: true, isProd: false}).content,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {default: TaskDetailSheet} = await import("../src/components/TaskDetailSheet.vue")
|
||||||
|
const store = (await import("../src/store/index.js")).default
|
||||||
|
|
||||||
|
// 最小渲染器:只需要能插入节点、读回属性、处理 v-show 的 style。
|
||||||
|
function fakeElement(tag) {
|
||||||
|
return {nodeType: 1, tag, children: [], parentNode: null, style: {}, attrs: {}, listeners: {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeOps() {
|
||||||
|
const remove = node => {
|
||||||
|
const parent = node.parentNode
|
||||||
|
if (!parent) return
|
||||||
|
const index = parent.children.indexOf(node)
|
||||||
|
if (index >= 0) parent.children.splice(index, 1)
|
||||||
|
node.parentNode = null
|
||||||
|
}
|
||||||
|
const insert = (node, parent, anchor = null) => {
|
||||||
|
if (node.parentNode) remove(node)
|
||||||
|
const index = anchor ? parent.children.indexOf(anchor) : -1
|
||||||
|
if (index >= 0) parent.children.splice(index, 0, node)
|
||||||
|
else parent.children.push(node)
|
||||||
|
node.parentNode = parent
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
remove,
|
||||||
|
createElement: fakeElement,
|
||||||
|
createText: text => ({nodeType: 3, text, parentNode: null}),
|
||||||
|
createComment: text => ({nodeType: 8, text, parentNode: null}),
|
||||||
|
setText: (node, text) => { node.text = text },
|
||||||
|
setElementText: (node, text) => { node.children = [{nodeType: 3, text, parentNode: node}] },
|
||||||
|
parentNode: node => node.parentNode,
|
||||||
|
nextSibling: node => {
|
||||||
|
const parent = node.parentNode
|
||||||
|
if (!parent) return null
|
||||||
|
const index = parent.children.indexOf(node)
|
||||||
|
return parent.children[index + 1] || null
|
||||||
|
},
|
||||||
|
querySelector: () => null,
|
||||||
|
setScopeId: (el, id) => { el.scopeId = id },
|
||||||
|
cloneNode: el => ({...fakeElement(el.tag), attrs: {...el.attrs}}),
|
||||||
|
insertStaticContent: (content, parent, anchor) => {
|
||||||
|
const node = fakeElement("static")
|
||||||
|
insert(node, parent, anchor)
|
||||||
|
return [node, node]
|
||||||
|
},
|
||||||
|
patchProp: (el, key, previous, next) => {
|
||||||
|
if (key === "style") el.style = next || {}
|
||||||
|
else if (/^on[A-Z]/.test(key)) el.listeners[key] = next
|
||||||
|
else el.attrs[key] = next
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountSheet() {
|
||||||
|
const app = createRenderer(nodeOps()).createApp(TaskDetailSheet)
|
||||||
|
// 真实入口在 main.js 里全局注册 Element Plus;这里只需要占位节点。
|
||||||
|
app.component("el-progress", {render: () => null})
|
||||||
|
const root = fakeElement("root")
|
||||||
|
app.mount(root)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
function descendants(node, out = []) {
|
||||||
|
for (const child of node.children || []) {
|
||||||
|
if (child.nodeType === 1) {
|
||||||
|
out.push(child)
|
||||||
|
descendants(child, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByClass(root, className) {
|
||||||
|
return descendants(root).find(node => node.attrs.class === className) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function textOf(root) {
|
||||||
|
const parts = []
|
||||||
|
const walk = node => {
|
||||||
|
if (node.nodeType === 3) { parts.push(node.text); return }
|
||||||
|
for (const child of node.children || []) walk(child)
|
||||||
|
}
|
||||||
|
walk(root)
|
||||||
|
return parts.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpen(root) {
|
||||||
|
const layer = findByClass(root, "sheet-layer")
|
||||||
|
assert.ok(layer, "抽屉根节点必须存在")
|
||||||
|
// v-show 只在关闭时写 display: none。
|
||||||
|
return layer.style.display !== "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskGallery = () => ({
|
||||||
|
gid: 5, name: "本地任务", status: "已提交", pages: 10, proceeding: 0,
|
||||||
|
thumb_link: "", createTime: 1, downloader: 7,
|
||||||
|
})
|
||||||
|
const remoteGallery = () => ({
|
||||||
|
gid: 9, name: "远端画廊", link: "https://example.org/g/9/k/", thumbnailUrl: "/t/9.jpg",
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
store.state.AuthCode = "auth"
|
||||||
|
store.state.userId = 7
|
||||||
|
store.state.detailGallery = null
|
||||||
|
store.state.chosenGallery = false
|
||||||
|
}
|
||||||
|
|
||||||
|
test("清空 detailGallery 会关上任务态抽屉", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.detailGallery = taskGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /本地任务/)
|
||||||
|
|
||||||
|
store.state.detailGallery = null
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("清空 chosenGallery 会关上远端态抽屉并复位模式", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = remoteGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /远端画廊/)
|
||||||
|
|
||||||
|
store.state.chosenGallery = false
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), false)
|
||||||
|
|
||||||
|
// 模式必须回到 task,否则下一次打开任务会渲染成远端面板。
|
||||||
|
store.state.detailGallery = taskGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /删除任务/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("远端态下 detailGallery 变空不得误关抽屉", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = remoteGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
|
||||||
|
// 任务流的 detail 被清空(例如列表刷新),此时屏幕上的是远端画廊,必须保持打开。
|
||||||
|
store.state.detailGallery = null
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /远端画廊/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("已规范化的 thumb_link 直接使用,不再二次代理", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
const proxied = ehThumbnailUrl("/t/9.jpg", "auth")
|
||||||
|
store.state.chosenGallery = {
|
||||||
|
...remoteGallery(), thumb_link: proxied, thumbnailUrl: "https://example.org/raw.jpg",
|
||||||
|
}
|
||||||
|
await nextTick()
|
||||||
|
const image = descendants(root).find(node => node.tag === "img")
|
||||||
|
assert.ok(image, "远端态必须渲染缩略图")
|
||||||
|
assert.equal(image.attrs.src, proxied)
|
||||||
|
assert.equal(image.attrs.src.split("ehThumbnail").length - 1, 1)
|
||||||
|
// 外层 path 仍是原始图片路径,没有被嵌套成代理 URL。
|
||||||
|
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "/t/9.jpg")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("只有原始上游地址时才在这里代理一次", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = {...remoteGallery(), thumbnailUrl: "https://example.org/raw.jpg"}
|
||||||
|
await nextTick()
|
||||||
|
const image = descendants(root).find(node => node.tag === "img")
|
||||||
|
assert.ok(image)
|
||||||
|
assert.equal(image.attrs.src, ehThumbnailUrl("https://example.org/raw.jpg", "auth"))
|
||||||
|
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "https://example.org/raw.jpg")
|
||||||
|
})
|
||||||
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
import {createTaskSocket} from '../src/utils/taskSocket.js'
|
|
||||||
|
|
||||||
function fixture() {
|
|
||||||
const sockets = [], states = [], messages = [], timers = new Map()
|
|
||||||
let id = 0, opens = 0
|
|
||||||
const manager = createTaskSocket({url: 'wss://example.invalid',
|
|
||||||
onOpen: () => opens++, onState: value => states.push(value), onMessage: event => messages.push(event.data),
|
|
||||||
setTimer: (fn, delay) => { timers.set(++id, {fn, delay}); return id },
|
|
||||||
clearTimer: key => timers.delete(key),
|
|
||||||
createSocket: () => {
|
|
||||||
const socket = {sent: [], send(value) { this.sent.push(value) }, close() { this.closed = true }}
|
|
||||||
sockets.push(socket)
|
|
||||||
return socket
|
|
||||||
}
|
|
||||||
})
|
|
||||||
function tick(delay) {
|
|
||||||
const entry = [...timers].find(([, timer]) => timer.delay === delay)
|
|
||||||
assert.ok(entry, `expected timer ${delay}`)
|
|
||||||
timers.delete(entry[0]); entry[1].fn()
|
|
||||||
}
|
|
||||||
return {manager, sockets, states, messages, timers, tick, opens: () => opens}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('reconnects and refreshes after each successful connection', () => {
|
|
||||||
const f = fixture(); f.manager.start()
|
|
||||||
f.sockets[0].onopen()
|
|
||||||
assert.deepEqual(f.sockets[0].sent, ['DownloaderWebsocket'])
|
|
||||||
f.sockets[0].onclose()
|
|
||||||
f.tick(1000)
|
|
||||||
f.sockets[1].onopen()
|
|
||||||
assert.equal(f.opens(), 2)
|
|
||||||
assert.equal(f.states.at(-1), 'connected')
|
|
||||||
assert.equal(f.timers.size, 0)
|
|
||||||
f.manager.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('backs off and times out connection attempts', () => {
|
|
||||||
const f = fixture(); f.manager.start()
|
|
||||||
f.tick(10000)
|
|
||||||
assert.equal(f.sockets[0].closed, true)
|
|
||||||
f.tick(1000); f.sockets[1].onerror(); f.tick(2000)
|
|
||||||
f.sockets[2].onerror(); f.tick(4000)
|
|
||||||
assert.equal(f.sockets.length, 4)
|
|
||||||
f.manager.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('stop cancels timers and ignores callbacks from replaced sockets', () => {
|
|
||||||
const f = fixture(); f.manager.start()
|
|
||||||
const oldOpen = f.sockets[0].onopen, oldMessage = f.sockets[0].onmessage
|
|
||||||
f.sockets[0].onerror()
|
|
||||||
f.manager.stop()
|
|
||||||
assert.equal(f.timers.size, 0)
|
|
||||||
f.manager.start()
|
|
||||||
oldOpen(); oldMessage({data: 'stale'})
|
|
||||||
assert.equal(f.opens(), 0)
|
|
||||||
assert.deepEqual(f.messages, [])
|
|
||||||
f.sockets[1].onopen()
|
|
||||||
f.sockets[1].onmessage({data: 'current'})
|
|
||||||
assert.deepEqual(f.messages, ['current'])
|
|
||||||
f.manager.stop()
|
|
||||||
})
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
import {ehThumbnailUrl, pickDefaultResolution} from '../src/utils/thumbnail.js'
|
|
||||||
|
|
||||||
test('remote thumbnail paths are wrapped with the auth code', () => {
|
|
||||||
const url = ehThumbnailUrl('samples/thumb.png', 'abc123')
|
|
||||||
assert.match(url, /^https:\/\/downloader\.lionwebsite\.xyz\/GalleryManage\/ehThumbnail\?/)
|
|
||||||
assert.match(url, /path=samples%2Fthumb\.png/)
|
|
||||||
assert.match(url, /AuthCode=abc123/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('a missing thumbnail path produces no url', () => {
|
|
||||||
assert.equal(ehThumbnailUrl('', 'abc123'), '')
|
|
||||||
assert.equal(ehThumbnailUrl(undefined, 'abc123'), '')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the original resolution is preselected when offered', () => {
|
|
||||||
const chosen = pickDefaultResolution({
|
|
||||||
'800x': '37.53MiB',
|
|
||||||
'1280x': '66.57MiB',
|
|
||||||
'2560x': '84.01MiB',
|
|
||||||
'Original': '498.2MiB',
|
|
||||||
})
|
|
||||||
assert.equal(chosen, 'Original')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the original label is matched case-insensitively', () => {
|
|
||||||
assert.equal(pickDefaultResolution({'800x': '1MiB', 'original': '2MiB'}), 'original')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('without an original the largest entry wins across units', () => {
|
|
||||||
const chosen = pickDefaultResolution({
|
|
||||||
'800x': '37.53MiB',
|
|
||||||
'2560x': '84.01MiB',
|
|
||||||
'2160x': '1.2GiB',
|
|
||||||
})
|
|
||||||
assert.equal(chosen, '2160x')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('an empty or absent resolution map selects nothing', () => {
|
|
||||||
assert.equal(pickDefaultResolution({}), '')
|
|
||||||
assert.equal(pickDefaultResolution(undefined), '')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('unparseable sizes fall back to the first entry instead of throwing', () => {
|
|
||||||
assert.equal(pickDefaultResolution({'800x': 'unknown', '1280x': 'unknown'}), '800x')
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user