mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-04-24 02:20:38 +08:00
Merge branch 'Rewrite0/Auto_Bangumi_WebUI@dc95c1a' into Auto_Bangumi dev/webui
This commit is contained in:
2
webui/.eslintignore
Normal file
2
webui/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/dist
|
||||
8
webui/.eslintrc.json
Normal file
8
webui/.eslintrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["@antfu", "prettier", "plugin:storybook/recommended"],
|
||||
"rules": {
|
||||
"antfu/if-newline": ["off"],
|
||||
"no-console": ["off"],
|
||||
"vue/custom-event-name-casing": ["off"]
|
||||
}
|
||||
}
|
||||
39
webui/.github/workflows/create-release-draft.yaml
vendored
Normal file
39
webui/.github/workflows/create-release-draft.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Create Release Draft
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
|
||||
- name: Build
|
||||
run: pnpm build && zip -r dist.zip dist
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
files: dist.zip
|
||||
25
webui/.gitignore
vendored
Normal file
25
webui/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist.zip
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
webui/.npmrc
Normal file
1
webui/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
public-hoist-pattern[]=@vue/runtime-core
|
||||
6
webui/.prettierignore
Normal file
6
webui/.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
/build
|
||||
/dist
|
||||
/pnpm-lock.yaml
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
router-type.d.ts
|
||||
3
webui/.prettierrc.json
Normal file
3
webui/.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
24
webui/.storybook/main.ts
Normal file
24
webui/.storybook/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
import Unocss from 'unocss/vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal(config) {
|
||||
config.plugins?.push(Unocss());
|
||||
// Add other configuration here depending on your use case
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
17
webui/.storybook/preview.ts
Normal file
17
webui/.storybook/preview.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Preview } from '@storybook/vue3';
|
||||
import '@unocss/reset/tailwind-compat.css';
|
||||
import 'uno.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
3
webui/.vscode/extensions.json
vendored
Normal file
3
webui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
21
webui/LICENSE
Normal file
21
webui/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Rewrite0
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3
webui/README.md
Normal file
3
webui/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Auto_Bangumi_WebUI
|
||||
|
||||
使用 Vue3 + TypeScript 构建的 [Auto_Bangumi](https://github.com/EstrellaXD/Auto_Bangumi) 的 WebUI
|
||||
14
webui/index.html
Normal file
14
webui/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Auto_Bangumi</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
60
webui/package.json
Normal file
60
webui/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "ab-webui",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"dev": "vite",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"axios": "^0.27.2",
|
||||
"lodash": "^4.17.21",
|
||||
"naive-ui": "^2.34.4",
|
||||
"pinia": "^2.1.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.38.6",
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@storybook/addon-essentials": "^7.0.12",
|
||||
"@storybook/addon-interactions": "^7.0.12",
|
||||
"@storybook/addon-links": "^7.0.12",
|
||||
"@storybook/blocks": "^7.0.12",
|
||||
"@storybook/testing-library": "0.0.14-next.2",
|
||||
"@storybook/vue3": "^7.0.12",
|
||||
"@storybook/vue3-vite": "^7.0.12",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.14",
|
||||
"@unocss/preset-rem-to-px": "^0.51.13",
|
||||
"@unocss/reset": "^0.51.13",
|
||||
"@vitejs/plugin-vue": "^4.2.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"prettier": "^2.8.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.62.1",
|
||||
"storybook": "^7.0.12",
|
||||
"typescript": "^4.9.5",
|
||||
"unocss": "^0.51.13",
|
||||
"unplugin-auto-import": "^0.10.3",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"unplugin-vue-router": "^0.6.4",
|
||||
"vite": "^4.3.5",
|
||||
"vitest": "^0.30.1",
|
||||
"vue-tsc": "^1.6.4"
|
||||
}
|
||||
}
|
||||
9507
webui/pnpm-lock.yaml
generated
Normal file
9507
webui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
webui/public/AutoBangumi.svg
Normal file
13
webui/public/AutoBangumi.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="159" height="24" viewBox="0 0 159 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.424 18.56H15.24L12.528 12.68H5.28001L2.59201 18.56H0.312012L8.61601 0.296021H9.24001L17.424 18.56ZM11.592 10.592L8.92801 4.66402L6.24001 10.592H11.592Z" fill="#FFF4DF"/>
|
||||
<path d="M30.7016 18.56H28.7096V16.592C27.4136 18.128 25.8776 18.896 24.1016 18.896C23.1576 18.896 22.3016 18.656 21.5336 18.176C20.7656 17.68 20.1896 17.008 19.8056 16.16C19.4216 15.296 19.2296 13.976 19.2296 12.2V5.48002H21.1976V11.72C21.1976 13.16 21.2616 14.144 21.3896 14.672C21.5336 15.184 21.7336 15.624 21.9896 15.992C22.2616 16.344 22.5976 16.608 22.9976 16.784C23.4136 16.96 23.9096 17.048 24.4856 17.048C25.0776 17.048 25.6536 16.904 26.2136 16.616C26.7736 16.328 27.2536 15.928 27.6536 15.416C28.0536 14.904 28.3256 14.36 28.4696 13.784C28.6296 13.208 28.7096 12.056 28.7096 10.328V5.48002H30.7016V18.56Z" fill="#FFF4DF"/>
|
||||
<path d="M40.0472 7.20802H37.4552V16.376C37.4552 16.712 37.5032 16.92 37.5992 17C37.6952 17.064 37.9352 17.096 38.3192 17.096H39.4712V18.8C38.7032 18.928 38.1192 18.992 37.7192 18.992C36.9512 18.992 36.3832 18.808 36.0152 18.44C35.6472 18.056 35.4632 17.456 35.4632 16.64V7.20802H33.2072V5.48002H35.4632V0.56002H37.4552V5.48002H40.0472V7.20802Z" fill="#FFF4DF"/>
|
||||
<path d="M48.372 5.14402C50.26 5.14402 51.86 5.82402 53.172 7.18402C54.5 8.54402 55.164 10.176 55.164 12.08C55.164 13.984 54.508 15.616 53.196 16.976C51.9 18.32 50.292 18.992 48.372 18.992C46.452 18.992 44.836 18.32 43.524 16.976C42.212 15.616 41.556 13.984 41.556 12.08C41.556 10.176 42.212 8.54402 43.524 7.18402C44.852 5.82402 46.468 5.14402 48.372 5.14402ZM48.372 17.096C49.7 17.096 50.828 16.616 51.756 15.656C52.7 14.68 53.172 13.512 53.172 12.152C53.172 10.776 52.692 9.59202 51.732 8.60002C50.788 7.59202 49.66 7.08802 48.348 7.08802C47.052 7.08802 45.932 7.59202 44.988 8.60002C44.044 9.59202 43.572 10.776 43.572 12.152C43.572 13.528 44.028 14.696 44.94 15.656C45.852 16.616 46.996 17.096 48.372 17.096Z" fill="#FFF4DF"/>
|
||||
<path d="M65.4906 8.93602C66.6106 9.35202 67.4506 9.94402 68.0106 10.712C68.5866 11.48 68.8746 12.392 68.8746 13.448C68.8746 14.92 68.3386 16.144 67.2666 17.12C66.2106 18.08 64.8266 18.56 63.1146 18.56H58.1946V0.99202H61.7706C63.6266 0.99202 65.0346 1.37602 65.9946 2.14402C66.9706 2.89602 67.4586 3.97602 67.4586 5.38402C67.4586 6.92002 66.8026 8.10402 65.4906 8.93602ZM60.2346 8.19202H61.0506C62.4906 8.19202 63.5626 7.96802 64.2666 7.52002C64.9866 7.07202 65.3466 6.36002 65.3466 5.38402C65.3466 3.80002 64.2506 3.00802 62.0586 3.00802H60.2346V8.19202ZM60.2346 16.544H62.2026C63.4346 16.544 64.3306 16.424 64.8906 16.184C65.4506 15.944 65.8986 15.576 66.2346 15.08C66.5706 14.584 66.7386 14.08 66.7386 13.568C66.7386 13.056 66.6346 12.608 66.4266 12.224C66.2346 11.824 65.9386 11.472 65.5386 11.168C65.1546 10.864 64.6746 10.64 64.0986 10.496C63.5386 10.336 62.5146 10.256 61.0266 10.256H60.2346V16.544Z" fill="#FFF4DF"/>
|
||||
<path d="M84.6958 18.56H82.7518V16.64C81.3278 18.208 79.6878 18.992 77.8318 18.992C75.9758 18.992 74.3838 18.312 73.0558 16.952C71.7438 15.576 71.0878 13.944 71.0878 12.056C71.0878 10.152 71.7518 8.52802 73.0798 7.18402C74.4078 5.82402 76.0238 5.14402 77.9278 5.14402C79.8478 5.14402 81.4558 5.92002 82.7518 7.47202V5.48002H84.6958V18.56ZM77.9758 17.144C79.3038 17.144 80.4478 16.664 81.4078 15.704C82.3678 14.744 82.8478 13.552 82.8478 12.128C82.8478 10.688 82.3758 9.48802 81.4318 8.52802C80.4878 7.55202 79.3278 7.06402 77.9518 7.06402C76.5918 7.06402 75.4398 7.56802 74.4958 8.57602C73.5518 9.56802 73.0798 10.744 73.0798 12.104C73.0798 13.464 73.5598 14.648 74.5198 15.656C75.4798 16.648 76.6318 17.144 77.9758 17.144Z" fill="#FFF4DF"/>
|
||||
<path d="M99.5186 18.56H97.5506V12.32C97.5506 10.88 97.4786 9.90402 97.3346 9.39202C97.2066 8.86402 97.0066 8.42402 96.7346 8.07202C96.4786 7.70402 96.1426 7.43202 95.7266 7.25602C95.3266 7.08002 94.8306 6.99202 94.2386 6.99202C93.6626 6.99202 93.0946 7.13602 92.5346 7.42402C91.9746 7.71202 91.4946 8.11202 91.0946 8.62402C90.6946 9.13602 90.4146 9.68002 90.2546 10.256C90.1106 10.832 90.0386 11.984 90.0386 13.712V18.56H88.0466V5.48002H90.0386V7.44802C91.3346 5.91202 92.8706 5.14402 94.6466 5.14402C95.5906 5.14402 96.4466 5.39202 97.2146 5.88802C97.9826 6.36802 98.5586 7.04002 98.9426 7.90402C99.3266 8.75202 99.5186 10.064 99.5186 11.84V18.56Z" fill="#FFF4DF"/>
|
||||
<path d="M115.775 15.488C115.775 17.216 115.623 18.496 115.319 19.328C115.031 20.16 114.583 20.864 113.975 21.44C113.383 22.016 112.663 22.448 111.815 22.736C110.983 23.04 110.063 23.192 109.055 23.192C105.647 23.192 103.375 21.752 102.239 18.872H104.351C105.279 20.536 106.823 21.368 108.983 21.368C109.975 21.368 110.863 21.176 111.647 20.792C112.431 20.408 112.983 19.896 113.303 19.256C113.623 18.616 113.783 17.632 113.783 16.304V16.184C113.159 16.888 112.407 17.424 111.527 17.792C110.647 18.16 109.735 18.344 108.791 18.344C106.903 18.344 105.319 17.696 104.039 16.4C102.775 15.104 102.151 13.576 102.167 11.816C102.151 9.97602 102.815 8.40802 104.159 7.11202C105.503 5.80002 107.095 5.14402 108.935 5.14402C110.791 5.14402 112.407 5.89602 113.783 7.40002V5.48002H115.775V15.488ZM113.855 11.816C113.855 10.44 113.391 9.30402 112.463 8.40802C111.551 7.51202 110.431 7.06402 109.103 7.06402C107.695 7.06402 106.511 7.55202 105.551 8.52802C104.607 9.48802 104.135 10.608 104.135 11.888C104.135 13.168 104.591 14.256 105.503 15.152C106.415 16.032 107.583 16.472 109.007 16.472C110.431 16.472 111.591 16.04 112.487 15.176C113.399 14.296 113.855 13.176 113.855 11.816Z" fill="#FFF4DF"/>
|
||||
<path d="M130.124 18.56H128.132V16.592C126.836 18.128 125.3 18.896 123.524 18.896C122.58 18.896 121.724 18.656 120.956 18.176C120.188 17.68 119.612 17.008 119.228 16.16C118.844 15.296 118.652 13.976 118.652 12.2V5.48002H120.62V11.72C120.62 13.16 120.684 14.144 120.812 14.672C120.956 15.184 121.156 15.624 121.412 15.992C121.684 16.344 122.02 16.608 122.42 16.784C122.836 16.96 123.332 17.048 123.908 17.048C124.5 17.048 125.076 16.904 125.636 16.616C126.196 16.328 126.676 15.928 127.076 15.416C127.476 14.904 127.748 14.36 127.892 13.784C128.052 13.208 128.132 12.056 128.132 10.328V5.48002H130.124V18.56Z" fill="#FFF4DF"/>
|
||||
<path d="M152.837 18.56H150.821V11.432C150.821 10.296 150.749 9.46402 150.605 8.93602C150.477 8.40802 150.181 7.96802 149.717 7.61602C149.269 7.24802 148.669 7.06402 147.917 7.06402C147.181 7.06402 146.485 7.30402 145.829 7.78402C145.173 8.26402 144.717 8.92002 144.461 9.75202C144.205 10.568 144.077 11.784 144.077 13.4V18.56H142.133V11.864C142.133 10.552 142.061 9.60802 141.917 9.03202C141.789 8.45602 141.493 7.98402 141.029 7.61602C140.565 7.24802 139.981 7.06402 139.277 7.06402C138.573 7.06402 137.901 7.28802 137.261 7.73602C136.637 8.16802 136.173 8.74402 135.869 9.46402C135.565 10.168 135.413 11.344 135.413 12.992V18.56H133.445V5.48002H135.413V7.32802C136.533 5.87202 137.933 5.14402 139.613 5.14402C140.525 5.14402 141.349 5.39202 142.085 5.88802C142.837 6.36802 143.373 7.05602 143.693 7.95202C144.237 7.04002 144.917 6.34402 145.733 5.86402C146.549 5.38402 147.413 5.14402 148.325 5.14402C149.573 5.14402 150.637 5.58402 151.517 6.46402C152.397 7.34402 152.837 9.00002 152.837 11.432V18.56Z" fill="#FFF4DF"/>
|
||||
<path d="M156.713 0.56002C157.145 0.56002 157.505 0.71202 157.793 1.01602C158.097 1.30402 158.249 1.66402 158.249 2.09602C158.249 2.51202 158.097 2.87202 157.793 3.17602C157.505 3.46402 157.145 3.60802 156.713 3.60802C156.297 3.60802 155.937 3.45602 155.633 3.15202C155.345 2.84802 155.201 2.49602 155.201 2.09602C155.201 1.68002 155.345 1.32002 155.633 1.01602C155.937 0.71202 156.297 0.56002 156.713 0.56002ZM157.721 18.56H155.729V5.48002H157.721V18.56Z" fill="#FFF4DF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
6
webui/public/favicon-light.svg
Normal file
6
webui/public/favicon-light.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C9.62662 -2.83022e-08 7.30655 0.703788 5.33316 2.02236C3.35977 3.34094 1.8217 5.21509 0.913446 7.4078C0.00519403 9.60051 -0.232446 12.0133 0.230577 14.3411C0.693599 16.6689 1.83649 18.8071 3.51472 20.4853C5.19295 22.1635 7.33114 23.3064 9.65892 23.7694C11.9867 24.2324 14.3995 23.9948 16.5922 23.0866C18.7849 22.1783 20.6591 20.6402 21.9776 18.6668C23.2962 16.6935 24 14.3734 24 12H16.1979C16.1979 12.8303 15.9517 13.6419 15.4905 14.3323C15.0292 15.0226 14.3736 15.5607 13.6065 15.8784C12.8394 16.1961 11.9953 16.2793 11.181 16.1173C10.3667 15.9553 9.6187 15.5555 9.03161 14.9684C8.44451 14.3813 8.0447 13.6333 7.88272 12.819C7.72074 12.0047 7.80387 11.1606 8.12161 10.3935C8.43934 9.62644 8.9774 8.97081 9.66775 8.50954C10.3581 8.04826 11.1697 7.80206 12 7.80206V0Z" fill="white"/>
|
||||
<circle cx="12" cy="12" r="2" fill="#DBBB7F"/>
|
||||
<path d="M19 11C19 10.2121 18.8577 9.43185 18.5813 8.7039C18.3049 7.97595 17.8998 7.31451 17.3891 6.75736C16.8784 6.20021 16.272 5.75825 15.6048 5.45672C14.9375 5.15519 14.2223 5 13.5 5L13.5 8.06002C13.8539 8.06002 14.2044 8.13607 14.5313 8.28381C14.8583 8.43156 15.1554 8.64812 15.4056 8.92112C15.6559 9.19412 15.8544 9.51822 15.9898 9.87492C16.1253 10.2316 16.195 10.6139 16.195 11H19Z" fill="#DBBB7F"/>
|
||||
<path d="M13.5 0C14.8789 1.7226e-08 16.2443 0.284523 17.5182 0.837325C18.7921 1.39013 19.9496 2.20038 20.9246 3.22183C21.8996 4.24327 22.6731 5.4559 23.2007 6.79048C23.7284 8.12506 24 9.55546 24 11L20.8595 11C20.8595 9.98751 20.6692 8.98494 20.2993 8.04953C19.9295 7.11411 19.3874 6.26417 18.704 5.54823C18.0206 4.8323 17.2093 4.26439 16.3164 3.87692C15.4235 3.48946 14.4665 3.29004 13.5 3.29004V0Z" fill="#DBBB7F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
17
webui/public/favicon.svg
Normal file
17
webui/public/favicon.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="97" height="96" viewBox="0 0 97 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1852_4762)">
|
||||
<path d="M48.0344 0C38.5409 -1.13209e-07 29.2606 2.81515 21.367 8.08946C13.4734 13.3638 7.32115 20.8603 3.68815 29.6312C0.0551389 38.4021 -0.895421 48.0532 0.956669 57.3643C2.80876 66.6754 7.38032 75.2282 14.0932 81.9411C20.8062 88.654 29.3589 93.2256 38.67 95.0777C47.9811 96.9298 57.6323 95.9792 66.4032 92.3462C75.174 88.7132 82.6706 82.5609 87.9449 74.6674C93.2192 66.7738 96.0344 57.4935 96.0344 48H64.8261C64.8261 51.3211 63.8413 54.5676 61.9962 57.329C60.1511 60.0904 57.5286 62.2426 54.4603 63.5136C51.392 64.7845 48.0157 65.117 44.7584 64.4691C41.5012 63.8212 38.5092 62.222 36.1608 59.8736C33.8124 57.5252 32.2132 54.5332 31.5652 51.2759C30.9173 48.0186 31.2499 44.6424 32.5208 41.5741C33.7917 38.5058 35.944 35.8833 38.7054 34.0382C41.4667 32.193 44.7133 31.2082 48.0344 31.2082V0Z" fill="url(#paint0_angular_1852_4762)"/>
|
||||
<circle cx="48.0344" cy="48" r="8" fill="#DBBB7F"/>
|
||||
<path d="M76.0344 43C76.0344 39.9796 75.4394 36.9888 74.2836 34.1983C73.1277 31.4078 71.4336 28.8723 69.2978 26.7365C67.1621 24.6008 64.6266 22.9066 61.8361 21.7508C59.0456 20.5949 56.0548 20 53.0344 20L53.0344 31.7301C54.5143 31.7301 55.9798 32.0216 57.3472 32.588C58.7145 33.1543 59.9569 33.9845 61.0034 35.031C62.0499 36.0775 62.88 37.3199 63.4464 38.6872C64.0128 40.0545 64.3043 41.52 64.3043 43H76.0344Z" fill="#DBBB7F"/>
|
||||
<path d="M53.0344 0C58.6812 6.73379e-08 64.2728 1.11223 69.4898 3.27318C74.7068 5.43413 79.447 8.60149 83.44 12.5944C87.4329 16.5873 90.6002 21.3276 92.7612 26.5446C94.9221 31.7616 96.0344 37.3532 96.0344 43L83.1733 43C83.1733 39.0421 82.3937 35.123 80.8791 31.4663C79.3645 27.8097 77.1445 24.4872 74.3458 21.6885C71.5472 18.8899 68.2247 16.6699 64.568 15.1552C60.9114 13.6406 56.9923 12.8611 53.0344 12.8611V0Z" fill="#DBBB7F"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_angular_1852_4762" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(48.0344 48) rotate(90) scale(48)">
|
||||
<stop offset="0.5" stop-color="#4B2996"/>
|
||||
<stop offset="0.749617" stop-color="#753677"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_1852_4762">
|
||||
<rect width="96" height="96" fill="white" transform="translate(0.0343628)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
39
webui/public/robots.txt
Normal file
39
webui/public/robots.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
# robots.txt generated at http://tool.chinaz.com/robots/
|
||||
User-agent: Baiduspider
|
||||
Disallow: /
|
||||
User-agent: Sosospider
|
||||
Disallow: /
|
||||
User-agent: sogou spider
|
||||
Disallow: /
|
||||
User-agent: YodaoBot
|
||||
Disallow: /
|
||||
User-agent: Googlebot
|
||||
Disallow: /
|
||||
User-agent: Bingbot
|
||||
Disallow: /
|
||||
User-agent: Slurp
|
||||
Disallow: /
|
||||
User-agent: Teoma
|
||||
Disallow: /
|
||||
User-agent: ia_archiver
|
||||
Disallow: /
|
||||
User-agent: twiceler
|
||||
Disallow: /
|
||||
User-agent: MSNBot
|
||||
Disallow: /
|
||||
User-agent: Scrubby
|
||||
Disallow: /
|
||||
User-agent: Robozilla
|
||||
Disallow: /
|
||||
User-agent: Gigabot
|
||||
Disallow: /
|
||||
User-agent: googlebot-image
|
||||
Disallow: /
|
||||
User-agent: googlebot-mobile
|
||||
Disallow: /
|
||||
User-agent: yahoo-mmcrawler
|
||||
Disallow: /
|
||||
User-agent: yahoo-blogs/v3.9
|
||||
Disallow: /
|
||||
User-agent: psbot
|
||||
Disallow: /
|
||||
33
webui/src/App.vue
Normal file
33
webui/src/App.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type GlobalThemeOverrides,
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
} from 'naive-ui';
|
||||
|
||||
const theme: GlobalThemeOverrides = {
|
||||
Spin: {
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
|
||||
const { refresh, isLogin } = useAuth();
|
||||
if (isLogin.value) {
|
||||
refresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<NConfigProvider :theme-overrides="theme">
|
||||
<NMessageProvider>
|
||||
<RouterView></RouterView>
|
||||
</NMessageProvider>
|
||||
</NConfigProvider>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style/transition';
|
||||
@import './style/global';
|
||||
</style>
|
||||
41
webui/src/api/auth.ts
Normal file
41
webui/src/api/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { LoginSuccess, Logout, Update } from '#/auth';
|
||||
|
||||
export const apiAuth = {
|
||||
async login(username: string, password: string) {
|
||||
const formData = new URLSearchParams({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
const { data } = await axios.post<LoginSuccess>(
|
||||
'api/v1/auth/login',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
const { data } = await axios.get<LoginSuccess>('api/v1/auth/refresh_token');
|
||||
return data;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const { data } = await axios.get<Logout>('api/v1/auth/logout');
|
||||
return data.message === 'logout success';
|
||||
},
|
||||
|
||||
async update(username: string, password: string) {
|
||||
const { data } = await axios.post<Update>('api/v1/auth/update', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
97
webui/src/api/bangumi.ts
Normal file
97
webui/src/api/bangumi.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
import type { ApiSuccess } from '#/api';
|
||||
|
||||
export const apiBangumi = {
|
||||
/**
|
||||
* 获取所有 bangumi 数据
|
||||
* @returns 所有 bangumi 数据
|
||||
*/
|
||||
async getAll() {
|
||||
const { data } = await axios.get<BangumiRule[]>('api/v1/bangumi/getAll');
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定 bangumiId 的规则
|
||||
* @param bangumiId bangumi id
|
||||
* @returns 指定 bangumi 的规则
|
||||
*/
|
||||
async getRule(bangumiId: number) {
|
||||
const { data } = await axios.get<BangumiRule>(
|
||||
`api/v1/bangumi/getRule/${bangumiId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新指定 bangumiId 的规则
|
||||
* @param bangumiData - 需要更新的规则
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async updateRule(bangumiRule: BangumiRule) {
|
||||
const { data } = await axios.post<ApiSuccess>(
|
||||
'api/v1/bangumi/updateRule',
|
||||
bangumiRule
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定 bangumiId 的数据库规则,会在重新匹配到后重建
|
||||
* @param bangumiId - 需要删除的 bangumi 的 id
|
||||
* @param file - 是否同时删除关联文件。
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async deleteRule(bangumiId: number, file: boolean) {
|
||||
const { data } = await axios.delete<ApiSuccess>(
|
||||
`api/v1/bangumi/deleteRule/${bangumiId}`,
|
||||
{
|
||||
params: {
|
||||
file,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定 bangumiId 的规则。如果 file 为 true,则同时删除关联文件。
|
||||
* @param bangumiId - 需要删除规则的 bangumi 的 id。
|
||||
* @param file - 是否同时删除关联文件。
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async disableRule(bangumiId: number, file: boolean) {
|
||||
const { data } = await axios.delete<ApiSuccess>(
|
||||
`api/v1/bangumi/disableRule/${bangumiId}`,
|
||||
{
|
||||
params: {
|
||||
file,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 启用指定 bangumiId 的规则
|
||||
* @param bangumiId - 需要启用的 bangumi 的 id
|
||||
*/
|
||||
async enableRule(bangumiId: number) {
|
||||
const { data } = await axios.get<ApiSuccess>(
|
||||
`api/v1/bangumi/enableRule/${bangumiId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置所有 bangumi 数据
|
||||
*/
|
||||
async resetAll() {
|
||||
const { data } = await axios.get<{
|
||||
message: 'OK';
|
||||
}>('api/v1/bangumi/resetAll');
|
||||
return data;
|
||||
},
|
||||
};
|
||||
25
webui/src/api/check.ts
Normal file
25
webui/src/api/check.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const apiCheck = {
|
||||
/**
|
||||
* 检测下载器
|
||||
*/
|
||||
async downloader() {
|
||||
const { data } = await axios.get('api/v1/check/downloader');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检测 RSS
|
||||
*/
|
||||
async rss() {
|
||||
const { data } = await axios.get('api/v1/check/rss');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检测所有
|
||||
*/
|
||||
async all() {
|
||||
const { data } = await axios.get('api/v1/check');
|
||||
return data;
|
||||
},
|
||||
};
|
||||
23
webui/src/api/config.ts
Normal file
23
webui/src/api/config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Config } from '#/config';
|
||||
|
||||
export const apiConfig = {
|
||||
/**
|
||||
* 获取 config 数据
|
||||
*/
|
||||
async getConfig() {
|
||||
const { data } = await axios.get<Config>('api/v1/getConfig');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新 config 数据
|
||||
* @param newConfig - 需要更新的 config
|
||||
*/
|
||||
async updateConfig(newConfig: Config) {
|
||||
const { data } = await axios.post<{
|
||||
message: 'Success' | 'Failed to update config';
|
||||
}>('api/v1/updateConfig', newConfig);
|
||||
|
||||
return data.message === 'Success';
|
||||
},
|
||||
};
|
||||
61
webui/src/api/download.ts
Normal file
61
webui/src/api/download.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
interface Status {
|
||||
status: 'Success';
|
||||
}
|
||||
|
||||
interface AnalysisError {
|
||||
status: 'Failed to parse link';
|
||||
}
|
||||
|
||||
export const apiDownload = {
|
||||
/**
|
||||
* 解析 RSS 链接
|
||||
* @param rss_link - RSS 链接
|
||||
*/
|
||||
async analysis(rss_link: string) {
|
||||
const fetchResult = createEventHook<BangumiRule>();
|
||||
const fetchError = createEventHook<AnalysisError>();
|
||||
|
||||
axios
|
||||
.post<any>('api/v1/download/analysis', {
|
||||
rss_link,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.status) {
|
||||
fetchError.trigger(data as AnalysisError);
|
||||
} else {
|
||||
fetchResult.trigger(data as BangumiRule);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
onResult: fetchResult.on,
|
||||
onError: fetchError.on,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 旧番
|
||||
* @param bangumiData - Bangumi 数据
|
||||
*/
|
||||
async collection(bangumiData: BangumiRule) {
|
||||
const { data } = await axios.post<Status>(
|
||||
'api/v1/download/collection',
|
||||
bangumiData
|
||||
);
|
||||
return data.status === 'Success';
|
||||
},
|
||||
|
||||
/**
|
||||
* 新番
|
||||
* @param bangumiData - Bangumi 数据
|
||||
*/
|
||||
async subscribe(bangumiData: BangumiRule) {
|
||||
const { data } = await axios.post<Status>(
|
||||
'api/v1/download/subscribe',
|
||||
bangumiData
|
||||
);
|
||||
return data.status === 'Success';
|
||||
},
|
||||
};
|
||||
11
webui/src/api/log.ts
Normal file
11
webui/src/api/log.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const apiLog = {
|
||||
async getLog() {
|
||||
const { data } = await axios.get<string>('api/v1/log');
|
||||
return data;
|
||||
},
|
||||
|
||||
async clearLog() {
|
||||
const { data } = await axios.get<{ status: 'ok' }>('api/v1/log/clear');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
};
|
||||
47
webui/src/api/program.ts
Normal file
47
webui/src/api/program.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
interface Success {
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
export const apiProgram = {
|
||||
/**
|
||||
* 重启
|
||||
*/
|
||||
async restart() {
|
||||
const { data } = await axios.get<Success>('api/v1/restart');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/**
|
||||
* 启动
|
||||
*/
|
||||
async start() {
|
||||
const { data } = await axios.get<Success>('api/v1/start');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止
|
||||
*/
|
||||
async stop() {
|
||||
const { data } = await axios.get<Success>('api/v1/stop');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
async status() {
|
||||
const { data } = await axios.get<{ status: 'running' | 'stop' }>(
|
||||
'api/v1/status'
|
||||
);
|
||||
return data.status === 'running';
|
||||
},
|
||||
|
||||
/**
|
||||
* 终止
|
||||
*/
|
||||
async shutdown() {
|
||||
const { data } = await axios.get<Success>('api/v1/shutdown');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
};
|
||||
146
webui/src/components/ab-add-bangumi.vue
Normal file
146
webui/src/components/ab-add-bangumi.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const { getAll } = useBangumiStore();
|
||||
const show = defineModel('show', { default: false });
|
||||
|
||||
const rss = ref('');
|
||||
const message = useMessage();
|
||||
const rule = ref<BangumiRule>({
|
||||
added: false,
|
||||
deleted: false,
|
||||
dpi: '',
|
||||
eps_collect: false,
|
||||
filter: [],
|
||||
group_name: '',
|
||||
id: 0,
|
||||
official_title: '',
|
||||
offset: 0,
|
||||
poster_link: '',
|
||||
rss_link: [],
|
||||
rule_name: '',
|
||||
save_path: '',
|
||||
season: 1,
|
||||
season_raw: '',
|
||||
source: null,
|
||||
subtitle: '',
|
||||
title_raw: '',
|
||||
year: null,
|
||||
});
|
||||
const analysis = reactive({
|
||||
loading: false,
|
||||
next: false,
|
||||
});
|
||||
|
||||
const loading = reactive({
|
||||
collect: false,
|
||||
subscribe: false,
|
||||
});
|
||||
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
rss.value = '';
|
||||
setTimeout(() => {
|
||||
analysis.next = false;
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
async function analyser() {
|
||||
if (rss.value === '') {
|
||||
message.error('Please enter the RSS link!');
|
||||
} else {
|
||||
try {
|
||||
analysis.loading = true;
|
||||
const { onError, onResult } = await apiDownload.analysis(rss.value);
|
||||
onResult((data) => {
|
||||
rule.value = data;
|
||||
analysis.loading = false;
|
||||
analysis.next = true;
|
||||
console.log('rule', data);
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
message.error(err.status);
|
||||
analysis.loading = false;
|
||||
console.log('error', err);
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('Failed to analyser!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function collect() {
|
||||
if (rule.value) {
|
||||
try {
|
||||
loading.collect = true;
|
||||
const res = await apiDownload.collection(rule.value);
|
||||
loading.collect = false;
|
||||
if (res) {
|
||||
message.success('Collect Success!');
|
||||
getAll();
|
||||
show.value = false;
|
||||
} else {
|
||||
message.error('Collect Failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Collect Error!');
|
||||
}
|
||||
}
|
||||
}
|
||||
async function subscribe() {
|
||||
if (rule.value) {
|
||||
try {
|
||||
loading.subscribe = true;
|
||||
const res = await apiDownload.subscribe(rule.value);
|
||||
loading.subscribe = false;
|
||||
if (res) {
|
||||
message.success('Subscribe Success!');
|
||||
getAll();
|
||||
show.value = false;
|
||||
} else {
|
||||
message.error('Subscribe Failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Subscribe Error!');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" title="Add Bangumi" css="w-360px">
|
||||
<div v-if="!analysis.next" space-y-12px>
|
||||
<ab-setting
|
||||
v-model:data="rss"
|
||||
label="RSS Link"
|
||||
type="input"
|
||||
:prop="{
|
||||
placeholder: 'Please enter the RSS link',
|
||||
}"
|
||||
:bottom-line="true"
|
||||
></ab-setting>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" :loading="analysis.loading" @click="analyser"
|
||||
>Analyse</ab-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ab-rule v-model:rule="rule"></ab-rule>
|
||||
|
||||
<div flex="~ justify-end" space-x-10px>
|
||||
<ab-button size="small" :loading="loading.collect" @click="collect"
|
||||
>Collect</ab-button
|
||||
>
|
||||
<ab-button size="small" :loading="loading.subscribe" @click="subscribe"
|
||||
>Subscribe</ab-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</template>
|
||||
65
webui/src/components/ab-bangumi-card.vue
Normal file
65
webui/src/components/ab-bangumi-card.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { ErrorPicture, Write } from '@icon-park/vue-next';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
poster: string;
|
||||
name: string;
|
||||
season: number;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-150px is-btn @click="() => $emit('click')">
|
||||
<div rounded-4px overflow-hidden poster-shandow rel>
|
||||
<div w-full h-210px>
|
||||
<template v-if="poster !== ''">
|
||||
<img :src="`https://mikanani.me${poster}`" alt="poster" wh-full />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div wh-full f-cer border="1 white">
|
||||
<ErrorPicture theme="outline" size="24" fill="#333" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
abs
|
||||
f-cer
|
||||
z-1
|
||||
inset-0
|
||||
opacity-0
|
||||
transition-all
|
||||
duration-300
|
||||
hover:backdrop-blur-2px
|
||||
hover:bg-white
|
||||
hover:bg-opacity-30
|
||||
hover:opacity-100
|
||||
active:duration-0
|
||||
active:bg-opacity-60
|
||||
class="group"
|
||||
>
|
||||
<div
|
||||
text-white
|
||||
rounded="1/2"
|
||||
wh-44px
|
||||
f-cer
|
||||
bg-theme-row
|
||||
class="group-active:poster-pen-active"
|
||||
>
|
||||
<Write size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div px-4px py-8px>
|
||||
<div text-h3 truncate>{{ name }}</div>
|
||||
<div text-main>Season {{ season }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
37
webui/src/components/ab-change-account.vue
Normal file
37
webui/src/components/ab-change-account.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
const show = defineModel('show', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const { user, update } = useAuth();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" title="Change Account" css="w-365px">
|
||||
<div space-y-16px>
|
||||
<ab-label label="Username">
|
||||
<input
|
||||
v-model="user.username"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Password">
|
||||
<input
|
||||
v-model="user.password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" @click="update">Update</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</template>
|
||||
33
webui/src/components/ab-container.vue
Normal file
33
webui/src/components/ab-container.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div rounded-10px overflow-hidden>
|
||||
<div
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
fx-cer
|
||||
px-20px
|
||||
h-45px
|
||||
justify-between
|
||||
select-none
|
||||
>
|
||||
<div text-h2>{{ title }}</div>
|
||||
|
||||
<slot name="title-right"></slot>
|
||||
</div>
|
||||
|
||||
<div p-20px bg-white>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
119
webui/src/components/ab-edit-rule.vue
Normal file
119
webui/src/components/ab-edit-rule.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'apply', rule: BangumiRule): void;
|
||||
(e: 'enable', id: number): void;
|
||||
(
|
||||
e: 'deleteFile',
|
||||
type: 'disable' | 'delete',
|
||||
opts: { id: number; deleteFile: boolean }
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const show = defineModel('show', { default: false });
|
||||
const rule = defineModel<BangumiRule>('rule', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const deleteFileDialog = reactive<{
|
||||
show: boolean;
|
||||
type: 'disable' | 'delete';
|
||||
}>({
|
||||
show: false,
|
||||
type: 'disable',
|
||||
});
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
deleteFileDialog.show = false;
|
||||
}
|
||||
});
|
||||
|
||||
const showDeleteFileDialog = (type: 'disable' | 'delete') => {
|
||||
deleteFileDialog.show = true;
|
||||
deleteFileDialog.type = type;
|
||||
};
|
||||
|
||||
const close = () => (show.value = false);
|
||||
|
||||
function emitdeleteFile(deleteFile: boolean) {
|
||||
emit('deleteFile', deleteFileDialog.type, {
|
||||
id: rule.value.id,
|
||||
deleteFile,
|
||||
});
|
||||
}
|
||||
function emitApply() {
|
||||
emit('apply', rule.value);
|
||||
}
|
||||
|
||||
function emitEnable() {
|
||||
emit('enable', rule.value.id);
|
||||
}
|
||||
|
||||
const popupTitle = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return 'Enable Rule';
|
||||
} else {
|
||||
return 'Edit Rule';
|
||||
}
|
||||
});
|
||||
|
||||
const boxSize = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return 'w-300px';
|
||||
} else {
|
||||
return 'w-380px';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" :title="popupTitle" :css="boxSize">
|
||||
<div v-if="rule.deleted">
|
||||
<div>Do you want to enable this rule?</div>
|
||||
|
||||
<div line my-8px></div>
|
||||
|
||||
<div fx-cer justify-center space-x-10px>
|
||||
<ab-button size="small" type="warn" @click="() => emitEnable()"
|
||||
>Yes</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="() => close()">No</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else space-y-12px>
|
||||
<ab-rule v-model:rule="rule"></ab-rule>
|
||||
|
||||
<div fx-cer justify-end space-x-10px>
|
||||
<ab-button
|
||||
size="small"
|
||||
type="warn"
|
||||
@click="() => showDeleteFileDialog('disable')"
|
||||
>Disable</ab-button
|
||||
>
|
||||
<ab-button
|
||||
size="small"
|
||||
type="warn"
|
||||
@click="() => showDeleteFileDialog('delete')"
|
||||
>Delete</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="emitApply">Apply</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ab-popup v-model:show="deleteFileDialog.show" title="Delete">
|
||||
<div>Delete Local File?</div>
|
||||
<div line my-8px></div>
|
||||
|
||||
<div fx-cer justify-center space-x-10px>
|
||||
<ab-button size="small" type="warn" @click="() => emitdeleteFile(true)"
|
||||
>Yes</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="() => emitdeleteFile(false)"
|
||||
>No</ab-button
|
||||
>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</ab-popup>
|
||||
</template>
|
||||
41
webui/src/components/ab-fold-panel.vue
Normal file
41
webui/src/components/ab-fold-panel.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { Down, Up } from '@icon-park/vue-next';
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Disclosure v-slot="{ open }">
|
||||
<div rounded-10px overflow-hidden h-max>
|
||||
<DisclosureButton
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
fx-cer
|
||||
px-20px
|
||||
h-45px
|
||||
justify-between
|
||||
>
|
||||
<div text-h2>{{ title }}</div>
|
||||
|
||||
<Component :is="open ? Up : Down" size="24" />
|
||||
</DisclosureButton>
|
||||
|
||||
<div bg-white py-20px :class="[open ? 'px-20px' : 'px-8px']">
|
||||
<div v-show="!open" line my-12px></div>
|
||||
|
||||
<DisclosurePanel>
|
||||
<slot></slot>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure>
|
||||
</template>
|
||||
18
webui/src/components/ab-label.vue
Normal file
18
webui/src/components/ab-label.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
}>(),
|
||||
{
|
||||
label: '',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ items-start" justify-between>
|
||||
<div>{{ label }}</div>
|
||||
|
||||
<slot> </slot>
|
||||
</div>
|
||||
</template>
|
||||
69
webui/src/components/ab-popup.vue
Normal file
69
webui/src/components/ab-popup.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from '@headlessui/vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
maskClick?: boolean;
|
||||
css?: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
maskClick: true,
|
||||
css: '',
|
||||
}
|
||||
);
|
||||
|
||||
const show = defineModel('show', { default: false });
|
||||
|
||||
function close() {
|
||||
if (props.maskClick) {
|
||||
show.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot appear :show="show" as="template">
|
||||
<Dialog as="div" class="relative z-10" @close="close">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-center justify-center p-4 text-center"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel>
|
||||
<ab-container :title="title" :class="[css]">
|
||||
<slot></slot>
|
||||
</ab-container>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
64
webui/src/components/ab-rule.vue
Normal file
64
webui/src/components/ab-rule.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const rule = defineModel<BangumiRule>('rule', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const items: SettingItem<BangumiRule>[] = [
|
||||
{
|
||||
configKey: 'official_title',
|
||||
label: 'Officical Ttile',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'year',
|
||||
label: 'Year',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'season',
|
||||
label: 'Season',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'offset',
|
||||
label: 'Offset',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'filter',
|
||||
label: 'Exclude',
|
||||
type: 'dynamic-tags',
|
||||
bottomLine: true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="rule[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</template>
|
||||
45
webui/src/components/ab-setting.vue
Normal file
45
webui/src/components/ab-setting.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { NDynamicTags } from 'naive-ui';
|
||||
import type { AbSettingProps } from '#/components';
|
||||
|
||||
withDefaults(defineProps<AbSettingProps>(), {
|
||||
css: '',
|
||||
bottomLine: false,
|
||||
});
|
||||
|
||||
const data = defineModel<any>('data');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ab-label :label="label">
|
||||
<AbSwitch
|
||||
v-if="type === 'switch'"
|
||||
v-model:checked="data"
|
||||
v-bind="prop"
|
||||
:class="css"
|
||||
></AbSwitch>
|
||||
|
||||
<AbSelect
|
||||
v-else-if="type === 'select'"
|
||||
v-model="data"
|
||||
v-bind="prop"
|
||||
:class="css"
|
||||
></AbSelect>
|
||||
|
||||
<input
|
||||
v-else-if="type === 'input'"
|
||||
v-model="data"
|
||||
ab-input
|
||||
:class="css"
|
||||
v-bind="prop"
|
||||
/>
|
||||
|
||||
<div v-else-if="type === 'dynamic-tags'" max-w-200px>
|
||||
<NDynamicTags v-model:value="data" size="small"></NDynamicTags>
|
||||
</div>
|
||||
</ab-label>
|
||||
|
||||
<div v-if="bottomLine" line my-12px></div>
|
||||
</div>
|
||||
</template>
|
||||
80
webui/src/components/ab-status-bar.vue
Normal file
80
webui/src/components/ab-status-bar.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
|
||||
import { AddOne, More } from '@icon-park/vue-next';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
running: boolean;
|
||||
items: {
|
||||
id: number;
|
||||
icon: any;
|
||||
label: string;
|
||||
handle?: () => void | Promise<void>;
|
||||
}[];
|
||||
}>(),
|
||||
{
|
||||
running: false,
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits(['clickAdd']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu>
|
||||
<div rel>
|
||||
<div fx-cer space-x-16px>
|
||||
<AddOne
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
@click="() => $emit('clickAdd')"
|
||||
/>
|
||||
|
||||
<MenuButton bg-transparent is-btn btn-click>
|
||||
<More theme="outline" size="24" fill="#fff" />
|
||||
</MenuButton>
|
||||
|
||||
<ab-status :running="running" />
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
abs
|
||||
top-50px
|
||||
w-120px
|
||||
rounded-8px
|
||||
bg-white
|
||||
overflow-hidden
|
||||
shadow
|
||||
z-99
|
||||
>
|
||||
<MenuItem v-for="i in items" :key="i.id" v-slot="{ active }">
|
||||
<div
|
||||
w-full
|
||||
h-32px
|
||||
px-12px
|
||||
fx-cer
|
||||
justify-between
|
||||
is-btn
|
||||
hover:text-white
|
||||
hover:bg-primary
|
||||
class="group"
|
||||
:class="[active ? 'text-white bg-theme-row' : 'text-black']"
|
||||
@click="() => i.handle && i.handle()"
|
||||
>
|
||||
<div text-main>{{ i.label }}</div>
|
||||
|
||||
<div
|
||||
class="group-hover:text-white"
|
||||
:class="[active ? 'text-white' : 'text-primary']"
|
||||
>
|
||||
<Component :is="i.icon" size="16"></Component>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-add.stories.ts
Normal file
22
webui/src/components/basic/ab-add.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbAdd from './ab-add.vue';
|
||||
|
||||
const meta: Meta<typeof AbAdd> = {
|
||||
title: 'basic/ab-add',
|
||||
component: AbAdd,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbAdd>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbAdd },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-add v-bind="args"></ab-add>',
|
||||
}),
|
||||
};
|
||||
41
webui/src/components/basic/ab-add.vue
Normal file
41
webui/src/components/basic/ab-add.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
rounded="1/2"
|
||||
wh-36px
|
||||
f-cer
|
||||
rel
|
||||
transition-colors
|
||||
class="box"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div class="line" abs></div>
|
||||
<div class="line" abs rotate-90></div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$normal: #493475;
|
||||
$hover: #756596;
|
||||
|
||||
.box {
|
||||
background: $normal;
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $normal;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
32
webui/src/components/basic/ab-button.stories.ts
Normal file
32
webui/src/components/basic/ab-button.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbButton from './ab-button.vue';
|
||||
|
||||
const meta: Meta<typeof AbButton> = {
|
||||
title: 'basic/ab-button',
|
||||
component: AbButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'warn'],
|
||||
},
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['big', 'normal', 'small'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbButton>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbButton },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-button v-bind="args">button</ab-button>',
|
||||
}),
|
||||
};
|
||||
70
webui/src/components/basic/ab-button.vue
Normal file
70
webui/src/components/basic/ab-button.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { NSpin } from 'naive-ui';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'primary' | 'warn';
|
||||
size?: 'big' | 'normal' | 'small';
|
||||
link?: string | null;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
type: 'primary',
|
||||
size: 'normal',
|
||||
link: null,
|
||||
loading: false,
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const buttonSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'rounded-10px text-h1 w-276px h-55px text-h1';
|
||||
case 'normal':
|
||||
return 'rounded-6px w-170px h-36px';
|
||||
case 'small':
|
||||
return 'rounded-6px w-86px h-28px text-main';
|
||||
}
|
||||
});
|
||||
|
||||
const loadingSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'large';
|
||||
case 'normal':
|
||||
return 'small';
|
||||
case 'small':
|
||||
return 18;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
text-white
|
||||
outline-none
|
||||
f-cer
|
||||
:class="[`type-${type}`, buttonSize]"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<NSpin :show="loading" :size="loadingSize">
|
||||
<slot></slot>
|
||||
</NSpin>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type {
|
||||
&-primary {
|
||||
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
|
||||
}
|
||||
|
||||
&-warn {
|
||||
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
webui/src/components/basic/ab-checkbox.stories.ts
Normal file
22
webui/src/components/basic/ab-checkbox.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbCheckbox from './ab-checkbox.vue';
|
||||
|
||||
const meta: Meta<typeof AbCheckbox> = {
|
||||
title: 'basic/ab-checkbox',
|
||||
component: AbCheckbox,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbCheckbox>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbCheckbox },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-checkbox v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
45
webui/src/components/basic/ab-checkbox.vue
Normal file
45
webui/src/components/basic/ab-checkbox.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { Switch } from '@headlessui/vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
small?: boolean;
|
||||
}>(),
|
||||
{
|
||||
small: false,
|
||||
}
|
||||
);
|
||||
|
||||
const checked = defineModel<boolean>({ default: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div flex items-center space-x-8px is-btn>
|
||||
<slot name="before"></slot>
|
||||
|
||||
<div
|
||||
rounded-4px
|
||||
rel
|
||||
f-cer
|
||||
bg-white
|
||||
border="3px #3c239f"
|
||||
:class="[small ? 'wh-16px' : 'wh-32px', !checked && 'group']"
|
||||
>
|
||||
<div
|
||||
rounded-2px
|
||||
transition-all
|
||||
duration-300
|
||||
:class="[
|
||||
small ? 'wh-8px' : 'wh-16px',
|
||||
checked ? 'bg-[#3c239f]' : 'bg-transparent',
|
||||
]"
|
||||
group-hover:bg="#cccad4"
|
||||
group-active:bg="#3c239f"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-page-title.stories.ts
Normal file
22
webui/src/components/basic/ab-page-title.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbPageTitle from './ab-page-title.vue';
|
||||
|
||||
const meta: Meta<typeof AbPageTitle> = {
|
||||
title: 'basic/ab-PageTitle',
|
||||
component: AbPageTitle,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbPageTitle>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbPageTitle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-page-title v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
17
webui/src/components/basic/ab-page-title.vue
Normal file
17
webui/src/components/basic/ab-page-title.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fx-cer space-x-12px>
|
||||
<div text-h1>{{ title }}</div>
|
||||
<div w-160px h-3px bg-theme-row rounded-full></div>
|
||||
</div>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-search.stories.ts
Normal file
22
webui/src/components/basic/ab-search.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSearch from './ab-search.vue';
|
||||
|
||||
const meta: Meta<typeof AbSearch> = {
|
||||
title: 'basic/ab-search',
|
||||
component: AbSearch,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSearch>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSearch },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-search v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
58
webui/src/components/basic/ab-search.vue
Normal file
58
webui/src/components/basic/ab-search.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import { Search } from '@icon-park/vue-next';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
value: '',
|
||||
placeholder: '',
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:value', 'click-search']);
|
||||
|
||||
function onInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
emit('update:value', input.value);
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
emit('click-search', props.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
bg="#7752B4"
|
||||
text-white
|
||||
fx-cer
|
||||
rounded-12px
|
||||
h-36px
|
||||
px-12px
|
||||
space-x-12px
|
||||
w-276px
|
||||
focus-within:w-396px
|
||||
transition-width
|
||||
>
|
||||
<Search
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
@click="onSearch"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
input-reset
|
||||
@input="onInput"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-select.stories.ts
Normal file
22
webui/src/components/basic/ab-select.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSelect from './ab-select.vue';
|
||||
|
||||
const meta: Meta<typeof AbSelect> = {
|
||||
title: 'basic/ab-select',
|
||||
component: AbSelect,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSelect>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSelect },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-select v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
111
webui/src/components/basic/ab-select.vue
Normal file
111
webui/src/components/basic/ab-select.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from '@headlessui/vue';
|
||||
import { Down, Up } from '@icon-park/vue-next';
|
||||
import { isObject, isString } from 'lodash';
|
||||
import type { SelectItem } from '#/components';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: SelectItem | string;
|
||||
items: Array<SelectItem | string>;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const selected = ref<SelectItem | string>(
|
||||
props.modelValue || (props.items?.[0] ?? '')
|
||||
);
|
||||
|
||||
const otherItems = computed(() => {
|
||||
return (
|
||||
props.items.filter((e) => {
|
||||
if (isString(e) && isString(selected.value)) {
|
||||
return e !== selected.value;
|
||||
} else if (isObject(e) && isObject(selected.value)) {
|
||||
return e.id !== selected.value.id;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const label = computed(() => {
|
||||
if (isString(selected.value)) {
|
||||
return selected.value;
|
||||
} else {
|
||||
return selected.value.label ?? selected.value.value;
|
||||
}
|
||||
});
|
||||
|
||||
function getLabel(item: SelectItem | string) {
|
||||
if (isString(item)) {
|
||||
return item;
|
||||
} else {
|
||||
return item.label ?? item.value;
|
||||
}
|
||||
}
|
||||
|
||||
function getDisabled(item: SelectItem | string) {
|
||||
return isString(item) ? false : item.disabled;
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
emit('update:modelValue', selected.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Listbox v-slot="{ open }" v-model="selected">
|
||||
<div
|
||||
rel
|
||||
flex="inline col"
|
||||
rounded-6px
|
||||
border="1px black"
|
||||
text-main
|
||||
py-4px
|
||||
px-12px
|
||||
>
|
||||
<ListboxButton bg-transparent fx-cer justify-between space-x-24px>
|
||||
<div>
|
||||
{{ label }}
|
||||
</div>
|
||||
<div :class="[{ hidden: open }]">
|
||||
<Down />
|
||||
</div>
|
||||
</ListboxButton>
|
||||
|
||||
<ListboxOptions mt-8px>
|
||||
<div flex="~ items-end" justify-between space-x-24px>
|
||||
<div flex="~ col" space-y-8px>
|
||||
<ListboxOption
|
||||
v-for="item in otherItems"
|
||||
v-slot="{ active }"
|
||||
:key="isString(item) ? item : item.id"
|
||||
:value="item"
|
||||
:disabled="getDisabled(item)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
{ 'text-primary': active },
|
||||
getDisabled(item) ? 'is-disabled' : 'is-btn',
|
||||
]"
|
||||
>
|
||||
{{ getLabel(item) }}
|
||||
</div>
|
||||
</ListboxOption>
|
||||
</div>
|
||||
|
||||
<div :class="[{ hidden: !open }]"><Up /></div>
|
||||
</div>
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-status.stories.ts
Normal file
22
webui/src/components/basic/ab-status.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbStatus from './ab-status.vue';
|
||||
|
||||
const meta: Meta<typeof AbStatus> = {
|
||||
title: 'basic/ab-status',
|
||||
component: AbStatus,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbStatus>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbStatus },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-status v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
23
webui/src/components/basic/ab-status.vue
Normal file
23
webui/src/components/basic/ab-status.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
running: boolean;
|
||||
}>(),
|
||||
{
|
||||
running: false,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div wh-24px f-cer>
|
||||
<div rounded="1/2" f-cer border="2px solid white" wh-22px>
|
||||
<div
|
||||
:class="[running ? 'bg-running' : 'bg-stopped']"
|
||||
rounded="1/2"
|
||||
wh-10px
|
||||
transition-colors
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
webui/src/components/basic/ab-switch.stories.ts
Normal file
22
webui/src/components/basic/ab-switch.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSwitch from './ab-switch.vue';
|
||||
|
||||
const meta: Meta<typeof AbSwitch> = {
|
||||
title: 'basic/ab-switch',
|
||||
component: AbSwitch,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSwitch>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSwitch },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-switch v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
74
webui/src/components/basic/ab-switch.vue
Normal file
74
webui/src/components/basic/ab-switch.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { Switch } from '@headlessui/vue';
|
||||
|
||||
const checked = defineModel<boolean>('checked', {
|
||||
default: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div
|
||||
is-btn
|
||||
w-48px
|
||||
h-28px
|
||||
rounded-full
|
||||
rel
|
||||
flex="inline items-center"
|
||||
transition-colors
|
||||
duration-300
|
||||
p-3px
|
||||
shadow="~ inset"
|
||||
class="box"
|
||||
:class="{ checked }"
|
||||
>
|
||||
<div
|
||||
wh-22px
|
||||
rounded="1/2"
|
||||
transition-all
|
||||
duration-300
|
||||
class="slider"
|
||||
:class="{ checked, 'translate-x-20px': checked }"
|
||||
></div>
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
$bg-unchecked: #929292;
|
||||
$bg-checked: #e7e7e7;
|
||||
|
||||
$slider-unchecked: #ececef;
|
||||
$slider-unchecked-hover: #dbd8ec;
|
||||
|
||||
$slider-checked: #1c1259;
|
||||
$slider-checked-hover: #62589e;
|
||||
|
||||
.box {
|
||||
background: $bg-unchecked;
|
||||
|
||||
&.checked {
|
||||
background: $bg-checked;
|
||||
}
|
||||
|
||||
&:hover .slider {
|
||||
&:not(.checked) {
|
||||
background: $slider-unchecked-hover;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: $slider-checked-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
&:not(.checked) {
|
||||
background: $slider-unchecked;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: $slider-checked;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
webui/src/components/layout/ab-sidebar.vue
Normal file
145
webui/src/components/layout/ab-sidebar.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Calendar,
|
||||
Download,
|
||||
Home,
|
||||
Log,
|
||||
Logout,
|
||||
MenuUnfold,
|
||||
Play,
|
||||
SettingTwo,
|
||||
} from '@icon-park/vue-next';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open?: boolean;
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
}
|
||||
);
|
||||
|
||||
const show = ref(props.open);
|
||||
const toggle = () => (show.value = !show.value);
|
||||
const route = useRoute();
|
||||
const { logout } = useAuth();
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 1,
|
||||
icon: Home,
|
||||
label: 'HomePage',
|
||||
path: '/bangumi',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Calendar,
|
||||
label: 'Calendar',
|
||||
path: '/calendar',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Play,
|
||||
label: 'Player',
|
||||
path: '/player',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Download,
|
||||
label: 'Downloader',
|
||||
path: '/downloader',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: Log,
|
||||
label: 'Log',
|
||||
path: '/log',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: SettingTwo,
|
||||
label: 'Config',
|
||||
path: '/config',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[show ? 'w-240px' : 'w-72px']"
|
||||
bg-theme-col
|
||||
text-white
|
||||
transition-width
|
||||
pb-12px
|
||||
rounded-12px
|
||||
>
|
||||
<div overflow-hidden wh-full flex="~ col">
|
||||
<div
|
||||
w-full
|
||||
h-60px
|
||||
is-btn
|
||||
f-cer
|
||||
rounded-t-10px
|
||||
bg="#E7E7E7"
|
||||
text="#2A1C52"
|
||||
rel
|
||||
@click="toggle"
|
||||
>
|
||||
<div :class="[!show && 'abs opacity-0']" transition-opacity>
|
||||
<div text-h1>Menu</div>
|
||||
</div>
|
||||
|
||||
<MenuUnfold
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#2A1C52"
|
||||
abs
|
||||
left="24px"
|
||||
:class="[show && 'rotate-y-180']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RouterLink
|
||||
v-for="i in items"
|
||||
:key="i.id"
|
||||
:to="i.path"
|
||||
replace
|
||||
:title="i.label"
|
||||
fx-cer
|
||||
px-24px
|
||||
space-x-42px
|
||||
h-48px
|
||||
is-btn
|
||||
transition-colors
|
||||
hover:bg="#F1F5FA"
|
||||
hover:text="#2A1C52"
|
||||
:class="[
|
||||
route.path === i.path && 'bg-[#F1F5FA] text-[#2A1C52]',
|
||||
i.hidden && 'hidden',
|
||||
]"
|
||||
>
|
||||
<Component :is="i.icon" :size="24" />
|
||||
<div text-h2>{{ i.label }}</div>
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
title="logout"
|
||||
mt-auto
|
||||
fx-cer
|
||||
px-24px
|
||||
space-x-42px
|
||||
h-48px
|
||||
is-btn
|
||||
transition-colors
|
||||
hover:bg="#F1F5FA"
|
||||
hover:text="#2A1C52"
|
||||
@click="logout"
|
||||
>
|
||||
<Logout :size="24" />
|
||||
<div text-h2>Logout</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
90
webui/src/components/layout/ab-topbar.vue
Normal file
90
webui/src/components/layout/ab-topbar.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Me,
|
||||
Pause,
|
||||
PlayOne,
|
||||
Power,
|
||||
Refresh,
|
||||
Format,
|
||||
} from '@icon-park/vue-next';
|
||||
|
||||
const search = ref('');
|
||||
const show = ref(false);
|
||||
const showAdd = ref(false);
|
||||
|
||||
const { onUpdate, offUpdate, start, pause, shutdown, restart, resetRule } =
|
||||
useProgramStore();
|
||||
const { running } = storeToRefs(useProgramStore());
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'Start',
|
||||
icon: PlayOne,
|
||||
handle: start,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Pause',
|
||||
icon: Pause,
|
||||
handle: pause,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Restart',
|
||||
icon: Refresh,
|
||||
handle: restart,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Shutdown',
|
||||
icon: Power,
|
||||
handle: shutdown,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'Reset Rule',
|
||||
icon: Format,
|
||||
handle: resetRule,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'Profile',
|
||||
icon: Me,
|
||||
handle: () => {
|
||||
show.value = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onBeforeMount(() => {
|
||||
onUpdate();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
offUpdate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-60px bg-theme-row text-white rounded-12px fx-cer px-24px>
|
||||
<div fx-cer space-x-16px>
|
||||
<img src="/favicon-light.svg" alt="favicon" wh-24px />
|
||||
<img src="/AutoBangumi.svg" alt="AutoBangumi" h-24px rel top-2px />
|
||||
</div>
|
||||
|
||||
<ab-search v-model:value="search" hidden />
|
||||
|
||||
<div ml-auto>
|
||||
<ab-status-bar
|
||||
:items="items"
|
||||
:running="running"
|
||||
@click-add="() => (showAdd = true)"
|
||||
></ab-status-bar>
|
||||
</div>
|
||||
|
||||
<ab-change-account v-model:show="show"></ab-change-account>
|
||||
|
||||
<ab-add-bangumi v-model:show="showAdd"></ab-add-bangumi>
|
||||
</div>
|
||||
</template>
|
||||
76
webui/src/components/setting/config-download.vue
Normal file
76
webui/src/components/setting/config-download.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Downloader, DownloaderType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const downloader = getSettingGroup('downloader');
|
||||
const downloaderType: DownloaderType = ['qbittorrent'];
|
||||
|
||||
const items: SettingItem<Downloader>[] = [
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Downloader Type',
|
||||
type: 'select',
|
||||
css: 'w-115px',
|
||||
prop: {
|
||||
items: downloaderType,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'host',
|
||||
label: 'Host',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '127.0.0.1:8989',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'username',
|
||||
label: 'Username',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'admin',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'password',
|
||||
label: 'Password',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'admindmin',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'path',
|
||||
label: 'Download Path',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '/downloads/Bangumi',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'ssl',
|
||||
label: 'SSL',
|
||||
type: 'switch',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Downloader Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="downloader[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
54
webui/src/components/setting/config-manage.vue
Normal file
54
webui/src/components/setting/config-manage.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiManage, RenameMethod } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const manage = getSettingGroup('bangumi_manage');
|
||||
const renameMethod: RenameMethod = ['normal', 'pn', 'advance', 'none'];
|
||||
|
||||
const items: SettingItem<BangumiManage>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'rename_method',
|
||||
label: 'Rename Method',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: renameMethod,
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'eps_complete',
|
||||
label: 'Eps complete',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'group_tag',
|
||||
label: 'Add Group Tag',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'remove_bad_torrent',
|
||||
label: 'Delete Bad Torrent',
|
||||
type: 'switch',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Manage Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="manage[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
67
webui/src/components/setting/config-normal.vue
Normal file
67
webui/src/components/setting/config-normal.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Log, Program } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const program = getSettingGroup('program');
|
||||
const log = getSettingGroup('log');
|
||||
|
||||
const programItems: SettingItem<Program>[] = [
|
||||
{
|
||||
configKey: 'rss_time',
|
||||
label: 'Interval Time of Rss',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'rename_time',
|
||||
label: 'Interval Time of Rename',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'webui_port',
|
||||
label: 'WebUI Port',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
];
|
||||
|
||||
const logItems: SettingItem<Log> = {
|
||||
configKey: 'debug_enable',
|
||||
label: 'Debug',
|
||||
type: 'switch',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Normal Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in programItems"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="program[i.configKey]"
|
||||
></ab-setting>
|
||||
|
||||
<ab-setting
|
||||
v-bind="logItems"
|
||||
v-model:data="log[logItems.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
58
webui/src/components/setting/config-notification.vue
Normal file
58
webui/src/components/setting/config-notification.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Notification, NotificationType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const notification = getSettingGroup('notification');
|
||||
const notificationType: NotificationType = ['telegram', 'server-chan', 'bark', 'wecom'];
|
||||
|
||||
const items: SettingItem<Notification>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Type',
|
||||
type: 'select',
|
||||
css: 'w-140px',
|
||||
prop: {
|
||||
items: notificationType,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'token',
|
||||
label: 'Token',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'token',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'chat_id',
|
||||
label: 'Chat ID',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'chat id',
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Notification Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="notification[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
88
webui/src/components/setting/config-parser.vue
Normal file
88
webui/src/components/setting/config-parser.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
RssParser,
|
||||
RssParserLang,
|
||||
RssParserMethodType,
|
||||
RssParserType,
|
||||
} from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const parser = getSettingGroup('rss_parser');
|
||||
|
||||
const sourceItems: RssParserType = ['mikan'];
|
||||
const langs: RssParserLang = ['zh', 'en', 'jp'];
|
||||
/** @ts-expect-error Incorrect order */
|
||||
const parserMethods: RssParserMethodType = ['tmdb', 'mikan', 'parser'];
|
||||
|
||||
const items: SettingItem<RssParser>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Source',
|
||||
type: 'select',
|
||||
css: 'w-115px',
|
||||
prop: {
|
||||
items: sourceItems,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'token',
|
||||
label: 'Token',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'token',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'custom_url',
|
||||
label: 'Custom Url',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'mikanime.tv',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'language',
|
||||
label: 'Language',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: langs,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'parser_type',
|
||||
label: 'Parser Type',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: parserMethods,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'filter',
|
||||
label: 'Exclude',
|
||||
type: 'dynamic-tags',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Parser Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="parser[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
23
webui/src/components/setting/config-player.vue
Normal file
23
webui/src/components/setting/config-player.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
const { types, type, url } = storeToRefs(usePlayerStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Media Player Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-model:data="type"
|
||||
type="select"
|
||||
label="type"
|
||||
:prop="{ items: types }"
|
||||
></ab-setting>
|
||||
|
||||
<ab-setting
|
||||
v-model:data="url"
|
||||
type="input"
|
||||
label="url"
|
||||
:prop="{ placeholder: 'media player url' }"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
75
webui/src/components/setting/config-proxy.vue
Normal file
75
webui/src/components/setting/config-proxy.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Proxy, ProxyType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const proxy = getSettingGroup('proxy');
|
||||
const proxyType: ProxyType = ['http', 'https', 'socks5'];
|
||||
|
||||
const items: SettingItem<Proxy>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Proxy Type',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: proxyType,
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'host',
|
||||
label: 'Host',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '127.0.0.1',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'port',
|
||||
label: 'Port',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '7890',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'username',
|
||||
label: 'Username',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'username',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'password',
|
||||
label: 'Password',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'password',
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Proxy Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="proxy[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
57
webui/src/hooks/useApi.ts
Normal file
57
webui/src/hooks/useApi.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
type AnyAsyncFuntion<TData = any> = (...args: any[]) => Promise<TData>;
|
||||
|
||||
export function useApi<
|
||||
TError = any,
|
||||
TApi extends AnyAsyncFuntion = AnyAsyncFuntion,
|
||||
TData = Awaited<ReturnType<TApi>>
|
||||
>(
|
||||
api: TApi,
|
||||
options?: {
|
||||
failRule?: (data: TData) => boolean;
|
||||
message?: {
|
||||
success?: string;
|
||||
fail?: string;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const data = ref<TData>();
|
||||
const isLoading = ref(false);
|
||||
|
||||
const fetchResult = createEventHook<TData>();
|
||||
const fetchError = createEventHook<TError>();
|
||||
const message = useMessage();
|
||||
|
||||
function execute(...params: Parameters<TApi>) {
|
||||
isLoading.value = true;
|
||||
|
||||
api(...params)
|
||||
.then((res: TData) => {
|
||||
data.value = res;
|
||||
fetchResult.trigger(res);
|
||||
|
||||
if (options?.failRule && options.failRule(res)) {
|
||||
options.message?.fail && message.error(options.message.fail);
|
||||
} else {
|
||||
options?.message?.success && message.success(options.message.success);
|
||||
}
|
||||
})
|
||||
.catch((err: TError) => {
|
||||
fetchError.trigger(err);
|
||||
|
||||
options?.message?.error && message.error(options.message.error);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
|
||||
execute,
|
||||
onResult: fetchResult.on,
|
||||
onError: fetchError.on,
|
||||
};
|
||||
}
|
||||
119
webui/src/hooks/useAuth.ts
Normal file
119
webui/src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { User } from '#/auth';
|
||||
import type { ApiError } from '#/api';
|
||||
|
||||
export const useAuth = createSharedComposable(() => {
|
||||
const auth = useLocalStorage('auth', '');
|
||||
const message = useMessage();
|
||||
|
||||
const user = reactive<User>({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const isLogin = computed(() => auth.value !== '');
|
||||
|
||||
function clearUser() {
|
||||
user.username = '';
|
||||
user.password = '';
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (user.username === '') {
|
||||
message.warning('Please Enter Username!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.password === '') {
|
||||
message.warning('Please Enter Password!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function login() {
|
||||
const { execute, onResult, onError } = useApi(apiAuth.login, {
|
||||
message: {
|
||||
success: 'Login Success!',
|
||||
},
|
||||
});
|
||||
|
||||
onResult((res) => {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
clearUser();
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
const error = err as ApiError;
|
||||
|
||||
if (error.status === 404) {
|
||||
message.error('请更新AutoBangumi!');
|
||||
} else {
|
||||
message.error(error.detail);
|
||||
}
|
||||
});
|
||||
|
||||
if (check()) {
|
||||
execute(user.username, user.password);
|
||||
}
|
||||
}
|
||||
|
||||
const { execute: logout, onResult: onLogoutResult } = useApi(apiAuth.logout, {
|
||||
failRule: (res) => !res,
|
||||
message: {
|
||||
success: 'Logout Success!',
|
||||
fail: 'Logout Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
onLogoutResult(() => {
|
||||
clearUser();
|
||||
auth.value = '';
|
||||
});
|
||||
|
||||
const { execute: refresh, onResult: onRefreshResult } = useApi(
|
||||
apiAuth.refresh
|
||||
);
|
||||
|
||||
onRefreshResult((res) => {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
});
|
||||
|
||||
function update() {
|
||||
const { execute, onResult } = useApi(apiAuth.update, {
|
||||
failRule: (res) => res.message !== 'update success',
|
||||
message: {
|
||||
success: 'Update Success!',
|
||||
fail: 'Update Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
onResult((res) => {
|
||||
if (res.message === 'update success') {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
clearUser();
|
||||
} else {
|
||||
user.password = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (check()) {
|
||||
if (user.password.length < 8) {
|
||||
message.error('Password must be at least 8 characters long!');
|
||||
} else {
|
||||
execute(user.username, user.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
auth,
|
||||
user,
|
||||
isLogin,
|
||||
|
||||
login,
|
||||
logout,
|
||||
refresh,
|
||||
update,
|
||||
};
|
||||
});
|
||||
6
webui/src/hooks/useMessage.ts
Normal file
6
webui/src/hooks/useMessage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
|
||||
export const useMessage = createSharedComposable(() => {
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
return message;
|
||||
});
|
||||
14
webui/src/main.ts
Normal file
14
webui/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { router } from './router';
|
||||
import App from './App.vue';
|
||||
|
||||
import '@unocss/reset/tailwind-compat.css';
|
||||
import 'virtual:uno.css';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(pinia);
|
||||
app.mount('#app');
|
||||
28
webui/src/pages/index.vue
Normal file
28
webui/src/pages/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Index',
|
||||
redirect: '/bangumi',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div layout-container>
|
||||
<ab-topbar />
|
||||
|
||||
<main layout-main>
|
||||
<ab-sidebar />
|
||||
|
||||
<div layout-content>
|
||||
<ab-page-title :title="$route.name"></ab-page-title>
|
||||
|
||||
<div overflow-auto mt-12px flex-grow>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
117
webui/src/pages/index/bangumi.vue
Normal file
117
webui/src/pages/index/bangumi.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const { data } = storeToRefs(useBangumiStore());
|
||||
const { getAll, useUpdateRule, useDisableRule, useEnableRule, useDeleteRule } =
|
||||
useBangumiStore();
|
||||
|
||||
const editRule = reactive<{
|
||||
show: boolean;
|
||||
item: BangumiRule;
|
||||
}>({
|
||||
show: false,
|
||||
item: {
|
||||
added: false,
|
||||
deleted: false,
|
||||
dpi: '',
|
||||
eps_collect: false,
|
||||
filter: [],
|
||||
group_name: '',
|
||||
id: 0,
|
||||
official_title: '',
|
||||
offset: 0,
|
||||
poster_link: '',
|
||||
rss_link: [],
|
||||
rule_name: '',
|
||||
save_path: '',
|
||||
season: 1,
|
||||
season_raw: '',
|
||||
source: null,
|
||||
subtitle: '',
|
||||
title_raw: '',
|
||||
year: null,
|
||||
},
|
||||
});
|
||||
|
||||
function open(data: BangumiRule) {
|
||||
editRule.show = true;
|
||||
editRule.item = data;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
editRule.show = false;
|
||||
getAll();
|
||||
}
|
||||
|
||||
const { execute: updateRule, onResult: onUpdateRuleResult } = useUpdateRule();
|
||||
const { execute: enableRule, onResult: onEnableRuleResult } = useEnableRule();
|
||||
const { execute: disableRule, onResult: onDisableRuleResult } =
|
||||
useDisableRule();
|
||||
const { execute: deleteRule, onResult: onDeleteRuleResult } = useDeleteRule();
|
||||
const message = useMessage();
|
||||
|
||||
onUpdateRuleResult(({ msg }) => {
|
||||
message.success(msg);
|
||||
refresh();
|
||||
});
|
||||
|
||||
onDisableRuleResult(({ msg }) => {
|
||||
message.success(msg);
|
||||
refresh();
|
||||
});
|
||||
|
||||
onEnableRuleResult(({ msg }) => {
|
||||
message.success(msg);
|
||||
refresh();
|
||||
});
|
||||
|
||||
onDeleteRuleResult(({ msg }) => {
|
||||
message.success(msg);
|
||||
refresh();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
getAll();
|
||||
});
|
||||
|
||||
function ruleManage(
|
||||
type: 'disable' | 'delete',
|
||||
id: number,
|
||||
deleteFile: boolean
|
||||
) {
|
||||
if (type === 'disable') {
|
||||
disableRule(id, deleteFile);
|
||||
}
|
||||
if (type === 'delete') {
|
||||
deleteRule(id, deleteFile);
|
||||
}
|
||||
}
|
||||
|
||||
definePage({
|
||||
name: 'Bangumi List',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ wrap" gap-y-12px gap-x-50px>
|
||||
<ab-bangumi-card
|
||||
v-for="i in data"
|
||||
:key="i.id"
|
||||
:class="[i.deleted && 'grayscale']"
|
||||
:poster="i.poster_link ?? ''"
|
||||
:name="i.official_title"
|
||||
:season="i.season"
|
||||
@click="() => open(i)"
|
||||
></ab-bangumi-card>
|
||||
|
||||
<ab-edit-rule
|
||||
v-model:show="editRule.show"
|
||||
v-model:rule="editRule.item"
|
||||
@enable="(id) => enableRule(id)"
|
||||
@delete-file="
|
||||
(type, { id, deleteFile }) => ruleManage(type, id, deleteFile)
|
||||
"
|
||||
@apply="(rule) => updateRule(rule)"
|
||||
></ab-edit-rule>
|
||||
</div>
|
||||
</template>
|
||||
9
webui/src/pages/index/calendar.vue
Normal file
9
webui/src/pages/index/calendar.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Calendar',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>null</div>
|
||||
</template>
|
||||
38
webui/src/pages/index/config.vue
Normal file
38
webui/src/pages/index/config.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
const { getConfig, setConfig } = useConfigStore();
|
||||
|
||||
getConfig();
|
||||
|
||||
definePage({
|
||||
name: 'Config',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full flex="~ col">
|
||||
<div grid="~ cols-2" gap-20px mb-auto>
|
||||
<div space-y-20px>
|
||||
<config-normal></config-normal>
|
||||
|
||||
<config-parser></config-parser>
|
||||
|
||||
<config-download></config-download>
|
||||
|
||||
<config-manage></config-manage>
|
||||
</div>
|
||||
|
||||
<div space-y-20px>
|
||||
<config-notification></config-notification>
|
||||
|
||||
<config-proxy></config-proxy>
|
||||
|
||||
<config-player></config-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div fx-cer justify-end gap-8px mt-20px>
|
||||
<ab-button type="warn" @click="getConfig">Cancel</ab-button>
|
||||
<ab-button @click="setConfig">Apply</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
9
webui/src/pages/index/downloader.vue
Normal file
9
webui/src/pages/index/downloader.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Downloader',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>null</div>
|
||||
</template>
|
||||
111
webui/src/pages/index/log.vue
Normal file
111
webui/src/pages/index/log.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
const { onUpdate, offUpdate, reset, copy } = useLogStore();
|
||||
const { log } = storeToRefs(useLogStore());
|
||||
|
||||
onActivated(() => {
|
||||
onUpdate();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
offUpdate();
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: 'Log',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ wrap" gap-12px>
|
||||
<ab-container title="Log" w-660px grow>
|
||||
<div
|
||||
rounded-10px
|
||||
border="1px solid black"
|
||||
overflow-auto
|
||||
p-10px
|
||||
max-h-60vh
|
||||
>
|
||||
<pre text-main>{{ log }}</pre>
|
||||
</div>
|
||||
|
||||
<div flex="~ justify-end" space-x-10px mt-12px>
|
||||
<ab-button type="warn" size="small" @click="reset">Reset</ab-button>
|
||||
<ab-button size="small" @click="copy">Copy</ab-button>
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<div grow w-500px space-y-20px>
|
||||
<ab-container title="Contact Infomation">
|
||||
<div space-y-12px>
|
||||
<ab-label label="Github">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://github.com/EstrellaXD/Auto_Bangumi"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="WebUI Repo">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://github.com/Rewrite0/Auto_Bangumi_WebUI"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<ab-label label="Twitter">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://twitter.com/Estrella_Pan"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Telegram Group">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://t.me/autobangumi"
|
||||
target="_blank"
|
||||
>
|
||||
Join
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<ab-container title="Bug Report">
|
||||
<div space-y-12px>
|
||||
<ab-button
|
||||
mx-auto
|
||||
text-16px
|
||||
w-300px
|
||||
h-46px
|
||||
rounded-10px
|
||||
link="https://github.com/EstrellaXD/Auto_Bangumi/issues"
|
||||
>Github Issue</ab-button
|
||||
>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<ab-button
|
||||
mx-auto
|
||||
text-16px
|
||||
w-300px
|
||||
h-46px
|
||||
rounded-10px
|
||||
link="mailto:estrellaxd05@gmail.com"
|
||||
>Email Contact</ab-button
|
||||
>
|
||||
</div>
|
||||
</ab-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
webui/src/pages/index/player.vue
Normal file
28
webui/src/pages/index/player.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Player',
|
||||
});
|
||||
|
||||
const { url } = storeToRefs(usePlayerStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="url === ''">
|
||||
<div wh-full f-cer text-h1 text-primary>
|
||||
<RouterLink to="/config" hover:underline
|
||||
>Please set up the media player</RouterLink
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<iframe
|
||||
v-else
|
||||
:src="url"
|
||||
frameborder="0"
|
||||
allowfullscreen="true"
|
||||
w-full
|
||||
h-full
|
||||
flex-1
|
||||
rounded-12px
|
||||
></iframe>
|
||||
</template>
|
||||
44
webui/src/pages/login.vue
Normal file
44
webui/src/pages/login.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
const { user, login } = useAuth();
|
||||
|
||||
definePage({
|
||||
name: 'Login',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div f-cer layout-container>
|
||||
<ab-container title="Login" w-365px>
|
||||
<div space-y-16px>
|
||||
<ab-label label="Username">
|
||||
<input
|
||||
v-model="user.username"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Password">
|
||||
<input
|
||||
v-model="user.password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
ab-input
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" @click="login">Login</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<div bg="#C7C4AB" text-white rounded-4px py-4px px-2em text-main>
|
||||
<div>Default: admin adminadmin</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
webui/src/router/index.ts
Normal file
34
webui/src/router/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router/auto';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
});
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const { isLogin } = useAuth();
|
||||
const { type, url } = storeToRefs(usePlayerStore());
|
||||
|
||||
if (!isLogin.value && to.path !== '/login') {
|
||||
return { name: 'Login' };
|
||||
}
|
||||
|
||||
if (isLogin.value && to.path === '/login') {
|
||||
return { name: 'Index' };
|
||||
}
|
||||
|
||||
if (type.value === 'jump' && url.value !== '' && to.path === '/player') {
|
||||
open(url.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
watch(isLogin, (val) => {
|
||||
if (to.path === '/login' && val) {
|
||||
router.replace({ name: 'Index' });
|
||||
}
|
||||
if (to.path !== '/login' && !val) {
|
||||
router.replace({ name: 'Login' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export { router };
|
||||
67
webui/src/store/bangumi.ts
Normal file
67
webui/src/store/bangumi.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
export const useBangumiStore = defineStore('bangumi', () => {
|
||||
const data = ref<BangumiRule[]>();
|
||||
|
||||
function getAll() {
|
||||
const { execute, onResult } = useApi(apiBangumi.getAll);
|
||||
|
||||
function sort(arr: BangumiRule[]) {
|
||||
return arr.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
|
||||
onResult((res) => {
|
||||
const enabled = sort(res.filter((e) => !e.deleted));
|
||||
const disabled = sort(res.filter((e) => e.deleted));
|
||||
|
||||
data.value = [...enabled, ...disabled];
|
||||
});
|
||||
|
||||
execute();
|
||||
}
|
||||
|
||||
function useUpdateRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.updateRule);
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
function useDisableRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.disableRule);
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
function useEnableRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.enableRule);
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
function useDeleteRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.deleteRule);
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
getAll,
|
||||
useUpdateRule,
|
||||
useDisableRule,
|
||||
useEnableRule,
|
||||
useDeleteRule,
|
||||
};
|
||||
});
|
||||
47
webui/src/store/config.ts
Normal file
47
webui/src/store/config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { type Config, initConfig } from '#/config';
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const config = ref<Config>(initConfig);
|
||||
|
||||
const { execute: getConfig, onResult: onGetConfigRusult } = useApi(
|
||||
apiConfig.getConfig
|
||||
);
|
||||
|
||||
onGetConfigRusult((res) => {
|
||||
config.value = res;
|
||||
});
|
||||
|
||||
const { execute: set, onResult: onSetRusult } = useApi(
|
||||
apiConfig.updateConfig,
|
||||
{
|
||||
failRule: (res) => !res,
|
||||
message: {
|
||||
success: 'Apply Success!',
|
||||
fail: 'Apply Failed!',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 保持 config 后重启,以应用最新配置
|
||||
*/
|
||||
onSetRusult(() => {
|
||||
const { restart } = useProgramStore();
|
||||
restart();
|
||||
});
|
||||
|
||||
const setConfig = () => {
|
||||
set(config.value);
|
||||
};
|
||||
|
||||
function getSettingGroup<Tkey extends keyof Config>(key: Tkey) {
|
||||
return computed<Config[Tkey]>(() => config.value[key]);
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
getConfig,
|
||||
setConfig,
|
||||
getSettingGroup,
|
||||
};
|
||||
});
|
||||
51
webui/src/store/log.ts
Normal file
51
webui/src/store/log.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export const useLogStore = defineStore('log', () => {
|
||||
const log = ref('');
|
||||
const { auth } = useAuth();
|
||||
const message = useMessage();
|
||||
|
||||
function get() {
|
||||
const { execute, onResult } = useApi(apiLog.getLog);
|
||||
|
||||
onResult((value) => {
|
||||
log.value = value;
|
||||
});
|
||||
|
||||
if (auth.value !== '') {
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
||||
const { execute: reset, onResult: onClearLogResult } = useApi(
|
||||
apiLog.clearLog
|
||||
);
|
||||
|
||||
onClearLogResult((res) => {
|
||||
if (res) {
|
||||
log.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(get, 3000, {
|
||||
immediate: false,
|
||||
immediateCallback: true,
|
||||
});
|
||||
|
||||
function copy() {
|
||||
const { copy: copyLog, isSupported } = useClipboard({ source: log });
|
||||
if (isSupported) {
|
||||
copyLog();
|
||||
message.success('Copy Success!');
|
||||
} else {
|
||||
message.error('Your browser does not support Clipboard API!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
log,
|
||||
get,
|
||||
reset,
|
||||
onUpdate,
|
||||
offUpdate,
|
||||
copy,
|
||||
};
|
||||
});
|
||||
13
webui/src/store/player.ts
Normal file
13
webui/src/store/player.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
type MediaPlayerType = 'jump' | 'iframe';
|
||||
|
||||
export const usePlayerStore = defineStore('player', () => {
|
||||
const types = ref<MediaPlayerType[]>(['jump', 'iframe']);
|
||||
const type = useLocalStorage<MediaPlayerType>('media-player-type', 'jump');
|
||||
const url = useLocalStorage<string>('media-player-url', '');
|
||||
|
||||
return {
|
||||
types,
|
||||
type,
|
||||
url,
|
||||
};
|
||||
});
|
||||
57
webui/src/store/program.ts
Normal file
57
webui/src/store/program.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export const useProgramStore = defineStore('program', () => {
|
||||
const { auth } = useAuth();
|
||||
const running = ref(false);
|
||||
|
||||
function getStatus() {
|
||||
const { execute, onResult } = useApi(apiProgram.status);
|
||||
|
||||
onResult((res) => {
|
||||
running.value = res;
|
||||
});
|
||||
|
||||
if (auth.value !== '') {
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
||||
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(
|
||||
getStatus,
|
||||
3000,
|
||||
{
|
||||
immediate: false,
|
||||
immediateCallback: true,
|
||||
}
|
||||
);
|
||||
|
||||
function opts(handle: string) {
|
||||
return {
|
||||
failRule: (res: boolean) => !res,
|
||||
message: {
|
||||
success: `${handle} Success!`,
|
||||
fail: `${handle} Failed!`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { execute: start } = useApi(apiProgram.start, opts('Start'));
|
||||
const { execute: pause } = useApi(apiProgram.stop, opts('Pause'));
|
||||
const { execute: shutdown } = useApi(apiProgram.shutdown, opts('Shutdown'));
|
||||
const { execute: restart } = useApi(apiProgram.restart, opts('Restart'));
|
||||
const { execute: resetRule } = useApi(
|
||||
apiBangumi.resetAll,
|
||||
opts('Reset Rule')
|
||||
);
|
||||
|
||||
return {
|
||||
running,
|
||||
getStatus,
|
||||
onUpdate,
|
||||
offUpdate,
|
||||
|
||||
start,
|
||||
pause,
|
||||
shutdown,
|
||||
restart,
|
||||
resetRule,
|
||||
};
|
||||
});
|
||||
50
webui/src/style/global.scss
Normal file
50
webui/src/style/global.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
$scrollbar-color: #372a87;
|
||||
|
||||
:root {
|
||||
--scrollbar-size: 6px;
|
||||
--scrollbar-color: transparent;
|
||||
--scrollbar-thumb-color: #{rgba($scrollbar-color, 0.5)};
|
||||
--scrollbar-thumb-hover-color: #{rgba($scrollbar-color, 1)};
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
/* 滚动槽--外层轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-color);
|
||||
}
|
||||
|
||||
/* 内层轨道(不包含滚动块部分) */
|
||||
/* 透明度设置为全透明,使得滚动条背景色为网页颜色 */
|
||||
::-webkit-scrollbar-track-piece {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: calc(var(--scrollbar-size) / 2);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条按钮 */
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 横向滚动条和纵向滚动条相交处尖角的颜色 */
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
13
webui/src/style/mixin.scss
Normal file
13
webui/src/style/mixin.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@mixin bg-mouse-event($normal, $hover, $active) {
|
||||
background: $normal;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition: none;
|
||||
background: $active;
|
||||
}
|
||||
}
|
||||
16
webui/src/style/transition.scss
Normal file
16
webui/src/style/transition.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
// transition
|
||||
|
||||
.fade {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// transition-group
|
||||
51
webui/src/utils/axios.ts
Normal file
51
webui/src/utils/axios.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import Axios from 'axios';
|
||||
import type { ApiError } from '#/api';
|
||||
|
||||
export const axios = Axios.create();
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
const { auth } = useAuth();
|
||||
|
||||
if (auth.value !== '' && config.headers) {
|
||||
config.headers.Authorization = auth.value;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(res) => {
|
||||
return res;
|
||||
},
|
||||
(err) => {
|
||||
const status = err.response.status as ApiError['status'];
|
||||
const detail = (err.response.data.detail ?? '') as ApiError['detail'];
|
||||
const msg = (err.response.data.msg ?? '') as ApiError['msg'];
|
||||
|
||||
const error = {
|
||||
status,
|
||||
detail,
|
||||
msg,
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
/** token 过期 */
|
||||
if (error.status === 401) {
|
||||
const { auth } = useAuth();
|
||||
auth.value = '';
|
||||
}
|
||||
|
||||
/** 执行失败 */
|
||||
if (error.status === 406) {
|
||||
message.error(error.msg);
|
||||
}
|
||||
|
||||
if (error.status === 500) {
|
||||
const msg = error.detail ? error.detail : 'Request Error!';
|
||||
message.error(msg);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
31
webui/tsconfig.json
Normal file
31
webui/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"#/*": ["types/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"types/*.ts",
|
||||
"types/dts/*.d.ts"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
webui/tsconfig.node.json
Normal file
9
webui/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
23
webui/types/api.ts
Normal file
23
webui/types/api.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type AuthError = 'Not authenticated';
|
||||
|
||||
export type LoginError = 'Password error' | 'User not found';
|
||||
|
||||
export type ApiErrorMessage = AuthError | LoginError;
|
||||
|
||||
/**
|
||||
* 401 Token 过期
|
||||
* 404 Not Found
|
||||
* 406 Not Acceptable
|
||||
* 500 Internal Server Error
|
||||
*/
|
||||
export type StatusCode = 401 | 404 | 406 | 500;
|
||||
|
||||
export type ApiError = {
|
||||
status: StatusCode;
|
||||
detail: ApiErrorMessage;
|
||||
msg: string;
|
||||
};
|
||||
|
||||
export interface ApiSuccess {
|
||||
msg: string;
|
||||
}
|
||||
18
webui/types/auth.ts
Normal file
18
webui/types/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface LoginSuccess {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expire: number;
|
||||
}
|
||||
|
||||
export interface Logout {
|
||||
message: 'logout success';
|
||||
}
|
||||
|
||||
export interface Update extends LoginSuccess {
|
||||
message: 'update success';
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
21
webui/types/bangumi.ts
Normal file
21
webui/types/bangumi.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface BangumiRule {
|
||||
added: boolean;
|
||||
deleted: boolean;
|
||||
dpi: string;
|
||||
eps_collect: boolean;
|
||||
filter: string[];
|
||||
group_name: string;
|
||||
id: number;
|
||||
official_title: string;
|
||||
offset: number;
|
||||
poster_link: string | null;
|
||||
rss_link: string[];
|
||||
rule_name: string;
|
||||
save_path: string;
|
||||
season: number;
|
||||
season_raw: string;
|
||||
source: string | null;
|
||||
subtitle: string;
|
||||
title_raw: string;
|
||||
year: string | null;
|
||||
}
|
||||
18
webui/types/components.ts
Normal file
18
webui/types/components.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface SelectItem {
|
||||
id: number;
|
||||
label?: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AbSettingProps {
|
||||
label: string;
|
||||
type: 'input' | 'switch' | 'select' | 'dynamic-tags';
|
||||
css?: string;
|
||||
prop?: any;
|
||||
bottomLine?: boolean;
|
||||
}
|
||||
|
||||
export type SettingItem<T> = AbSettingProps & {
|
||||
configKey: keyof T;
|
||||
};
|
||||
124
webui/types/config.ts
Normal file
124
webui/types/config.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { UnionToTuple } from '#/utils';
|
||||
|
||||
export interface Config {
|
||||
program: {
|
||||
rss_time: number;
|
||||
rename_time: number;
|
||||
webui_port: number;
|
||||
};
|
||||
downloader: {
|
||||
type: 'qbittorrent';
|
||||
host: string;
|
||||
username: string;
|
||||
password: string;
|
||||
path: string;
|
||||
ssl: boolean;
|
||||
};
|
||||
rss_parser: {
|
||||
enable: boolean;
|
||||
type: 'mikan';
|
||||
token: string;
|
||||
custom_url: string;
|
||||
filter: Array<string>;
|
||||
language: 'zh' | 'en' | 'jp';
|
||||
parser_type: 'tmdb' | 'mikan' | 'parser';
|
||||
};
|
||||
bangumi_manage: {
|
||||
enable: boolean;
|
||||
eps_complete: boolean;
|
||||
rename_method: 'normal' | 'pn' | 'advance' | 'none';
|
||||
group_tag: boolean;
|
||||
remove_bad_torrent: boolean;
|
||||
};
|
||||
log: {
|
||||
debug_enable: boolean;
|
||||
};
|
||||
proxy: {
|
||||
enable: boolean;
|
||||
type: 'http' | 'https' | 'socks5';
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
notification: {
|
||||
enable: boolean;
|
||||
type: 'telegram' | 'server-chan' | 'bark' | 'wecom';
|
||||
token: string;
|
||||
chat_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const initConfig: Config = {
|
||||
program: {
|
||||
rss_time: 0,
|
||||
rename_time: 0,
|
||||
webui_port: 0,
|
||||
},
|
||||
downloader: {
|
||||
type: 'qbittorrent',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
path: '',
|
||||
ssl: false,
|
||||
},
|
||||
rss_parser: {
|
||||
enable: true,
|
||||
type: 'mikan',
|
||||
token: '',
|
||||
custom_url: '',
|
||||
filter: [],
|
||||
language: 'zh',
|
||||
parser_type: 'parser',
|
||||
},
|
||||
bangumi_manage: {
|
||||
enable: true,
|
||||
eps_complete: true,
|
||||
rename_method: 'normal',
|
||||
group_tag: true,
|
||||
remove_bad_torrent: true,
|
||||
},
|
||||
log: {
|
||||
debug_enable: false,
|
||||
},
|
||||
proxy: {
|
||||
enable: false,
|
||||
type: 'http',
|
||||
host: '',
|
||||
port: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
notification: {
|
||||
enable: false,
|
||||
type: 'telegram',
|
||||
token: '',
|
||||
chat_id: '',
|
||||
},
|
||||
};
|
||||
|
||||
type getItem<T extends keyof Config> = Pick<Config, T>[T];
|
||||
|
||||
export type Program = getItem<'program'>;
|
||||
export type Downloader = getItem<'downloader'>;
|
||||
export type RssParser = getItem<'rss_parser'>;
|
||||
export type BangumiManage = getItem<'bangumi_manage'>;
|
||||
export type Log = getItem<'log'>;
|
||||
export type Proxy = getItem<'proxy'>;
|
||||
export type Notification = getItem<'notification'>;
|
||||
|
||||
/** 下载方式 */
|
||||
export type DownloaderType = UnionToTuple<Downloader['type']>;
|
||||
/** rss parser 源 */
|
||||
export type RssParserType = UnionToTuple<RssParser['type']>;
|
||||
/** rss parser 方法 */
|
||||
export type RssParserMethodType = UnionToTuple<RssParser['parser_type']>;
|
||||
/** rss parser 语言 */
|
||||
export type RssParserLang = UnionToTuple<RssParser['language']>;
|
||||
/** 重命名方式 */
|
||||
export type RenameMethod = UnionToTuple<BangumiManage['rename_method']>;
|
||||
/** 代理类型 */
|
||||
export type ProxyType = UnionToTuple<Proxy['type']>;
|
||||
/** 通知类型 */
|
||||
export type NotificationType = UnionToTuple<Notification['type']>;
|
||||
295
webui/types/dts/auto-imports.d.ts
vendored
Normal file
295
webui/types/dts/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const afterAll: typeof import('vitest')['afterAll']
|
||||
const afterEach: typeof import('vitest')['afterEach']
|
||||
const apiAuth: typeof import('../../src/api/auth')['apiAuth']
|
||||
const apiBangumi: typeof import('../../src/api/bangumi')['apiBangumi']
|
||||
const apiCheck: typeof import('../../src/api/check')['apiCheck']
|
||||
const apiConfig: typeof import('../../src/api/config')['apiConfig']
|
||||
const apiDownload: typeof import('../../src/api/download')['apiDownload']
|
||||
const apiLog: typeof import('../../src/api/log')['apiLog']
|
||||
const apiProgram: typeof import('../../src/api/program')['apiProgram']
|
||||
const assert: typeof import('vitest')['assert']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const axios: typeof import('../../src/utils/axios')['axios']
|
||||
const beforeAll: typeof import('vitest')['beforeAll']
|
||||
const beforeEach: typeof import('vitest')['beforeEach']
|
||||
const chai: typeof import('vitest')['chai']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineLoader: typeof import('vue-router/auto')['defineLoader']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const describe: typeof import('vitest')['describe']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const expect: typeof import('vitest')['expect']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const it: typeof import('vitest')['it']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const suite: typeof import('vitest')['suite']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const test: typeof import('vitest')['test']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useApi: typeof import('../../src/hooks/useApi')['useApi']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('../../src/hooks/useAuth')['useAuth']
|
||||
const useBangumiStore: typeof import('../../src/store/bangumi')['useBangumiStore']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfigStore: typeof import('../../src/store/config')['useConfigStore']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useLogStore: typeof import('../../src/store/log')['useLogStore']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMessage: typeof import('../../src/hooks/useMessage')['useMessage']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePlayerStore: typeof import('../../src/store/player')['usePlayerStore']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const useProgramStore: typeof import('../../src/store/program')['useProgramStore']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const vi: typeof import('vitest')['vi']
|
||||
const vitest: typeof import('vitest')['vitest']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
43
webui/types/dts/components.d.ts
vendored
Normal file
43
webui/types/dts/components.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AbAdd: typeof import('./../../src/components/basic/ab-add.vue')['default']
|
||||
AbAddBangumi: typeof import('./../../src/components/ab-add-bangumi.vue')['default']
|
||||
AbBangumiCard: typeof import('./../../src/components/ab-bangumi-card.vue')['default']
|
||||
AbButton: typeof import('./../../src/components/basic/ab-button.vue')['default']
|
||||
AbChangeAccount: typeof import('./../../src/components/ab-change-account.vue')['default']
|
||||
AbCheckbox: typeof import('./../../src/components/basic/ab-checkbox.vue')['default']
|
||||
AbContainer: typeof import('./../../src/components/ab-container.vue')['default']
|
||||
AbEditRule: typeof import('./../../src/components/ab-edit-rule.vue')['default']
|
||||
AbFoldPanel: typeof import('./../../src/components/ab-fold-panel.vue')['default']
|
||||
AbLabel: typeof import('./../../src/components/ab-label.vue')['default']
|
||||
AbPageTitle: typeof import('./../../src/components/basic/ab-page-title.vue')['default']
|
||||
AbPopup: typeof import('./../../src/components/ab-popup.vue')['default']
|
||||
AbRule: typeof import('./../../src/components/ab-rule.vue')['default']
|
||||
AbSearch: typeof import('./../../src/components/basic/ab-search.vue')['default']
|
||||
AbSelect: typeof import('./../../src/components/basic/ab-select.vue')['default']
|
||||
AbSetting: typeof import('./../../src/components/ab-setting.vue')['default']
|
||||
AbSidebar: typeof import('./../../src/components/layout/ab-sidebar.vue')['default']
|
||||
AbStatus: typeof import('./../../src/components/basic/ab-status.vue')['default']
|
||||
AbStatusBar: typeof import('./../../src/components/ab-status-bar.vue')['default']
|
||||
AbSwitch: typeof import('./../../src/components/basic/ab-switch.vue')['default']
|
||||
AbTopbar: typeof import('./../../src/components/layout/ab-topbar.vue')['default']
|
||||
ConfigDownload: typeof import('./../../src/components/setting/config-download.vue')['default']
|
||||
ConfigManage: typeof import('./../../src/components/setting/config-manage.vue')['default']
|
||||
ConfigNormal: typeof import('./../../src/components/setting/config-normal.vue')['default']
|
||||
ConfigNotification: typeof import('./../../src/components/setting/config-notification.vue')['default']
|
||||
ConfigParser: typeof import('./../../src/components/setting/config-parser.vue')['default']
|
||||
ConfigPlayer: typeof import('./../../src/components/setting/config-player.vue')['default']
|
||||
ConfigProxy: typeof import('./../../src/components/setting/config-proxy.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
148
webui/types/dts/router-type.d.ts
vendored
Normal file
148
webui/types/dts/router-type.d.ts
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
/// <reference types="unplugin-vue-router/client" />
|
||||
|
||||
import type {
|
||||
// type safe route locations
|
||||
RouteLocationTypedList,
|
||||
RouteLocationResolvedTypedList,
|
||||
RouteLocationNormalizedTypedList,
|
||||
RouteLocationNormalizedLoadedTypedList,
|
||||
RouteLocationAsString,
|
||||
RouteLocationAsRelativeTypedList,
|
||||
RouteLocationAsPathTypedList,
|
||||
|
||||
// helper types
|
||||
// route definitions
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
|
||||
// vue-router extensions
|
||||
_RouterTyped,
|
||||
RouterLinkTyped,
|
||||
RouterLinkPropsTyped,
|
||||
NavigationGuard,
|
||||
UseLinkFnTyped,
|
||||
|
||||
// data fetching
|
||||
_DataLoader,
|
||||
_DefineLoaderOptions,
|
||||
} from 'unplugin-vue-router/types'
|
||||
|
||||
declare module 'vue-router/auto/routes' {
|
||||
export interface RouteNamedMap {
|
||||
'Index': RouteRecordInfo<'Index', '/', Record<never, never>, Record<never, never>>,
|
||||
'Bangumi List': RouteRecordInfo<'Bangumi List', '/bangumi', Record<never, never>, Record<never, never>>,
|
||||
'Calendar': RouteRecordInfo<'Calendar', '/calendar', Record<never, never>, Record<never, never>>,
|
||||
'Config': RouteRecordInfo<'Config', '/config', Record<never, never>, Record<never, never>>,
|
||||
'Downloader': RouteRecordInfo<'Downloader', '/downloader', Record<never, never>, Record<never, never>>,
|
||||
'Log': RouteRecordInfo<'Log', '/log', Record<never, never>, Record<never, never>>,
|
||||
'Player': RouteRecordInfo<'Player', '/player', Record<never, never>, Record<never, never>>,
|
||||
'Login': RouteRecordInfo<'Login', '/login', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router/auto' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export type RouterTyped = _RouterTyped<RouteNamedMap>
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
|
||||
| RouteLocationAsString<RouteNamedMap>
|
||||
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
|
||||
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
|
||||
/**
|
||||
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
|
||||
|
||||
export function useRouter(): RouterTyped
|
||||
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
export const useLink: UseLinkFnTyped<RouteNamedMap>
|
||||
|
||||
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
|
||||
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
|
||||
|
||||
export const RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
|
||||
|
||||
// Experimental Data Fetching
|
||||
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
name: Name,
|
||||
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
loader: (route: RouteLocationNormalizedLoaded) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
|
||||
export {
|
||||
_definePage as definePage,
|
||||
_HasDataLoaderMeta as HasDataLoaderMeta,
|
||||
_setupDataFetchingGuard as setupDataFetchingGuard,
|
||||
_stopDataFetchingScope as stopDataFetchingScope,
|
||||
} from 'unplugin-vue-router/runtime'
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export interface TypesConfig {
|
||||
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
|
||||
beforeRouteLeave: NavigationGuard<RouteNamedMap>
|
||||
|
||||
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
|
||||
$router: _RouterTyped<RouteNamedMap>
|
||||
|
||||
RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
}
|
||||
}
|
||||
7
webui/types/dts/vite-env.d.ts
vendored
Normal file
7
webui/types/dts/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
39
webui/types/utils.ts
Normal file
39
webui/types/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 将联合类型转为对应的交叉函数类型
|
||||
* @template U 联合类型
|
||||
*/
|
||||
export type UnionToInterFunction<U> = (
|
||||
U extends any ? (k: () => U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 获取联合类型中的最后一个类型
|
||||
* @template U 联合类型
|
||||
*/
|
||||
export type GetUnionLast<U> = UnionToInterFunction<U> extends { (): infer A }
|
||||
? A
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 在元组类型中前置插入一个新的类型(元素);
|
||||
* @template Tuple 元组类型
|
||||
* @template E 新的类型
|
||||
*/
|
||||
export type Prepend<Tuple extends any[], E> = [E, ...Tuple];
|
||||
|
||||
/**
|
||||
* 联合类型转元组类型;
|
||||
* @template Union 联合类型
|
||||
* @template T 初始元组类型
|
||||
* @template Last 传入联合类型中的最后一个类型(元素),自动生成,内部使用
|
||||
*/
|
||||
export type UnionToTuple<
|
||||
Union,
|
||||
T extends any[] = [],
|
||||
Last = GetUnionLast<Union>
|
||||
> = {
|
||||
0: T;
|
||||
1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>;
|
||||
}[[Union] extends [never] ? 0 : 1];
|
||||
83
webui/unocss.config.ts
Normal file
83
webui/unocss.config.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
defineConfig,
|
||||
presetAttributify,
|
||||
presetIcons,
|
||||
presetUno,
|
||||
} from 'unocss';
|
||||
import presetRemToPx from '@unocss/preset-rem-to-px';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetRemToPx(),
|
||||
presetAttributify(),
|
||||
presetIcons({ cdn: 'https://esm.sh/' }),
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: '#493475',
|
||||
running: '#A3D491',
|
||||
stopped: '#DF7F91',
|
||||
page: '#F0F0F0',
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
[
|
||||
'bg-theme-row',
|
||||
{
|
||||
background: 'linear-gradient(90.5deg, #372A87 1.53%, #9B4D9C 96.48%)',
|
||||
},
|
||||
],
|
||||
[
|
||||
'bg-theme-col',
|
||||
{
|
||||
background: 'linear-gradient(180deg, #3C239F 0%, #793572 100%)',
|
||||
},
|
||||
],
|
||||
[
|
||||
'poster-shandow',
|
||||
{
|
||||
filter: 'drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.1))',
|
||||
},
|
||||
],
|
||||
[
|
||||
'poster-pen-active',
|
||||
{
|
||||
background: '#B4ABC6',
|
||||
'box-shadow': '2px 2px 4px rgba(0, 0, 0, 0.25)',
|
||||
},
|
||||
],
|
||||
],
|
||||
shortcuts: [
|
||||
[/^wh-(.*)$/, ([, t]) => `w-${t} h-${t}`],
|
||||
|
||||
[
|
||||
'layout-container',
|
||||
'wh-screen min-w-1024px min-h-768px p-16px space-y-12px flex flex-col bg-page',
|
||||
],
|
||||
[
|
||||
'layout-main',
|
||||
'flex space-x-20px overflow-hidden h-[calc(100vh_-_2_*_16px_-_60px_-_12px)]',
|
||||
],
|
||||
['layout-content', 'overflow-hidden h-full flex flex-col flex-1'],
|
||||
|
||||
['rel', 'relative'],
|
||||
['abs', 'absolute'],
|
||||
['fx-cer', 'flex items-center'],
|
||||
['f-cer', 'fx-cer justify-center'],
|
||||
['text-h1', 'text-24px'],
|
||||
['text-h2', 'text-20px'],
|
||||
['text-h3', 'text-16px'],
|
||||
['text-main', 'text-12px'],
|
||||
[
|
||||
'ab-input',
|
||||
'outline-none min-w-0 w-200px h-28px px-12px text-main text-right rounded-6px border-1 border-black shadow-inset hover:border-color-[#7A46AE]',
|
||||
],
|
||||
['input-error', 'border-color-[#CA0E0E]'],
|
||||
['is-btn', 'cursor-pointer select-none'],
|
||||
['is-disabled', 'cursor-not-allowed select-none'],
|
||||
['input-reset', 'bg-transparent min-w-0 flex-1 outline-none'],
|
||||
['btn-click', 'hover:scale-110 active:scale-100'],
|
||||
['line', 'w-full h-1px bg-[#DFE1EF]'],
|
||||
],
|
||||
});
|
||||
59
webui/vite.config.ts
Normal file
59
webui/vite.config.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { resolve } from 'node:path';
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import VueRouter from 'unplugin-vue-router/vite';
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [
|
||||
VueRouter({
|
||||
dts: 'types/dts/router-type.d.ts',
|
||||
}),
|
||||
vue({
|
||||
script: {
|
||||
defineModel: true,
|
||||
},
|
||||
}),
|
||||
UnoCSS(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vitest', 'pinia', '@vueuse/core', VueRouterAutoImports],
|
||||
dts: 'types/dts/auto-imports.d.ts',
|
||||
dirs: ['src/api', 'src/store', 'src/hooks', 'src/utils'],
|
||||
}),
|
||||
Components({
|
||||
dts: 'types/dts/components.d.ts',
|
||||
dirs: [
|
||||
'src/components',
|
||||
'src/components/basic',
|
||||
'src/components/layout',
|
||||
'src/components/setting',
|
||||
],
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@import "./src/style/mixin.scss";',
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'#': resolve(__dirname, 'types'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api/.*': 'http://127.0.0.1:7892',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user