mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-06 20:43:03 +08:00
es lint fix
This commit is contained in:
186
.eslintrc.js
186
.eslintrc.js
@@ -3,186 +3,19 @@ module.exports = {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
'@antfu/eslint-config-vue',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:sonarjs/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
|
||||
// 'plugin:unicorn/recommended',
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 13,
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'vue',
|
||||
'@typescript-eslint',
|
||||
'regex',
|
||||
],
|
||||
extends: ['@antfu/eslint-config-vue', 'plugin:sonarjs/recommended'],
|
||||
ignorePatterns: ['src/@iconify/*.js', 'node_modules', 'dist', '*.d.ts'],
|
||||
plugins: ['regex'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
|
||||
// indentation (Already present in TypeScript)
|
||||
'comma-spacing': ['error', { before: false, after: true }],
|
||||
'key-spacing': ['error', { afterColon: true }],
|
||||
'sonarjs/no-duplicate-string': 'warn',
|
||||
|
||||
'vue/first-attribute-linebreak': ['error', {
|
||||
singleline: 'beside',
|
||||
multiline: 'below',
|
||||
}],
|
||||
|
||||
'antfu/top-level-function': 'off',
|
||||
|
||||
// indentation (Already present in TypeScript)
|
||||
'indent': ['error', 2],
|
||||
|
||||
// Enforce trailing comma (Already present in TypeScript)
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
|
||||
// Enforce consistent spacing inside braces of object (Already present in TypeScript)
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
|
||||
// Disable max-len
|
||||
'max-len': 'off',
|
||||
|
||||
// we don't want it
|
||||
'semi': ['error', 'never'],
|
||||
|
||||
// add parens ony when required in arrow function
|
||||
'arrow-parens': ['error', 'as-needed'],
|
||||
|
||||
// add new line above comment
|
||||
'newline-before-return': 'error',
|
||||
|
||||
// add new line above comment
|
||||
'lines-around-comment': [
|
||||
'error',
|
||||
{
|
||||
beforeBlockComment: true,
|
||||
beforeLineComment: true,
|
||||
allowBlockStart: true,
|
||||
allowClassStart: true,
|
||||
allowObjectStart: true,
|
||||
allowArrayStart: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Ignore _ as unused variable
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_+$' }],
|
||||
|
||||
'array-element-newline': ['error', 'consistent'],
|
||||
'array-bracket-newline': ['error', 'consistent'],
|
||||
|
||||
'vue/multi-word-component-names': 'off',
|
||||
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{ blankLine: 'always', prev: 'expression', next: 'const' },
|
||||
{ blankLine: 'always', prev: 'const', next: 'expression' },
|
||||
{ blankLine: 'always', prev: 'multiline-const', next: '*' },
|
||||
{ blankLine: 'always', prev: '*', next: 'multiline-const' },
|
||||
],
|
||||
|
||||
// Plugin: eslint-plugin-import
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/newline-after-import': ['error', { count: 1 }],
|
||||
'no-restricted-imports': ['error', 'vuetify/components'],
|
||||
|
||||
// For omitting extension for ts files
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
|
||||
// ignore virtual files
|
||||
'import/no-unresolved': [2, {
|
||||
ignore: [
|
||||
'~pages$',
|
||||
'virtual:generated-layouts',
|
||||
|
||||
// Ignore vite's ?raw imports
|
||||
'.*\?raw',
|
||||
],
|
||||
}],
|
||||
|
||||
// Thanks: https://stackoverflow.com/a/63961972/10796681
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
|
||||
// Plugin: eslint-plugin-promise
|
||||
'promise/always-return': 'off',
|
||||
'promise/catch-or-return': 'off',
|
||||
|
||||
// ESLint plugin vue
|
||||
'vue/block-tag-newline': 'error',
|
||||
'vue/component-api-style': 'error',
|
||||
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase', {
|
||||
ignores: [
|
||||
'/^(click):[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/',
|
||||
],
|
||||
}],
|
||||
'vue/define-macros-order': 'error',
|
||||
'vue/html-comment-content-newline': 'error',
|
||||
'vue/html-comment-content-spacing': 'error',
|
||||
'vue/html-comment-indent': 'error',
|
||||
'vue/match-component-file-name': 'error',
|
||||
'vue/no-child-content': 'error',
|
||||
'vue/require-default-prop': 'off',
|
||||
|
||||
// NOTE this rule only supported in SFC, Users of the unplugin-vue-define-options should disable that rule: https://github.com/vuejs/eslint-plugin-vue/issues/1886
|
||||
// 'vue/no-duplicate-attr-inheritance': 'error',
|
||||
'vue/no-empty-component-block': 'error',
|
||||
'vue/no-multiple-objects-in-class': 'error',
|
||||
'vue/no-reserved-component-names': 'error',
|
||||
'vue/no-template-target-blank': 'error',
|
||||
'vue/no-useless-mustaches': 'error',
|
||||
'vue/no-useless-v-bind': 'error',
|
||||
'vue/padding-line-between-blocks': 'error',
|
||||
'vue/prefer-separate-static-class': 'error',
|
||||
'vue/prefer-true-attribute-shorthand': 'error',
|
||||
'vue/v-on-function-call': 'error',
|
||||
'vue/no-restricted-class': ['error', '/^(p|m)(l|r)-/'],
|
||||
'vue/valid-v-slot': ['error', {
|
||||
allowModifiers: true,
|
||||
}],
|
||||
|
||||
// -- Extension Rules
|
||||
'vue/no-irregular-whitespace': 'error',
|
||||
'vue/template-curly-spacing': 'error',
|
||||
|
||||
// -- Sonarlint
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-nested-template-literals': 'off',
|
||||
|
||||
// -- Unicorn
|
||||
// 'unicorn/filename-case': 'off',
|
||||
// 'unicorn/prevent-abbreviations': ['error', {
|
||||
// replacements: {
|
||||
// props: false,
|
||||
// },
|
||||
// }],
|
||||
|
||||
// Internal Rules
|
||||
'valid-appcardcode-code-prop': 'error',
|
||||
'valid-appcardcode-demo-sfc': 'error',
|
||||
|
||||
// https://github.com/gmullerb/eslint-plugin-regex
|
||||
'regex/invalid': [
|
||||
'error',
|
||||
@@ -213,23 +46,16 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
regex: 'useLayouts\\(',
|
||||
message: '`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.',
|
||||
message:
|
||||
'`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.',
|
||||
files: {
|
||||
inspect: '^(?!.*(@core|@layouts)).*',
|
||||
},
|
||||
},
|
||||
{
|
||||
regex: 'import axios from \'axios\'',
|
||||
replacement: 'import axios from \'@axios\'',
|
||||
message: 'Use axios instances created in \'src/plugin/axios.ts\' instead of unconfigured axios',
|
||||
files: {
|
||||
ignore: '^.*plugins/axios.ts.*',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Ignore files
|
||||
'\.eslintrc\.js',
|
||||
'.eslintrc.js',
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"endOfLine": "lf",
|
||||
"singleAttributePerLine": true
|
||||
}
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "preserve",
|
||||
"requirePragma": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"endOfLine": "lf",
|
||||
"singleAttributePerLine": false
|
||||
}
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"mgmcdermott.vscode-language-babel",
|
||||
"editorconfig.editorconfig",
|
||||
"xabikos.javascriptsnippets",
|
||||
@@ -12,4 +13,4 @@
|
||||
"cipchk.cssrem",
|
||||
"matijao.vue-nuxt-snippets"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
211
.vscode/settings.json
vendored
211
.vscode/settings.json
vendored
@@ -1,114 +1,107 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
"editor.formatOnSave": true,
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.eol": "\n",
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
},
|
||||
// SCSS
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint"
|
||||
},
|
||||
// JSON
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
// Vue
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// Extension: Volar
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
// Extension: Stylelint
|
||||
"stylelint.packageManager": "yarn",
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"scss",
|
||||
"vue"
|
||||
],
|
||||
// Extension: Spell Checker
|
||||
"cSpell.words": [
|
||||
"Composables",
|
||||
"Customizer",
|
||||
"flagpack",
|
||||
"Iconify",
|
||||
"psudo",
|
||||
"stylelint",
|
||||
"touchless",
|
||||
"triggerer",
|
||||
"vuetify"
|
||||
],
|
||||
// Extension: Comment Anchors
|
||||
"commentAnchors.tags.list": [
|
||||
{
|
||||
"tag": "ℹ️",
|
||||
"scope": "hidden",
|
||||
// This color is taken from "Better Comments" Extension (?)
|
||||
"highlightColor": "#3498DB",
|
||||
"styleComment": true,
|
||||
"isItalic": false
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"editor.autoClosingBrackets": "always"
|
||||
{
|
||||
"tag": "👉",
|
||||
"scope": "file",
|
||||
// This color is taken from "Better Comments" Extension (*)
|
||||
"highlightColor": "#98C379",
|
||||
"styleComment": true,
|
||||
"isItalic": false
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
},
|
||||
// SCSS
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint"
|
||||
},
|
||||
// JSON
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
// Vue
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Wscats.vue",
|
||||
},
|
||||
// Extension: Volar
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
// Extension: ESLint
|
||||
"eslint.options": {
|
||||
"rulePaths": [
|
||||
"eslint-internal-rules"
|
||||
]
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true,
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
"eslint.packageManager": "yarn",
|
||||
// Extension: Stylelint
|
||||
"stylelint.packageManager": "yarn",
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"scss",
|
||||
"vue"
|
||||
{
|
||||
"tag": "❗",
|
||||
"scope": "hidden",
|
||||
// This color is taken from "Better Comments" Extension (*)
|
||||
"highlightColor": "#FF2D00",
|
||||
"styleComment": true,
|
||||
"isItalic": false
|
||||
}
|
||||
],
|
||||
// Extension: fabiospampinato.vscode-highlight
|
||||
"highlight.regexFlags": "gi",
|
||||
"highlight.regexes": {
|
||||
// We flaged this for enforcing logical CSS properties
|
||||
"(100vh|translate|margin:|padding:|margin-left|margin-right|rotate|text-align|border-top|border-right|border-bottom|border-left|float|background-position|transform|width|height|top|left|bottom|right|float|clear|(p|m)(l|r)-|border-(start|end)-(start|end)-radius)": [
|
||||
{
|
||||
// "rangeBehavior": 1,
|
||||
"borderWidth": "1px",
|
||||
"borderColor": "tomato",
|
||||
"borderStyle": "solid"
|
||||
}
|
||||
],
|
||||
// Extension: Spell Checker
|
||||
"cSpell.words": [
|
||||
"Composables",
|
||||
"Customizer",
|
||||
"flagpack",
|
||||
"Iconify",
|
||||
"psudo",
|
||||
"stylelint",
|
||||
"touchless",
|
||||
"triggerer",
|
||||
"vuetify"
|
||||
],
|
||||
// Extension: Comment Anchors
|
||||
"commentAnchors.tags.list": [
|
||||
{
|
||||
"tag": "ℹ️",
|
||||
"scope": "hidden",
|
||||
// This color is taken from "Better Comments" Extension (?)
|
||||
"highlightColor": "#3498DB",
|
||||
"styleComment": true,
|
||||
"isItalic": false,
|
||||
},
|
||||
{
|
||||
"tag": "👉",
|
||||
"scope": "file",
|
||||
// This color is taken from "Better Comments" Extension (*)
|
||||
"highlightColor": "#98C379",
|
||||
"styleComment": true,
|
||||
"isItalic": false
|
||||
},
|
||||
{
|
||||
"tag": "❗",
|
||||
"scope": "hidden",
|
||||
// This color is taken from "Better Comments" Extension (*)
|
||||
"highlightColor": "#FF2D00",
|
||||
"styleComment": true,
|
||||
"isItalic": false,
|
||||
},
|
||||
],
|
||||
// Extension: fabiospampinato.vscode-highlight
|
||||
"highlight.regexFlags": "gi",
|
||||
"highlight.regexes": {
|
||||
// We flaged this for enforcing logical CSS properties
|
||||
"(100vh|translate|margin:|padding:|margin-left|margin-right|rotate|text-align|border-top|border-right|border-bottom|border-left|float|background-position|transform|width|height|top|left|bottom|right|float|clear|(p|m)(l|r)-|border-(start|end)-(start|end)-radius)": [
|
||||
{
|
||||
// "rangeBehavior": 1,
|
||||
"borderWidth": "1px",
|
||||
"borderColor": "tomato",
|
||||
"borderStyle": "solid"
|
||||
}
|
||||
],
|
||||
"(overflow-x:|overflow-y:)": [
|
||||
{
|
||||
// "rangeBehavior": 1,
|
||||
"borderWidth": "1px",
|
||||
"borderColor": "green",
|
||||
"borderStyle": "solid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": true
|
||||
"(overflow-x:|overflow-y:)": [
|
||||
{
|
||||
// "rangeBehavior": 1,
|
||||
"borderWidth": "1px",
|
||||
"borderColor": "green",
|
||||
"borderStyle": "solid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": true
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . -c .eslintrc.js --fix --rulesdir eslint-internal-rules/ --ext .ts,.js,.vue,.tsx,.jsx",
|
||||
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
|
||||
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
|
||||
"postinstall": "npm run build:icons"
|
||||
},
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(["click"]);
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
// 按钮点击
|
||||
const onClick = () => {
|
||||
emit("click");
|
||||
};
|
||||
function onClick() {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn class="absolute right-3 top-3" @click="onClick">
|
||||
<IconBtn
|
||||
class="absolute right-3 top-3"
|
||||
@click="onClick"
|
||||
>
|
||||
<VIcon icon="mdi-close" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
errorCode?: string;
|
||||
errorTitle?: string;
|
||||
errorDescription?: string;
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
errorDescription?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center mb-4">
|
||||
<!-- 👉 Title and subtitle -->
|
||||
<h1 v-if="props.errorCode" class="text-h1 font-weight-medium">
|
||||
<h1
|
||||
v-if="props.errorCode"
|
||||
class="text-h1 font-weight-medium"
|
||||
>
|
||||
{{ props.errorCode }}
|
||||
</h1>
|
||||
<h5 v-if="props.errorTitle" class="text-h5 font-weight-medium mb-3">
|
||||
<h5
|
||||
v-if="props.errorTitle"
|
||||
class="text-h5 font-weight-medium mb-3"
|
||||
>
|
||||
{{ props.errorTitle }}
|
||||
</h5>
|
||||
<p v-if="props.errorDescription">
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<div class="absolute top-0 right-0 flex items-center justify-between p-2">
|
||||
<div class="pointer-events-none z-40 flex items-center">
|
||||
<div
|
||||
class="relative inline-flex whitespace-nowrap rounded-full border-gray-700 font-semibold leading-5 ring-gray-700"
|
||||
>
|
||||
<div
|
||||
class="rounded-full bg-opacity-80 shadow-md w-5 border p-0 bg-green-500 border-green-400 ring-green-400 text-green-100"
|
||||
>
|
||||
<div class="relative inline-flex whitespace-nowrap rounded-full border-gray-700 font-semibold leading-5 ring-gray-700">
|
||||
<div class="rounded-full bg-opacity-80 shadow-md w-5 border p-0 bg-green-500 border-green-400 ring-green-400 text-green-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
@@ -17,7 +13,7 @@
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
menuList?: unknown[];
|
||||
itemProps?: boolean;
|
||||
menuList?: unknown[]
|
||||
itemProps?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
|
||||
<VMenu v-if="props.menuList" activator="parent" close-on-content-click>
|
||||
<VList :items="props.menuList" :item-props="props.itemProps" />
|
||||
<VMenu
|
||||
v-if="props.menuList"
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList
|
||||
:items="props.menuList"
|
||||
:item-props="props.itemProps"
|
||||
/>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeSwitcherTheme } from "@layouts/types";
|
||||
import { ref, watch } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { ref, watch } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
|
||||
const props = defineProps<{
|
||||
themes: ThemeSwitcherTheme[];
|
||||
}>();
|
||||
themes: ThemeSwitcherTheme[]
|
||||
}>()
|
||||
|
||||
const { name: themeName, global: globalTheme } = useTheme();
|
||||
const { name: themeName, global: globalTheme } = useTheme()
|
||||
|
||||
const savedTheme = ref(localStorage.getItem("theme") ?? themeName);
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||
|
||||
const {
|
||||
state: currentThemeName,
|
||||
next: getNextThemeName,
|
||||
index: currentThemeIndex,
|
||||
} = useCycleList(
|
||||
props.themes.map((t) => t.name),
|
||||
{ initialValue: savedTheme.value }
|
||||
);
|
||||
props.themes.map(t => t.name),
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
const changeTheme = () => {
|
||||
const nextTheme = getNextThemeName();
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
|
||||
globalTheme.name.value = nextTheme;
|
||||
savedTheme.value = nextTheme;
|
||||
localStorage.setItem("theme", nextTheme);
|
||||
};
|
||||
globalTheme.name.value = nextTheme
|
||||
savedTheme.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
(val) => {
|
||||
currentThemeName.value = val;
|
||||
}
|
||||
);
|
||||
currentThemeName.value = val
|
||||
},
|
||||
)
|
||||
|
||||
// Apply saved theme on page load
|
||||
onMounted(() => {
|
||||
globalTheme.name.value = savedTheme.value;
|
||||
});
|
||||
globalTheme.name.value = savedTheme.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ThemeInstance } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
|
||||
// 👉 Colors variables
|
||||
const colorVariables = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
function colorVariables(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})`
|
||||
const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})`
|
||||
const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})`
|
||||
@@ -11,7 +11,7 @@ const colorVariables = (themeColors: ThemeInstance['themes']['value']['colors'])
|
||||
return { themeSecondaryTextColor, themeDisabledTextColor, themeBorderColor, themePrimaryTextColor }
|
||||
}
|
||||
|
||||
export const getScatterChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getScatterChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const scatterColors = {
|
||||
series1: '#ff9f43',
|
||||
series2: '#7367f0',
|
||||
@@ -67,7 +67,7 @@ export const getScatterChartConfig = (themeColors: ThemeInstance['themes']['valu
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getLineChartSimpleConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getLineChartSimpleConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
@@ -94,7 +94,7 @@ export const getLineChartSimpleConfig = (themeColors: ThemeInstance['themes']['v
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
custom(data: any) {
|
||||
return `<div class='bar-chart pa-2'>
|
||||
<span>${data.series[data.seriesIndex][data.dataPointIndex]}%</span>
|
||||
@@ -137,7 +137,7 @@ export const getLineChartSimpleConfig = (themeColors: ThemeInstance['themes']['v
|
||||
}
|
||||
}
|
||||
|
||||
export const getBarChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getBarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
@@ -180,7 +180,7 @@ export const getBarChartConfig = (themeColors: ThemeInstance['themes']['value'][
|
||||
}
|
||||
}
|
||||
|
||||
export const getCandlestickChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getCandlestickChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const candlestickColors = {
|
||||
series1: '#28c76f',
|
||||
series2: '#ea5455',
|
||||
@@ -231,7 +231,7 @@ export const getCandlestickChartConfig = (themeColors: ThemeInstance['themes']['
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getRadialBarChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getRadialBarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const radialBarColors = {
|
||||
series1: '#fdd835',
|
||||
series2: '#32baff',
|
||||
@@ -282,7 +282,7 @@ export const getRadialBarChartConfig = (themeColors: ThemeInstance['themes']['va
|
||||
fontSize: '1.125rem',
|
||||
|
||||
color: themePrimaryTextColor,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
formatter(w: { globals: { seriesTotals: any[]; series: string | any[] } }) {
|
||||
const totalValue
|
||||
= w.globals.seriesTotals.reduce((a: number, b: number) => {
|
||||
@@ -307,7 +307,7 @@ export const getRadialBarChartConfig = (themeColors: ThemeInstance['themes']['va
|
||||
}
|
||||
}
|
||||
|
||||
export const getDonutChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getDonutChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const donutColors = {
|
||||
series1: '#fdd835',
|
||||
series2: '#00d4bd',
|
||||
@@ -401,7 +401,7 @@ export const getDonutChartConfig = (themeColors: ThemeInstance['themes']['value'
|
||||
}
|
||||
}
|
||||
|
||||
export const getAreaChartSplineConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getAreaChartSplineConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const areaColors = {
|
||||
series3: '#e0cffe',
|
||||
series2: '#b992fe',
|
||||
@@ -482,7 +482,7 @@ export const getAreaChartSplineConfig = (themeColors: ThemeInstance['themes']['v
|
||||
}
|
||||
}
|
||||
|
||||
export const getColumnChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getColumnChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const columnColors = {
|
||||
series1: '#826af9',
|
||||
series2: '#d2b0ff',
|
||||
@@ -568,7 +568,7 @@ export const getColumnChartConfig = (themeColors: ThemeInstance['themes']['value
|
||||
}
|
||||
}
|
||||
|
||||
export const getHeatMapChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getHeatMapChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
@@ -627,7 +627,7 @@ export const getHeatMapChartConfig = (themeColors: ThemeInstance['themes']['valu
|
||||
}
|
||||
}
|
||||
|
||||
export const getRadarChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
|
||||
export function getRadarChartConfig(themeColors: ThemeInstance['themes']['value']['colors']) {
|
||||
const radarColors = {
|
||||
series1: '#9b88fa',
|
||||
series2: '#ffa1a1',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isToday } from './index'
|
||||
|
||||
export const avatarText = (value: string) => {
|
||||
export function avatarText(value: string) {
|
||||
if (!value)
|
||||
return ''
|
||||
const nameArray = value.split(' ')
|
||||
@@ -9,7 +9,7 @@ export const avatarText = (value: string) => {
|
||||
}
|
||||
|
||||
// TODO: Try to implement this: https://twitter.com/fireship_dev/status/1565424801216311297
|
||||
export const kFormatter = (num: number) => {
|
||||
export function kFormatter(num: number) {
|
||||
const regex = /\B(?=(\d{3})+(?!\d))/g
|
||||
|
||||
return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
@@ -22,7 +22,7 @@ export const kFormatter = (num: number) => {
|
||||
* @param {String} value date to format
|
||||
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
|
||||
*/
|
||||
export const formatDate = (value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) => {
|
||||
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
|
||||
if (!value)
|
||||
return value
|
||||
|
||||
@@ -35,7 +35,7 @@ export const formatDate = (value: string, formatting: Intl.DateTimeFormatOptions
|
||||
* @param {String} value date to format
|
||||
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current
|
||||
*/
|
||||
export const formatDateToMonthShort = (value: string, toTimeForCurrentDay = true) => {
|
||||
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
|
||||
const date = new Date(value)
|
||||
let formatting: Record<string, string> = { month: 'short', day: 'numeric' }
|
||||
|
||||
@@ -51,50 +51,47 @@ export const prefixWithPlus = (value: number) => value > 0 ? `+${value}` : value
|
||||
export const formatSeason = (value: string) => value ? `S${value.padStart(2, '0')}` : ''
|
||||
|
||||
// 格式化为xx[TGMK]B
|
||||
export const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 0) {
|
||||
throw new Error("字节数不能为负数。");
|
||||
}
|
||||
export function formatFileSize(bytes: number) {
|
||||
if (bytes < 0)
|
||||
throw new Error('字节数不能为负数。')
|
||||
|
||||
const units = ["B", "K", "M", "G", "T"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
const units = ['B', 'K', 'M', 'G', 'T']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 将时间秒格式化为时分秒
|
||||
export const formatSeconds = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
export function formatSeconds(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
let formattedTime = '';
|
||||
let formattedTime = ''
|
||||
|
||||
if (hours > 0) {
|
||||
formattedTime += `${hours}小时`;
|
||||
}
|
||||
if (hours > 0)
|
||||
formattedTime += `${hours}小时`
|
||||
|
||||
if (minutes > 0) {
|
||||
formattedTime += `${minutes}分`;
|
||||
}
|
||||
if (minutes > 0)
|
||||
formattedTime += `${minutes}分`
|
||||
|
||||
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0) {
|
||||
formattedTime += `${remainingSeconds}秒`;
|
||||
}
|
||||
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0)
|
||||
formattedTime += `${remainingSeconds}秒`
|
||||
|
||||
return formattedTime;
|
||||
return formattedTime
|
||||
}
|
||||
|
||||
|
||||
// YYYY-MM-DD 转化为Date
|
||||
export const parseDate = (dateString: string): Date => {
|
||||
if (!dateString) return new Date();
|
||||
const [year, month, day] = dateString.split('-').map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
export function parseDate(dateString: string): Date {
|
||||
if (!dateString)
|
||||
return new Date()
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 👉 IsEmpty
|
||||
export const isEmpty = (value: unknown): boolean => {
|
||||
export function isEmpty(value: unknown): boolean {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return true
|
||||
|
||||
@@ -7,20 +7,21 @@ export const isEmpty = (value: unknown): boolean => {
|
||||
}
|
||||
|
||||
// 👉 IsNullOrUndefined
|
||||
export const isNullOrUndefined = (value: unknown): value is undefined | null => {
|
||||
export function isNullOrUndefined(value: unknown): value is undefined | null {
|
||||
return value === null || value === undefined
|
||||
}
|
||||
|
||||
// 👉 IsEmptyArray
|
||||
export const isEmptyArray = (arr: unknown): boolean => {
|
||||
export function isEmptyArray(arr: unknown): boolean {
|
||||
return Array.isArray(arr) && arr.length === 0
|
||||
}
|
||||
|
||||
// 👉 IsObject
|
||||
export const isObject = (obj: unknown): obj is Record<string, unknown> =>
|
||||
obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
|
||||
export function isObject(obj: unknown): obj is Record<string, unknown> {
|
||||
return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
|
||||
}
|
||||
|
||||
export const isToday = (date: Date) => {
|
||||
export function isToday(date: Date) {
|
||||
const today = new Date()
|
||||
|
||||
return (
|
||||
@@ -33,38 +34,42 @@ export const isToday = (date: Date) => {
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export const calculateTimeDifference = (inputTime: string): string => {
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
if (!inputTime) {
|
||||
return '';
|
||||
}
|
||||
const inputDate = new Date(inputTime)
|
||||
const currentDate = new Date()
|
||||
|
||||
const inputDate = new Date(inputTime);
|
||||
const currentDate = new Date();
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime();
|
||||
const secondsDifference = Math.floor(timeDifference / 1000);
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
if (secondsDifference < 60) {
|
||||
return `${secondsDifference}秒`;
|
||||
} else if (secondsDifference < 3600) {
|
||||
const minutes = Math.floor(secondsDifference / 60);
|
||||
return `${minutes}分钟`;
|
||||
} else if (secondsDifference < 86400) {
|
||||
const hours = Math.floor(secondsDifference / 3600);
|
||||
return `${hours}小时`;
|
||||
} else {
|
||||
const days = Math.floor(secondsDifference / 86400);
|
||||
return `${days}天`;
|
||||
return `${secondsDifference}秒`
|
||||
}
|
||||
else if (secondsDifference < 3600) {
|
||||
const minutes = Math.floor(secondsDifference / 60)
|
||||
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
else if (secondsDifference < 86400) {
|
||||
const hours = Math.floor(secondsDifference / 3600)
|
||||
|
||||
return `${hours}小时`
|
||||
}
|
||||
else {
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
|
||||
return `${days}天`
|
||||
}
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export const isContained = (subArray: any[], mainArray: any[]): boolean => {
|
||||
return subArray.every(element => mainArray.includes(element));
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
}
|
||||
|
||||
// 判断两个数组是否存在交集
|
||||
export const isIntersected = (array1: any[], array2: any[]): boolean => {
|
||||
return array1.some(element => array2.includes(element));
|
||||
export function isIntersected(array1: any[], array2: any[]): boolean {
|
||||
return array1.some(element => array2.includes(element))
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ const target = join(__dirname, 'icons-bundle.js');
|
||||
|
||||
// Export to JSON
|
||||
const content = iconSet.export()
|
||||
|
||||
bundle += `addCollection(${JSON.stringify(content)});\n`
|
||||
}
|
||||
}
|
||||
@@ -258,7 +259,7 @@ const target = join(__dirname, 'icons-bundle.js');
|
||||
await fs.writeFile(target, bundle, 'utf8')
|
||||
|
||||
console.log(`Saved ${target} (${bundle.length} bytes)`)
|
||||
})().catch(err => {
|
||||
})().catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
@@ -274,7 +275,8 @@ function removeMetaData(iconSet: IconifyJSON) {
|
||||
'prefixes',
|
||||
'suffixes',
|
||||
]
|
||||
props.forEach(prop => {
|
||||
|
||||
props.forEach((prop) => {
|
||||
delete iconSet[prop]
|
||||
})
|
||||
}
|
||||
@@ -284,12 +286,14 @@ function removeMetaData(iconSet: IconifyJSON) {
|
||||
*/
|
||||
function organizeIconsList(icons: string[]): Record<string, string[]> {
|
||||
const sorted: Record<string, string[]> = Object.create(null)
|
||||
icons.forEach(icon => {
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const item = stringToIcon(icon)
|
||||
if (!item)
|
||||
return
|
||||
|
||||
const prefix = item.prefix
|
||||
|
||||
const prefixList = sorted[prefix]
|
||||
? sorted[prefix]
|
||||
: (sorted[prefix] = [])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as VerticalNavLayout } from './components/VerticalNavLayout.vue'
|
||||
export { default as VerticalNavLink } from './components/VerticalNavLink.vue'
|
||||
export { default as VerticalNavSectionTitle } from './components/VerticalNavSectionTitle.vue'
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
props.toggleIsOverlayNavActive(false)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const isVerticalNavScrolled = ref(false)
|
||||
const updateIsVerticalNavScrolled = (val: boolean) => isVerticalNavScrolled.value = val
|
||||
const updateIsVerticalNavScrolled = (val: boolean) => (isVerticalNavScrolled.value = val)
|
||||
|
||||
const handleNavScroll = (evt: Event) => {
|
||||
function handleNavScroll(evt: Event) {
|
||||
isVerticalNavScrolled.value = (evt.target as HTMLElement).scrollTop > 0
|
||||
}
|
||||
</script>
|
||||
@@ -54,14 +55,8 @@ const handleNavScroll = (evt: Event) => {
|
||||
<!-- 👉 Header -->
|
||||
<div class="nav-header">
|
||||
<slot name="nav-header">
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="app-logo d-flex align-center gap-x-3 app-title-wrapper"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
v-html="logo"
|
||||
/>
|
||||
<RouterLink to="/" class="app-logo d-flex align-center gap-x-3 app-title-wrapper">
|
||||
<div class="d-flex" v-html="logo" />
|
||||
|
||||
<h1 class="font-weight-medium leading-normal text-xl">
|
||||
MoviePilot
|
||||
@@ -72,10 +67,7 @@ const handleNavScroll = (evt: Event) => {
|
||||
<slot name="before-nav-items">
|
||||
<div class="vertical-nav-items-shadow" />
|
||||
</slot>
|
||||
<slot
|
||||
name="nav-items"
|
||||
:update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled"
|
||||
>
|
||||
<slot name="nav-items" :update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled">
|
||||
<PerfectScrollbar
|
||||
tag="ul"
|
||||
class="nav-items"
|
||||
@@ -91,8 +83,8 @@ const handleNavScroll = (evt: Event) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins";
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
// 👉 Vertical Nav
|
||||
.layout-vertical-nav {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import VerticalNav from "@layouts/components/VerticalNav.vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
const isOverlayNavActive = ref(false);
|
||||
const isLayoutOverlayVisible = ref(false);
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive);
|
||||
const isOverlayNavActive = ref(false)
|
||||
const isLayoutOverlayVisible = ref(false)
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
|
||||
|
||||
const route = useRoute();
|
||||
const { mdAndDown } = useDisplay();
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
// ℹ️ This is alternative to below two commented watcher
|
||||
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible);
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
const scrollDistance = ref(window.scrollY);
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("scroll", () => {
|
||||
scrollDistance.value = window.scrollY;
|
||||
});
|
||||
});
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
@@ -29,63 +29,63 @@ export default defineComponent({
|
||||
VerticalNav,
|
||||
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
|
||||
{
|
||||
"nav-header": () => slots["vertical-nav-header"]?.(),
|
||||
"before-nav-items": () => slots["before-vertical-nav-items"]?.(),
|
||||
default: () => slots["vertical-nav-content"]?.(),
|
||||
"after-nav-items": () => slots["after-vertical-nav-items"]?.(),
|
||||
}
|
||||
);
|
||||
'nav-header': () => slots['vertical-nav-header']?.(),
|
||||
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
|
||||
'default': () => slots['vertical-nav-content']?.(),
|
||||
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
|
||||
},
|
||||
)
|
||||
|
||||
// 👉 Navbar
|
||||
const navbar = h("header", { class: ["layout-navbar navbar-blur"] }, [
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h(
|
||||
"div",
|
||||
{ class: "navbar-content-container" },
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
])
|
||||
|
||||
const main = h(
|
||||
"main",
|
||||
{ class: "layout-page-content" },
|
||||
h("div", { class: "page-content-container" }, slots.default?.())
|
||||
);
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h('div', { class: 'page-content-container' }, slots.default?.()),
|
||||
)
|
||||
|
||||
// 👉 Footer
|
||||
const footer = h("footer", { class: "layout-footer" }, [
|
||||
h("div", { class: "footer-content-container" }, slots.footer?.()),
|
||||
]);
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
h('div', { class: 'footer-content-container' }, slots.footer?.()),
|
||||
])
|
||||
|
||||
// 👉 Overlay
|
||||
const layoutOverlay = h("div", {
|
||||
class: ["layout-overlay", { visible: isLayoutOverlayVisible.value }],
|
||||
const layoutOverlay = h('div', {
|
||||
class: ['layout-overlay', { visible: isLayoutOverlayVisible.value }],
|
||||
onClick: () => {
|
||||
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value;
|
||||
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return h(
|
||||
"div",
|
||||
'div',
|
||||
{
|
||||
class: [
|
||||
"layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid",
|
||||
"layout-navbar-fixed",
|
||||
mdAndDown.value && "layout-overlay-nav",
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && "window-scrolled",
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[
|
||||
verticalNav,
|
||||
h("div", { class: "layout-content-wrapper" }, [navbar, main, footer]),
|
||||
h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]),
|
||||
layoutOverlay,
|
||||
]
|
||||
);
|
||||
};
|
||||
],
|
||||
)
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -24,7 +24,7 @@ export const getComputedNavLinkToProp = computed(() => (link: NavLink) => {
|
||||
* @param hex
|
||||
*/
|
||||
|
||||
export const hexToRgb = (hex: string) => {
|
||||
export function hexToRgb(hex: string) {
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ValidationRule } from 'vuetify/types/services/validation'
|
||||
import type { ValidationRule } from 'vuetify/types/services/validation'
|
||||
|
||||
// 必输校验
|
||||
export const requiredValidator: ValidationRule = (value: any) => !!value || '此项为必填项'
|
||||
|
||||
74
src/App.vue
74
src/App.vue
@@ -1,67 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import avatar1 from "@images/avatars/avatar-1.png";
|
||||
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import api from "./api";
|
||||
import { User } from "./api/types";
|
||||
import store from "./store";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from './api'
|
||||
import type { User } from './api/types'
|
||||
import store from './store'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
|
||||
// 路由
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// SSE持续接收消息
|
||||
const startSSEMessager = () => {
|
||||
const token = store.state.auth.token;
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`
|
||||
);
|
||||
eventSource.addEventListener("message", (event) => {
|
||||
const message = event.data;
|
||||
if (message) {
|
||||
$toast.info(message);
|
||||
}
|
||||
});
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message)
|
||||
$toast.info(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close();
|
||||
});
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 当前用户信息
|
||||
const accountInfo = ref<User>({
|
||||
id: 0,
|
||||
name: "",
|
||||
password: "",
|
||||
email: "",
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
avatar: avatar1,
|
||||
});
|
||||
})
|
||||
|
||||
// 调用API,加载当前用户数据
|
||||
const loadAccountInfo = async () => {
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const user: User = await api.get(`user/current`);
|
||||
accountInfo.value = user;
|
||||
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const user: User = await api.get('user/current')
|
||||
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar)
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onMounted(() => {
|
||||
loadAccountInfo();
|
||||
startSSEMessager();
|
||||
});
|
||||
loadAccountInfo()
|
||||
startSSEMessager()
|
||||
})
|
||||
|
||||
// 提供给所有元素复用
|
||||
provide("accountInfo", accountInfo);
|
||||
provide('accountInfo', accountInfo)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
@@ -8,7 +8,7 @@ const api = axios.create({
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
api.interceptors.request.use(config => {
|
||||
api.interceptors.request.use((config) => {
|
||||
// 在请求头中添加token
|
||||
const token = store.state.auth.token
|
||||
if (token)
|
||||
@@ -18,9 +18,9 @@ api.interceptors.request.use(config => {
|
||||
})
|
||||
|
||||
// 添加响应拦截器
|
||||
api.interceptors.response.use(response => {
|
||||
api.interceptors.response.use((response) => {
|
||||
return response.data
|
||||
}, error => {
|
||||
}, (error) => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(error)
|
||||
@@ -28,6 +28,7 @@ api.interceptors.response.use(response => {
|
||||
else if (error.response.status === 403) {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
export function configureNProgress() {
|
||||
NProgress.configure({
|
||||
showSpinner: false
|
||||
});
|
||||
showSpinner: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function startNProgress() {
|
||||
NProgress.start();
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
export function doneNProgress() {
|
||||
NProgress.done();
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
644
src/api/types.ts
644
src/api/types.ts
@@ -1,493 +1,693 @@
|
||||
// 订阅
|
||||
export interface Subscribe {
|
||||
id: number;
|
||||
id: number
|
||||
|
||||
// 订阅名称
|
||||
name: string;
|
||||
name: string
|
||||
|
||||
// 订阅年份
|
||||
year: string;
|
||||
year: string
|
||||
|
||||
// 订阅类型 电影/电视剧
|
||||
type: string;
|
||||
type: string
|
||||
|
||||
// 搜索关键字
|
||||
keyword?: string;
|
||||
tmdbid: number;
|
||||
doubanid?: string;
|
||||
keyword?: string
|
||||
tmdbid: number
|
||||
doubanid?: string
|
||||
|
||||
// 季号
|
||||
season?: number;
|
||||
season?: number
|
||||
|
||||
// 海报
|
||||
poster?: string;
|
||||
poster?: string
|
||||
|
||||
// 背景图
|
||||
backdrop?: string;
|
||||
backdrop?: string
|
||||
|
||||
// 评分
|
||||
vote?: number;
|
||||
vote?: number
|
||||
|
||||
// 描述
|
||||
description?: string;
|
||||
description?: string
|
||||
|
||||
// 过滤规则
|
||||
filter?: string;
|
||||
filter?: string
|
||||
|
||||
// 包含
|
||||
include?: string;
|
||||
include?: string
|
||||
|
||||
// 排除
|
||||
exclude?: string;
|
||||
exclude?: string
|
||||
|
||||
// 总集数
|
||||
total_episode?: number;
|
||||
total_episode?: number
|
||||
|
||||
// 开始集数
|
||||
start_episode?: number;
|
||||
start_episode?: number
|
||||
|
||||
// 缺失集数
|
||||
lack_episode?: number;
|
||||
lack_episode?: number
|
||||
|
||||
// 附加信息
|
||||
note?: string;
|
||||
note?: string
|
||||
|
||||
// 状态:N-新建, R-订阅中
|
||||
state: string;
|
||||
state: string
|
||||
|
||||
// 最后更新时间
|
||||
last_update: string;
|
||||
last_update: string
|
||||
|
||||
// 订阅用户
|
||||
username: string;
|
||||
username: string
|
||||
|
||||
// 订阅站点
|
||||
sites: number[],
|
||||
sites: number[]
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
export interface TransferHistory {
|
||||
|
||||
// ID
|
||||
id: number;
|
||||
id: number
|
||||
|
||||
// 源目录
|
||||
src?: string;
|
||||
src?: string
|
||||
|
||||
// 目的目录
|
||||
dest?: string;
|
||||
dest?: string
|
||||
|
||||
// 转移模式link/copy/move/softlink
|
||||
mode?: string;
|
||||
mode?: string
|
||||
|
||||
// 类型:电影、电视剧
|
||||
type?: string;
|
||||
type?: string
|
||||
|
||||
// 二级分类
|
||||
category?: string;
|
||||
category?: string
|
||||
|
||||
// 标题
|
||||
title?: string;
|
||||
title?: string
|
||||
|
||||
// 年份
|
||||
year?: string;
|
||||
year?: string
|
||||
|
||||
// TMDBID
|
||||
tmdbid?: number;
|
||||
tmdbid?: number
|
||||
|
||||
// IMDBID
|
||||
imdbid?: string;
|
||||
imdbid?: string
|
||||
|
||||
// TVDBID
|
||||
tvdbid?: number;
|
||||
tvdbid?: number
|
||||
|
||||
// 豆瓣ID
|
||||
doubanid?: string;
|
||||
doubanid?: string
|
||||
|
||||
// 季Sxx
|
||||
seasons?: string;
|
||||
seasons?: string
|
||||
|
||||
// 集Exx
|
||||
episodes?: string;
|
||||
episodes?: string
|
||||
|
||||
// 海报
|
||||
image?: string;
|
||||
image?: string
|
||||
|
||||
// 下载器Hash
|
||||
download_hash?: string;
|
||||
download_hash?: string
|
||||
|
||||
// 状态 1-成功,0-失败
|
||||
status: boolean;
|
||||
status: boolean
|
||||
|
||||
// 失败原因
|
||||
errmsg?: string;
|
||||
errmsg?: string
|
||||
|
||||
// 日期
|
||||
date?: string;
|
||||
date?: string
|
||||
}
|
||||
|
||||
// 媒体信息
|
||||
export interface MediaInfo {
|
||||
|
||||
// 类型 电影、电视剧
|
||||
type?: string;
|
||||
type?: string
|
||||
|
||||
// 媒体标题
|
||||
title?: string;
|
||||
title?: string
|
||||
|
||||
// 年份
|
||||
year?: string;
|
||||
year?: string
|
||||
|
||||
// 标题(年)
|
||||
title_year?: string;
|
||||
title_year?: string
|
||||
|
||||
// 季号
|
||||
season?: number;
|
||||
season?: number
|
||||
|
||||
// TMDB ID
|
||||
tmdb_id?: number;
|
||||
tmdb_id?: number
|
||||
|
||||
// IMDB ID
|
||||
imdb_id?: string;
|
||||
imdb_id?: string
|
||||
|
||||
// TVDB ID
|
||||
tvdb_id?: string;
|
||||
tvdb_id?: string
|
||||
|
||||
// 豆瓣ID
|
||||
douban_id?: string;
|
||||
douban_id?: string
|
||||
|
||||
// 媒体原语种
|
||||
original_language?: string;
|
||||
original_language?: string
|
||||
|
||||
// 媒体原发行标题
|
||||
original_title?: string;
|
||||
original_title?: string
|
||||
|
||||
// 媒体发行日期
|
||||
release_date?: string;
|
||||
release_date?: string
|
||||
|
||||
// 背景图片
|
||||
backdrop_path?: string;
|
||||
backdrop_path?: string
|
||||
|
||||
// 海报图片
|
||||
poster_path?: string;
|
||||
poster_path?: string
|
||||
|
||||
// 评分
|
||||
vote_average: number;
|
||||
vote_average: number
|
||||
|
||||
// 描述
|
||||
overview?: string;
|
||||
overview?: string
|
||||
|
||||
// 二级分类
|
||||
category?: string;
|
||||
category?: string
|
||||
|
||||
// 详情页面
|
||||
detail_link?: string;
|
||||
detail_link?: string
|
||||
}
|
||||
|
||||
// TMDB季信息
|
||||
export interface TmdbSeason {
|
||||
|
||||
// 上映日期
|
||||
air_date?: string;
|
||||
air_date?: string
|
||||
|
||||
// 总集数
|
||||
episode_count?: number;
|
||||
episode_count?: number
|
||||
|
||||
// 季名称
|
||||
name?: string;
|
||||
name?: string
|
||||
|
||||
// 描述
|
||||
overview?: string;
|
||||
overview?: string
|
||||
|
||||
// 海报
|
||||
poster_path?: string;
|
||||
poster_path?: string
|
||||
|
||||
// 季号
|
||||
season_number?: number;
|
||||
season_number?: number
|
||||
|
||||
// 评分
|
||||
vote_average?: number;
|
||||
vote_average?: number
|
||||
}
|
||||
|
||||
// TMDB集信息
|
||||
export interface TmdbEpisode {
|
||||
|
||||
// 上映日期
|
||||
air_date?: string;
|
||||
air_date?: string
|
||||
|
||||
// 集号
|
||||
episode_number?: number;
|
||||
episode_number?: number
|
||||
|
||||
// 剧集名称
|
||||
name?: string;
|
||||
name?: string
|
||||
|
||||
// 描述
|
||||
overview?: string;
|
||||
overview?: string
|
||||
|
||||
// 时长
|
||||
runtime?: number;
|
||||
runtime?: number
|
||||
|
||||
// 季号
|
||||
season_number?: number;
|
||||
season_number?: number
|
||||
|
||||
// 海报
|
||||
still_path?: string;
|
||||
still_path?: string
|
||||
|
||||
// 评分
|
||||
vote_average?: number;
|
||||
vote_average?: number
|
||||
|
||||
// 演职人员
|
||||
crew: Object[];
|
||||
crew: Object[]
|
||||
|
||||
// 嘉宾
|
||||
guest_stars: Object[];
|
||||
guest_stars: Object[]
|
||||
}
|
||||
|
||||
// 站点
|
||||
export interface Site {
|
||||
|
||||
// ID
|
||||
id: number;
|
||||
id: number
|
||||
|
||||
// 站点名称
|
||||
name: string;
|
||||
name: string
|
||||
|
||||
// 站点主域名Key
|
||||
domain: string;
|
||||
domain: string
|
||||
|
||||
// 站点地址
|
||||
url: string;
|
||||
url: string
|
||||
|
||||
// 站点优先级
|
||||
pri?: number;
|
||||
pri?: number
|
||||
|
||||
// RSS地址
|
||||
rss?: string;
|
||||
rss?: string
|
||||
|
||||
// Cookie
|
||||
cookie?: string;
|
||||
cookie?: string
|
||||
|
||||
// User-Agent
|
||||
ua?: string;
|
||||
ua?: string
|
||||
|
||||
// 是否使用代理
|
||||
proxy?: number;
|
||||
proxy?: number
|
||||
|
||||
// 过滤规则
|
||||
filter?: string;
|
||||
filter?: string
|
||||
|
||||
// 是否演染
|
||||
render?: number;
|
||||
render?: number
|
||||
|
||||
// 是否公开站点
|
||||
public?: number;
|
||||
public?: number
|
||||
|
||||
// 备注
|
||||
note?: string;
|
||||
note?: string
|
||||
|
||||
// 流控单位周期
|
||||
limit_interval?: number;
|
||||
limit_interval?: number
|
||||
|
||||
// 流控次数
|
||||
limit_count?: number;
|
||||
limit_count?: number
|
||||
|
||||
// 流控间隔
|
||||
limit_seconds?: number;
|
||||
limit_seconds?: number
|
||||
|
||||
// 是否启用
|
||||
is_active: boolean;
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 正在下载
|
||||
export interface DownloadingInfo {
|
||||
|
||||
// HASH
|
||||
hash?: string;
|
||||
hash?: string
|
||||
|
||||
// 种子名称
|
||||
title?: string;
|
||||
title?: string
|
||||
|
||||
// 识别后的名称
|
||||
name?: string;
|
||||
name?: string
|
||||
|
||||
// 年份
|
||||
year?: string;
|
||||
year?: string
|
||||
|
||||
// SXXEXX
|
||||
season_episode?: string;
|
||||
season_episode?: string
|
||||
|
||||
// 大小
|
||||
size?: number;
|
||||
size?: number
|
||||
|
||||
// 下载进 度
|
||||
progress?: number;
|
||||
progress?: number
|
||||
|
||||
// 状态
|
||||
state?: string;
|
||||
state?: string
|
||||
|
||||
// 下载速度
|
||||
dlspeed?: string;
|
||||
dlspeed?: string
|
||||
|
||||
// 上传速度
|
||||
upspeed?: string;
|
||||
upspeed?: string
|
||||
|
||||
// 媒体信息
|
||||
media: { [key: string]: any };
|
||||
media: { [key: string]: any }
|
||||
}
|
||||
|
||||
// 缺失剧集信息
|
||||
export interface NotExistMediaInfo {
|
||||
|
||||
// 季
|
||||
season: number;
|
||||
season: number
|
||||
|
||||
// 剧集列表
|
||||
episodes: number[];
|
||||
episodes: number[]
|
||||
|
||||
// 总集数
|
||||
total_episodes: number;
|
||||
total_episodes: number
|
||||
|
||||
// 开始集
|
||||
start_episode: number;
|
||||
start_episode: number
|
||||
}
|
||||
|
||||
// 插件
|
||||
export interface Plugin {
|
||||
id?: string;
|
||||
id?: string
|
||||
|
||||
// 插件名称
|
||||
plugin_name?: string;
|
||||
plugin_name?: string
|
||||
|
||||
// 插件描述
|
||||
plugin_desc?: string;
|
||||
plugin_desc?: string
|
||||
|
||||
// 插件图标
|
||||
plugin_icon?: string;
|
||||
plugin_icon?: string
|
||||
|
||||
// 主题色
|
||||
plugin_color?: string;
|
||||
plugin_color?: string
|
||||
|
||||
// 插件版本
|
||||
plugin_version?: string;
|
||||
plugin_version?: string
|
||||
|
||||
// 插件作者
|
||||
plugin_author?: string;
|
||||
plugin_author?: string
|
||||
|
||||
// 作者主页
|
||||
author_url?: string;
|
||||
author_url?: string
|
||||
|
||||
// 插件配置项ID前缀
|
||||
plugin_config_prefix?: string;
|
||||
plugin_config_prefix?: string
|
||||
|
||||
// 加载顺序
|
||||
plugin_order?: number;
|
||||
plugin_order?: number
|
||||
|
||||
// 可使用的用户级别
|
||||
auth_level?: number;
|
||||
auth_level?: number
|
||||
|
||||
// 是否已安装
|
||||
installed?: boolean;
|
||||
installed?: boolean
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
export interface TorrentInfo {
|
||||
|
||||
// 站点ID
|
||||
site?: number;
|
||||
site?: number
|
||||
|
||||
// 站点名称
|
||||
site_name?: string;
|
||||
site_name?: string
|
||||
|
||||
// 站点Cookie
|
||||
site_cookie?: string;
|
||||
site_cookie?: string
|
||||
|
||||
// 站点UA
|
||||
site_ua?: string;
|
||||
site_ua?: string
|
||||
|
||||
// 站点是否使用代理
|
||||
site_proxy: boolean;
|
||||
site_proxy: boolean
|
||||
|
||||
// 站点优先级
|
||||
site_order: number;
|
||||
site_order: number
|
||||
|
||||
// 种子名称
|
||||
title?: string;
|
||||
title?: string
|
||||
|
||||
// 种子副标题
|
||||
description?: string;
|
||||
description?: string
|
||||
|
||||
// IMDB ID
|
||||
imdbid: string;
|
||||
imdbid: string
|
||||
|
||||
// 种子链接
|
||||
enclosure?: string;
|
||||
enclosure?: string
|
||||
|
||||
// 详情页面
|
||||
page_url?: string;
|
||||
page_url?: string
|
||||
|
||||
// 种子大小
|
||||
size: number;
|
||||
size: number
|
||||
|
||||
// 做种者
|
||||
seeders: number;
|
||||
seeders: number
|
||||
|
||||
// 下载者
|
||||
peers: number;
|
||||
peers: number
|
||||
|
||||
// 完成者
|
||||
grabs: number;
|
||||
grabs: number
|
||||
|
||||
// 发布时间
|
||||
pubdate?: string;
|
||||
pubdate?: string
|
||||
|
||||
// 已过时间
|
||||
date_elapsed?: string;
|
||||
date_elapsed?: string
|
||||
|
||||
// 上传因子
|
||||
uploadvolumefactor: number;
|
||||
uploadvolumefactor: number
|
||||
|
||||
// 下载因子
|
||||
downloadvolumefactor: number;
|
||||
downloadvolumefactor: number
|
||||
|
||||
// HR
|
||||
hit_and_run: boolean;
|
||||
hit_and_run: boolean
|
||||
|
||||
// 种子标签
|
||||
labels: string[];
|
||||
labels: string[]
|
||||
|
||||
// 种子优先级
|
||||
pri_order: number;
|
||||
pri_order: number
|
||||
|
||||
// 促销描述
|
||||
volume_factor: string;
|
||||
volume_factor: string
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
export interface MetaInfo {
|
||||
|
||||
// 是否处理的文件
|
||||
isfile: boolean;
|
||||
isfile: boolean
|
||||
|
||||
// 原字符串
|
||||
org_string?: string;
|
||||
org_string?: string
|
||||
|
||||
// 副标题
|
||||
subtitle?: string;
|
||||
subtitle?: string
|
||||
|
||||
// 类型 电影、电视剧
|
||||
type: string;
|
||||
type: string
|
||||
|
||||
// 识别的中文名
|
||||
cn_name?: string;
|
||||
cn_name?: string
|
||||
|
||||
// 识别的英文名
|
||||
en_name?: string;
|
||||
en_name?: string
|
||||
|
||||
// 年份
|
||||
year?: string;
|
||||
year?: string
|
||||
|
||||
// 总季数
|
||||
total_seasons: number;
|
||||
total_seasons: number
|
||||
|
||||
// 识别的开始季 数字
|
||||
begin_season?: number;
|
||||
begin_season?: number
|
||||
|
||||
// 识别的结束季 数字
|
||||
end_season?: number;
|
||||
end_season?: number
|
||||
|
||||
// 总集数
|
||||
total_episodes: number;
|
||||
total_episodes: number
|
||||
|
||||
// 识别的开始集
|
||||
begin_episode?: number;
|
||||
begin_episode?: number
|
||||
|
||||
// 识别的结束集
|
||||
end_episode?: number;
|
||||
end_episode?: number
|
||||
|
||||
// Partx Cd Dvd Disk Disc
|
||||
part?: string;
|
||||
part?: string
|
||||
|
||||
// 识别的资源类型
|
||||
resource_type?: string;
|
||||
resource_type?: string
|
||||
|
||||
// 识别的效果
|
||||
resource_effect?: string;
|
||||
resource_effect?: string
|
||||
|
||||
// 识别的分辨率
|
||||
resource_pix?: string;
|
||||
resource_pix?: string
|
||||
|
||||
// 识别的制作组/字幕组
|
||||
resource_team?: string;
|
||||
resource_team?: string
|
||||
|
||||
// 视频编码
|
||||
video_encode?: string;
|
||||
video_encode?: string
|
||||
|
||||
// 音频编码
|
||||
audio_encode?: string;
|
||||
audio_encode?: string
|
||||
|
||||
// 名称(自动中英文)
|
||||
name: string;
|
||||
name: string
|
||||
|
||||
// SXX-SXX
|
||||
season: string;
|
||||
season: string
|
||||
|
||||
// SXX-SXX 有季号才返回
|
||||
sea: string;
|
||||
sea: string
|
||||
|
||||
// begin_season 的数字,电视剧没有季的返回1
|
||||
season_seq: string;
|
||||
season_seq: string
|
||||
|
||||
// 季的数组
|
||||
season_list: number[];
|
||||
season_list: number[]
|
||||
|
||||
// Exx-Exx
|
||||
episode: string;
|
||||
episode: string
|
||||
|
||||
// 集的数组
|
||||
episode_list: number[];
|
||||
episode_list: number[]
|
||||
|
||||
// ExxExx
|
||||
episodes: string;
|
||||
//xx-xx
|
||||
episode_seqs: string;
|
||||
episodes: string
|
||||
|
||||
// xx-xx
|
||||
episode_seqs: string
|
||||
|
||||
// begin_episode 的数字
|
||||
episode_seq: string;
|
||||
episode_seq: string
|
||||
|
||||
// SxxExx
|
||||
season_episode: string;
|
||||
season_episode: string
|
||||
|
||||
// 资源类型字符串,含分辨率
|
||||
resource_term: string;
|
||||
resource_term: string
|
||||
|
||||
// 发布组/字幕组字符串
|
||||
release_group: string;
|
||||
release_group: string
|
||||
|
||||
// 视频编码
|
||||
video_term: string;
|
||||
video_term: string
|
||||
|
||||
// 音频编码
|
||||
audio_term: string;
|
||||
audio_term: string
|
||||
|
||||
// 资源类型+特效
|
||||
edition: string;
|
||||
edition: string
|
||||
}
|
||||
|
||||
// 上下文信息
|
||||
export interface Context {
|
||||
|
||||
// 元信息
|
||||
meta_info: MetaInfo;
|
||||
meta_info: MetaInfo
|
||||
|
||||
// 媒体信息
|
||||
media_info: MediaInfo;
|
||||
media_info: MediaInfo
|
||||
|
||||
// 种子信息
|
||||
torrent_info: TorrentInfo;
|
||||
torrent_info: TorrentInfo
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
password: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
avatar: string;
|
||||
id: number
|
||||
name: string
|
||||
password: string
|
||||
email: string
|
||||
is_active: boolean
|
||||
is_superuser: boolean
|
||||
avatar: string
|
||||
}
|
||||
|
||||
// 存储空间
|
||||
export interface Storage {
|
||||
total_storage: number;
|
||||
used_storage: number;
|
||||
total_storage: number
|
||||
used_storage: number
|
||||
}
|
||||
|
||||
// 媒体统计
|
||||
export interface MediaStatistic {
|
||||
|
||||
// 电影总数
|
||||
movie_count: number;
|
||||
movie_count: number
|
||||
|
||||
// 电视剧总数
|
||||
tv_count: number;
|
||||
tv_count: number
|
||||
|
||||
// 电视剧总集数
|
||||
episode_count: number;
|
||||
episode_count: number
|
||||
|
||||
// 用户数量
|
||||
user_count: number;
|
||||
user_count: number
|
||||
}
|
||||
|
||||
// 后台进程
|
||||
export interface Process {
|
||||
|
||||
// 进程ID
|
||||
pid: number;
|
||||
pid: number
|
||||
|
||||
// 进程名称
|
||||
name: string;
|
||||
name: string
|
||||
|
||||
// 进程状态
|
||||
status: string;
|
||||
status: string
|
||||
|
||||
// 进程启动时间
|
||||
create_time: number;
|
||||
create_time: number
|
||||
|
||||
// 进程运行时间
|
||||
run_time: number;
|
||||
run_time: number
|
||||
|
||||
// 进程CPU占用率
|
||||
cpu: number;
|
||||
cpu: number
|
||||
|
||||
// 进程内存占用
|
||||
memory: number;
|
||||
memory: number
|
||||
}
|
||||
|
||||
// 下载器信息
|
||||
export interface DownloaderInfo {
|
||||
|
||||
// 下载速度
|
||||
download_speed: number;
|
||||
download_speed: number
|
||||
|
||||
// 上传速度
|
||||
upload_speed: number;
|
||||
upload_speed: number
|
||||
|
||||
// 下载量
|
||||
download_size: number;
|
||||
download_size: number
|
||||
|
||||
// 上传量
|
||||
upload_size: number;
|
||||
upload_size: number
|
||||
|
||||
// 剩余空间
|
||||
free_space: number;
|
||||
free_space: number
|
||||
}
|
||||
|
||||
// 定时服务信息
|
||||
export interface ScheduleInfo {
|
||||
// ID
|
||||
id: string;
|
||||
// 名称
|
||||
name: string;
|
||||
// 状态
|
||||
status: string;
|
||||
// 下次运行时间
|
||||
next_run: string;
|
||||
}
|
||||
|
||||
// ID
|
||||
id: string
|
||||
|
||||
// 名称
|
||||
name: string
|
||||
|
||||
// 状态
|
||||
status: string
|
||||
|
||||
// 下次运行时间
|
||||
next_run: string
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
export interface NotificationSwitch {
|
||||
|
||||
// 消息类型
|
||||
mtype: string;
|
||||
mtype: string
|
||||
|
||||
// 开关
|
||||
wechat: boolean;
|
||||
telegram: boolean;
|
||||
slack: boolean;
|
||||
wechat: boolean
|
||||
telegram: boolean
|
||||
slack: boolean
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import misc404 from '@images/pages/404.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import tree from '@images/pages/tree.png'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
@@ -16,8 +18,6 @@ interface Props {
|
||||
errorTitle?: string
|
||||
errorDescription?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,69 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { DownloadingInfo } from "@/api/types";
|
||||
import api from '@/api'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
info: Object as PropType<DownloadingInfo>,
|
||||
});
|
||||
})
|
||||
|
||||
// 是否显示卡片
|
||||
const cardState = ref(true);
|
||||
const cardState = ref(true)
|
||||
|
||||
// 进度条
|
||||
const getPercentage = () => {
|
||||
return props.info?.progress ?? 0;
|
||||
};
|
||||
function getPercentage() {
|
||||
return props.info?.progress ?? 0
|
||||
}
|
||||
|
||||
// 速度
|
||||
const getSpeedText = () => {
|
||||
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`;
|
||||
};
|
||||
function getSpeedText() {
|
||||
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`
|
||||
}
|
||||
|
||||
// 下载状态
|
||||
const isDownloading = ref(props.info?.state === "downloading" ? true : false);
|
||||
const isDownloading = ref(props.info?.state === 'downloading')
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false);
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
const imageLoadHandler = () => {
|
||||
imageLoaded.value = true;
|
||||
};
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 计算文本类
|
||||
const getTextClass = () => {
|
||||
return imageLoaded.value ? "text-white" : "";
|
||||
};
|
||||
function getTextClass() {
|
||||
return imageLoaded.value ? 'text-white' : ''
|
||||
}
|
||||
|
||||
// 下载状态控制
|
||||
const toggleDownload = async () => {
|
||||
let operation = isDownloading.value ? "stop" : "start";
|
||||
async function toggleDownload() {
|
||||
const operation = isDownloading.value ? 'stop' : 'start'
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put(
|
||||
`download/${props.info?.hash}/${operation}`
|
||||
);
|
||||
if (result.success) {
|
||||
isDownloading.value = !isDownloading.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
`download/${props.info?.hash}/${operation}`,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
isDownloading.value = !isDownloading.value
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除下截
|
||||
const deleteDownload = async () => {
|
||||
async function deleteDownload() {
|
||||
try {
|
||||
await api.delete(`download/${props.info?.hash}`);
|
||||
cardState.value = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await api.delete(`download/${props.info?.hash}`)
|
||||
cardState.value = false
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :key="props.info?.hash" v-if="cardState">
|
||||
<VCard
|
||||
v-if="cardState"
|
||||
:key="props.info?.hash"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.info?.media.image"
|
||||
@@ -74,18 +79,32 @@ const deleteDownload = async () => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="break-words whitespace-normal" :class="getTextClass()">
|
||||
<VCardTitle
|
||||
class="break-words whitespace-normal"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.info?.media.title || props.info?.name }}
|
||||
{{ props.info?.season_episode }}
|
||||
</VCardTitle>
|
||||
|
||||
<VCardSubtitle class="break-words whitespace-normal" :class="getTextClass()">
|
||||
<VCardSubtitle
|
||||
class="break-words whitespace-normal"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.info?.title }}
|
||||
</VCardSubtitle>
|
||||
|
||||
<VCardText class="text-subtitle-1 pt-3 pb-1" :class="getTextClass()"> {{ getSpeedText() }} </VCardText>
|
||||
<VCardText
|
||||
class="text-subtitle-1 pt-3 pb-1"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ getSpeedText() }}
|
||||
</VCardText>
|
||||
|
||||
<VCardText v-if="getPercentage() > 0" :class="getTextClass()">
|
||||
<VCardText
|
||||
v-if="getPercentage() > 0"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
<VProgressLinear :model-value="getPercentage()" />
|
||||
</VCardText>
|
||||
|
||||
@@ -93,7 +112,11 @@ const deleteDownload = async () => {
|
||||
<VBtn @click="toggleDownload">
|
||||
<span class="ms-2">{{ isDownloading ? "暂停" : "开始" }}</span>
|
||||
</VBtn>
|
||||
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
||||
<VBtn
|
||||
color="error"
|
||||
icon="mdi-trash-can-outline"
|
||||
@click="deleteDownload"
|
||||
/>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(["close", "changed"]);
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
pri: String,
|
||||
rules: Array as PropType<string[]>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
|
||||
// 按钮点击
|
||||
const onClose = () => {
|
||||
emit("close");
|
||||
};
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 选项变化
|
||||
const filtersChanged = (value: string[]) => {
|
||||
emit("changed", props.pri, value);
|
||||
};
|
||||
function filtersChanged(value: string[]) {
|
||||
emit('changed', props.pri, value)
|
||||
}
|
||||
|
||||
// 过滤规则下拉框
|
||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: "特效字幕", value: " SPECSUB " },
|
||||
{ title: "中文字幕", value: " CNSUB " },
|
||||
{ title: "分辨率: 4K", value: " 4K " },
|
||||
{ title: "分辨率: 1080P", value: " 1080P " },
|
||||
{ title: "分辨率: 720P", value: " 720P " },
|
||||
{ title: "排除: 720P", value: " !720P " },
|
||||
{ title: "质量: 蓝光原盘", value: " BLU " },
|
||||
{ title: "排除: 蓝光原盘", value: " !BLU ", color: "error" },
|
||||
{ title: "质量: REMUX", value: " REMUX " },
|
||||
{ title: "排除: REMUX", value: " !REMUX ", color: "error" },
|
||||
{ title: "质量: WEB-DL", value: " WEBDL " },
|
||||
{ title: "排除: WEB-DL", value: " !WEBDL ", color: "error" },
|
||||
{ title: "编码: H265", value: " H265 " },
|
||||
{ title: "排除: H265", value: " !H265 ", color: "error" },
|
||||
{ title: "编码: H264", value: " H264 " },
|
||||
{ title: "排除: H264", value: " !H264 ", color: "error" },
|
||||
{ title: "效果: 杜比视界", value: " DOLBY " },
|
||||
{ title: "排除: 杜比视界", value: " !DOLBY ", color: "error" },
|
||||
{ title: "效果: HDR", value: " HDR " },
|
||||
{ title: "排除: HDR", value: " !HDR ", color: "error" },
|
||||
{ title: "国语配音", value: " CNVOI ", color: "error" },
|
||||
{ title: "排除: 国语配音", value: " !CNVOI ", color: "error" },
|
||||
{ title: "促销: 免费", value: " FREE " },
|
||||
]);
|
||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||
{ title: '中文字幕', value: ' CNSUB ' },
|
||||
{ title: '分辨率: 4K', value: ' 4K ' },
|
||||
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
||||
{ title: '分辨率: 720P', value: ' 720P ' },
|
||||
{ title: '排除: 720P', value: ' !720P ' },
|
||||
{ title: '质量: 蓝光原盘', value: ' BLU ' },
|
||||
{ title: '排除: 蓝光原盘', value: ' !BLU ', color: 'error' },
|
||||
{ title: '质量: REMUX', value: ' REMUX ' },
|
||||
{ title: '排除: REMUX', value: ' !REMUX ', color: 'error' },
|
||||
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
||||
{ title: '排除: WEB-DL', value: ' !WEBDL ', color: 'error' },
|
||||
{ title: '编码: H265', value: ' H265 ' },
|
||||
{ title: '排除: H265', value: ' !H265 ', color: 'error' },
|
||||
{ title: '编码: H264', value: ' H264 ' },
|
||||
{ title: '排除: H264', value: ' !H264 ', color: 'error' },
|
||||
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
||||
{ title: '排除: 杜比视界', value: ' !DOLBY ', color: 'error' },
|
||||
{ title: '效果: HDR', value: ' HDR ' },
|
||||
{ title: '排除: HDR', value: ' !HDR ', color: 'error' },
|
||||
{ title: '国语配音', value: ' CNVOI ', color: 'error' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ', color: 'error' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
])
|
||||
|
||||
// 已选择的过滤规则
|
||||
const selectedFilters = ref<string[]>(props.rules ?? []);
|
||||
const selectedFilters = ref<string[]>(props.rules ?? [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<VCard
|
||||
variant="tonal"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
variant="underlined"
|
||||
:key="props.pri"
|
||||
v-model="selectedFilters"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
@update:modelValue="filtersChanged"
|
||||
chips
|
||||
label=""
|
||||
multiple
|
||||
>
|
||||
</VSelect>
|
||||
@update:modelValue="filtersChanged"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardItem>
|
||||
|
||||
@@ -1,209 +1,225 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatSeason } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import { doneNProgress, startNProgress } from "@/api/nprogress";
|
||||
import { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from "@/api/types";
|
||||
import router from "@/router";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
|
||||
import router from '@/router'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaInfo>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 图片加载状态
|
||||
const isImageLoaded = ref(false);
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片加载失败
|
||||
const imageLoadError = ref(false);
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// TMDB识别标志
|
||||
const tmdbFlag = ref(true);
|
||||
const tmdbFlag = ref(true)
|
||||
|
||||
// 当前订阅状态
|
||||
const isSubscribed = ref(false);
|
||||
const isSubscribed = ref(false)
|
||||
|
||||
// 本地存在状态
|
||||
const isExists = ref(false);
|
||||
const isExists = ref(false)
|
||||
|
||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({});
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
// 订阅季弹窗
|
||||
const subscribeSeasonDialog = ref(false);
|
||||
const subscribeSeasonDialog = ref(false)
|
||||
|
||||
// 季详情
|
||||
const seasonInfos = ref<TmdbSeason[]>([]);
|
||||
const seasonInfos = ref<TmdbSeason[]>([])
|
||||
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<TmdbSeason[]>([])
|
||||
|
||||
// 订阅弹窗选择的多季
|
||||
const subscribeSeasons = () => {
|
||||
subscribeSeasonDialog.value = false;
|
||||
function subscribeSeasons() {
|
||||
subscribeSeasonDialog.value = false
|
||||
seasonsSelected.value.forEach((season) => {
|
||||
addSubscribe(season.season_number);
|
||||
});
|
||||
};
|
||||
addSubscribe(season.season_number)
|
||||
})
|
||||
}
|
||||
|
||||
// 角标颜色
|
||||
const getChipColor = (type: string) => {
|
||||
if (type === "电影") {
|
||||
return "border-blue-500 bg-blue-600";
|
||||
} else if (type === "电视剧") {
|
||||
return " bg-indigo-500 border-indigo-600";
|
||||
} else {
|
||||
return "border-purple-600 bg-purple-600";
|
||||
}
|
||||
};
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影')
|
||||
return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧')
|
||||
return ' bg-indigo-500 border-indigo-600'
|
||||
else
|
||||
return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 添加订阅处理
|
||||
const handleAddSubscribe = async () => {
|
||||
if (props.media?.type == "电视剧" && props.media?.tmdb_id) {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
async function handleAddSubscribe() {
|
||||
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
|
||||
// TMDB电视剧
|
||||
// 查询TMDB所有季信息
|
||||
await getMediaSeasons();
|
||||
await getMediaSeasons()
|
||||
if (!seasonInfos.value) {
|
||||
$toast.error(`${props.media?.title} 查询剧集信息失败!`);
|
||||
return;
|
||||
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 检查各季的缺失状态
|
||||
await checkSeasonsNotExists();
|
||||
if (!tmdbFlag.value) {
|
||||
return;
|
||||
}
|
||||
if (seasonInfos.value.length == 1) {
|
||||
await checkSeasonsNotExists()
|
||||
if (!tmdbFlag.value)
|
||||
return
|
||||
|
||||
if (seasonInfos.value.length === 1) {
|
||||
// 只有1季
|
||||
if (!seasonsNotExisted.value[1]) {
|
||||
// 已存在
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
|
||||
} else {
|
||||
// 添加订阅
|
||||
addSubscribe();
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
|
||||
}
|
||||
} else {
|
||||
else {
|
||||
// 添加订阅
|
||||
addSubscribe()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 弹出季选择列表,支持多选
|
||||
subscribeSeasonDialog.value = true;
|
||||
}
|
||||
} else if (props.media?.type == "电视剧") {
|
||||
// 豆瓣电视剧,只会有一季
|
||||
let season = props.media?.season || 1;
|
||||
// 检查缺失情况
|
||||
await checkSeasonsNotExists();
|
||||
if (!tmdbFlag.value) {
|
||||
return;
|
||||
}
|
||||
if (!seasonsNotExisted.value[season]) {
|
||||
// 已存在
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
|
||||
} else {
|
||||
// 添加订阅
|
||||
addSubscribe(season);
|
||||
}
|
||||
} else {
|
||||
// 电影
|
||||
let exists = await checkMovieExists();
|
||||
if (exists) {
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
|
||||
} else {
|
||||
addSubscribe();
|
||||
subscribeSeasonDialog.value = true
|
||||
}
|
||||
}
|
||||
};
|
||||
else if (props.media?.type === '电视剧') {
|
||||
// 豆瓣电视剧,只会有一季
|
||||
const season = props.media?.season || 1
|
||||
|
||||
// 检查缺失情况
|
||||
await checkSeasonsNotExists()
|
||||
if (!tmdbFlag.value)
|
||||
return
|
||||
|
||||
if (!seasonsNotExisted.value[season]) {
|
||||
// 已存在
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
|
||||
}
|
||||
else {
|
||||
// 添加订阅
|
||||
addSubscribe(season)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 电影
|
||||
const exists = await checkMovieExists()
|
||||
if (exists)
|
||||
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
|
||||
else
|
||||
addSubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
const addSubscribe = async (season: number = 0) => {
|
||||
async function addSubscribe(season = 0) {
|
||||
// 开始处理
|
||||
startNProgress();
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post("subscribe", {
|
||||
const result: { [key: string]: any } = await api.post('subscribe', {
|
||||
name: props.media?.title,
|
||||
type: props.media?.type,
|
||||
year: props.media?.year,
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
doubanid: props.media?.douban_id,
|
||||
season: season,
|
||||
});
|
||||
season,
|
||||
})
|
||||
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
// 订阅成功
|
||||
isSubscribed.value = true;
|
||||
isSubscribed.value = true
|
||||
}
|
||||
|
||||
// 提示
|
||||
showSubscribeAddToast(
|
||||
result.success,
|
||||
props.media?.title ?? "",
|
||||
props.media?.title ?? '',
|
||||
season,
|
||||
result.message
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
result.message,
|
||||
)
|
||||
}
|
||||
doneNProgress();
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
const showSubscribeAddToast = (
|
||||
result: boolean,
|
||||
function showSubscribeAddToast(result: boolean,
|
||||
title: string,
|
||||
season: number,
|
||||
message: string
|
||||
) => {
|
||||
if (season) {
|
||||
title = `${title} ${formatSeason(season.toString())}`;
|
||||
}
|
||||
if (result) {
|
||||
$toast.success(`${title} 添加订阅成功!`);
|
||||
} else {
|
||||
$toast.error(`${title} 添加订阅失败:${message}!`);
|
||||
}
|
||||
};
|
||||
message: string) {
|
||||
if (season)
|
||||
title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
if (result)
|
||||
$toast.success(`${title} 添加订阅成功!`)
|
||||
else
|
||||
$toast.error(`${title} 添加订阅失败:${message}!`)
|
||||
}
|
||||
|
||||
// 调用API取消订阅
|
||||
const removeSubscribe = async () => {
|
||||
async function removeSubscribe() {
|
||||
// 开始处理
|
||||
startNProgress();
|
||||
startNProgress()
|
||||
try {
|
||||
let mediaid = props.media?.tmdb_id
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`;
|
||||
: `douban:${props.media?.douban_id}`
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
{
|
||||
params: {
|
||||
season: props.media?.season,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
isSubscribed.value = false;
|
||||
$toast.success(`${props.media?.title} 已取消订阅!`);
|
||||
} else {
|
||||
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}!`);
|
||||
isSubscribed.value = false
|
||||
$toast.success(`${props.media?.title} 已取消订阅!`)
|
||||
}
|
||||
else {
|
||||
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
doneNProgress();
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已订阅
|
||||
const handleCheckSubscribe = async () => {
|
||||
async function handleCheckSubscribe() {
|
||||
try {
|
||||
const result = await checkSubscribe(props.media?.season);
|
||||
if (result) {
|
||||
isSubscribed.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const result = await checkSubscribe(props.media?.season)
|
||||
if (result)
|
||||
isSubscribed.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已存在
|
||||
const handleCheckExists = async () => {
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
const result: {[key:string]: any} = await api.get(`media/exists`, {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
title: props.media?.title,
|
||||
@@ -211,143 +227,145 @@ const handleCheckExists = async () => {
|
||||
season: props.media?.season,
|
||||
mtype: props.media?.type,
|
||||
},
|
||||
});
|
||||
if (result.success) {
|
||||
isExists.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
})
|
||||
|
||||
if (result.success)
|
||||
isExists.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API检查是否已订阅,电视剧需要指定季
|
||||
const checkSubscribe = async (season: number = 0) => {
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
let mediaid = props.media?.tmdb_id
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`;
|
||||
: `douban:${props.media?.douban_id}`
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season: season,
|
||||
season,
|
||||
},
|
||||
});
|
||||
return result.id || null;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
})
|
||||
|
||||
return result.id || null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查所有季的缺失状态
|
||||
const checkSeasonsNotExists = async () => {
|
||||
async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress();
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post(`download/notexists`, props.media);
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
// 0-已存在 1-部分缺失 2-全部缺失
|
||||
let state = 0;
|
||||
if (item.episodes.length == 0) {
|
||||
state = 2;
|
||||
} else if (item.episodes.length < item.total_episodes) {
|
||||
state = 1;
|
||||
}
|
||||
seasonsNotExisted.value[item.season] = state;
|
||||
});
|
||||
let state = 0
|
||||
if (item.episodes.length === 0)
|
||||
state = 2
|
||||
else if (item.episodes.length < item.total_episodes)
|
||||
state = 1
|
||||
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`);
|
||||
tmdbFlag.value = false;
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`)
|
||||
tmdbFlag.value = false
|
||||
}
|
||||
|
||||
// 处理完成
|
||||
doneNProgress();
|
||||
};
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 检查电影是否存在
|
||||
const checkMovieExists = async () => {
|
||||
async function checkMovieExists() {
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post(`download/notexists`, props.media);
|
||||
if (!result || result.length === 0) {
|
||||
// 没有缺失的就是存在
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
return !result || result.length === 0
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询TMDB的所有季信息
|
||||
const getMediaSeasons = async () => {
|
||||
async function getMediaSeasons() {
|
||||
try {
|
||||
seasonInfos.value = await api.get(`tmdb/${props.media?.tmdb_id}/seasons`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
seasonInfos.value = await api.get(`tmdb/${props.media?.tmdb_id}/seasons`)
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
const handleSubscribe = () => {
|
||||
if (isSubscribed.value) {
|
||||
removeSubscribe();
|
||||
} else {
|
||||
handleAddSubscribe();
|
||||
}
|
||||
};
|
||||
function handleSubscribe() {
|
||||
if (isSubscribed.value)
|
||||
removeSubscribe()
|
||||
else
|
||||
handleAddSubscribe()
|
||||
}
|
||||
|
||||
// 拼装详情页链接
|
||||
const getDetailLink = () => {
|
||||
let link = "";
|
||||
if (props.media?.douban_id) {
|
||||
link = `https://movie.douban.com/subject/${props.media?.douban_id}/`;
|
||||
} else if (props.media?.type === "电影") {
|
||||
link = `https://www.themoviedb.org/movie/${props.media?.tmdb_id}`;
|
||||
} else if (props.media?.type === "电视剧") {
|
||||
link = `https://www.themoviedb.org/tv/${props.media?.tmdb_id}`;
|
||||
}
|
||||
return link;
|
||||
};
|
||||
function getDetailLink() {
|
||||
let link = ''
|
||||
if (props.media?.douban_id)
|
||||
link = `https://movie.douban.com/subject/${props.media?.douban_id}/`
|
||||
else if (props.media?.type === '电影')
|
||||
link = `https://www.themoviedb.org/movie/${props.media?.tmdb_id}`
|
||||
else if (props.media?.type === '电视剧')
|
||||
link = `https://www.themoviedb.org/tv/${props.media?.tmdb_id}`
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
// 计算存在状态的颜色
|
||||
const getExistColor = (season: number) => {
|
||||
let state = seasonsNotExisted.value[season];
|
||||
if (!state) {
|
||||
return "success";
|
||||
}
|
||||
if (state === 1) {
|
||||
return "warning";
|
||||
} else if (state === 2) {
|
||||
return "error";
|
||||
} else {
|
||||
return "success";
|
||||
}
|
||||
};
|
||||
function getExistColor(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return 'success'
|
||||
|
||||
if (state === 1)
|
||||
return 'warning'
|
||||
else if (state === 2)
|
||||
return 'error'
|
||||
else
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 计算存在状态的文本
|
||||
const getExistText = (season: number) => {
|
||||
let state = seasonsNotExisted.value[season];
|
||||
if (!state) {
|
||||
return "已存在";
|
||||
}
|
||||
if (state === 1) {
|
||||
return "部分缺失";
|
||||
} else if (state === 2) {
|
||||
return "缺失";
|
||||
} else {
|
||||
return "已存在";
|
||||
}
|
||||
};
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return '已存在'
|
||||
|
||||
if (state === 1)
|
||||
return '部分缺失'
|
||||
else if (state === 2)
|
||||
return '缺失'
|
||||
else
|
||||
return '已存在'
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
const openDetailWindow = () => {
|
||||
window.open(getDetailLink(), "_blank");
|
||||
};
|
||||
function openDetailWindow() {
|
||||
window.open(getDetailLink(), '_blank')
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
const handleSearch = () => {
|
||||
function handleSearch() {
|
||||
router.push({
|
||||
path: "/resource",
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: `${
|
||||
props.media?.tmdb_id
|
||||
@@ -356,34 +374,31 @@ const handleSearch = () => {
|
||||
}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
// 装载时检查是否已订阅
|
||||
onBeforeMount(() => {
|
||||
handleCheckSubscribe();
|
||||
handleCheckExists();
|
||||
});
|
||||
handleCheckSubscribe()
|
||||
handleCheckExists()
|
||||
})
|
||||
|
||||
// 订阅季表头
|
||||
const seasonsHeaders = [
|
||||
{ title: "季", key: "title", sortable: false },
|
||||
{ title: "集数", key: "episodes", sortable: false },
|
||||
{ title: "评分", key: "vote", sortable: false },
|
||||
{ title: "状态", key: "status", sortable: false },
|
||||
];
|
||||
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<TmdbSeason[]>([]);
|
||||
{ title: '季', key: 'title', sortable: false },
|
||||
{ title: '集数', key: 'episodes', sortable: false },
|
||||
{ title: '评分', key: 'vote', sortable: false },
|
||||
{ title: '状态', key: 'status', sortable: false },
|
||||
]
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = (url: string) => {
|
||||
function getImgUrl(url: string) {
|
||||
// 如果地址中包含douban则使用中转代理
|
||||
if (url.includes("doubanio.com")) {
|
||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
if (url.includes('doubanio.com'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
|
||||
|
||||
return url
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -404,13 +419,13 @@ const getImgUrl = (url: string) => {
|
||||
:src="getImgUrl(props.media?.poster_path || '')"
|
||||
class="object-cover aspect-w-2 aspect-h-3"
|
||||
:class="hover.isHovering ? 'on-hover' : ''"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
@error="imageLoadError = true"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="relative animate-pulse bg-gray-300 w-full">
|
||||
<div class="w-full h-full"></div>
|
||||
<div class="w-full h-full" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 类型角标 -->
|
||||
@@ -423,12 +438,12 @@ const getImgUrl = (url: string) => {
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 本地存在标识 -->
|
||||
<ExistIcon v-if="isExists"/>
|
||||
<ExistIcon v-if="isExists" />
|
||||
<!-- 评分角标 -->
|
||||
<VChip
|
||||
v-if="props.media?.vote_average && !isExists"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
v-if="props.media?.vote_average && !isExists"
|
||||
:class="getChipColor('rating')"
|
||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
@@ -436,21 +451,23 @@ const getImgUrl = (url: string) => {
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
@click.stop="openDetailWindow"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.year }}</span>
|
||||
<h1
|
||||
class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.overview }}
|
||||
</p>
|
||||
<div class="flex align-center justify-between">
|
||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
|
||||
<IconBtn
|
||||
icon="mdi-magnify"
|
||||
color="white"
|
||||
@click.stop="handleSearch"
|
||||
/>
|
||||
<IconBtn
|
||||
icon="mdi-heart"
|
||||
:color="isSubscribed ? 'error' : 'white'"
|
||||
@@ -484,39 +501,53 @@ const getImgUrl = (url: string) => {
|
||||
height="auto"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<span class="d-block whitespace-nowrap"
|
||||
>第 {{ item.raw.season_number }} 季
|
||||
</span></template
|
||||
>
|
||||
<span class="d-block whitespace-nowrap">第 {{ item.raw.season_number }} 季
|
||||
</span>
|
||||
</template>
|
||||
<template #item.episodes="{ item }">
|
||||
<VChip variant="outlined" size="small">{{ item.raw.episode_count }}</VChip>
|
||||
<VChip
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{{ item.raw.episode_count }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.vote="{ item }">
|
||||
{{ item.raw.vote_average }}
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="getExistColor(item.raw.season_number)"
|
||||
v-if="seasonsNotExisted"
|
||||
:color="getExistColor(item.raw.season_number)"
|
||||
flat
|
||||
size="small"
|
||||
>{{ getExistText(item.raw.season_number) }}</VChip
|
||||
>
|
||||
{{ getExistText(item.raw.season_number) }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #no-data> 没有数据 </template>
|
||||
<template #bottom></template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="subscribeSeasonDialog = false"> 取消 </VBtn>
|
||||
<VBtn @click="subscribeSeasonDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="subscribeSeasons" @keydown.enter="subscribeSeasons"> 确定 </VBtn>
|
||||
<VBtn
|
||||
@click="subscribeSeasons"
|
||||
@keydown.enter="subscribeSeasons"
|
||||
>
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.on-hover img {
|
||||
@apply brightness-50;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,63 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { Plugin } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(["install"]);
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
plugin: Object as PropType<Plugin>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['install'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 安装插件
|
||||
const installPlugin = async () => {
|
||||
async function installPlugin() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`
|
||||
);
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`);
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
||||
|
||||
// 通知父组件刷新
|
||||
emit("install");
|
||||
} else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`);
|
||||
emit('install')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :width="props.width" :height="props.height" @click="installPlugin">
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="installPlugin"
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
>
|
||||
<VAvatar size="128" class="shadow">
|
||||
<VImg :src="`/plugin/${props.plugin?.plugin_icon}`" aspect-ratio="4/3" cover />
|
||||
<VAvatar
|
||||
size="128"
|
||||
class="shadow"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle>
|
||||
@@ -50,13 +66,18 @@ const installPlugin = async () => {
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
作者:<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
作者:<a
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
<style type="scss" scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-cover-blurred::before {
|
||||
position: absolute;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
|
||||
@@ -1,62 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { Plugin } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(["remove"]);
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
plugin: Object as PropType<Plugin>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 本身是否可见
|
||||
const isVisible = ref(true);
|
||||
const isVisible = ref(true)
|
||||
|
||||
// 卸载插件
|
||||
const uninstallPlugin = async () => {
|
||||
async function uninstallPlugin() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`);
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`);
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
|
||||
|
||||
// 通知父组件刷新
|
||||
emit("remove");
|
||||
} else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`);
|
||||
emit('remove')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示插件详情
|
||||
const showPluginInfo = () => {};
|
||||
function showPluginInfo() {}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: "卸载",
|
||||
title: '卸载',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: "mdi-trash-can-outline",
|
||||
color: "error",
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
]);
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
v-if="isVisible"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="showPluginInfo"
|
||||
v-if="isVisible"
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
@@ -65,26 +69,36 @@ const dropdownItems = ref([
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
:key="i"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon"></VIcon>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title"></VListItemTitle>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar size="128" class="shadow">
|
||||
<VImg :src="`/plugin/${props.plugin?.plugin_icon}`" aspect-ratio="4/3" cover />
|
||||
<VAvatar
|
||||
size="128"
|
||||
class="shadow"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardItem class="py-2">
|
||||
@@ -95,7 +109,8 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
<style type="scss" scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-cover-blurred::before {
|
||||
position: absolute;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
|
||||
@@ -1,183 +1,207 @@
|
||||
<script lang="ts" setup>
|
||||
import { numberValidator, requiredValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import { Site } from "@/api/types";
|
||||
import ExistIcon from "@core/components/ExistIcon.vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
site: Object as PropType<Site>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 密码输入
|
||||
const isPasswordVisible = ref(false);
|
||||
const isPasswordVisible = ref(false)
|
||||
|
||||
// 图标
|
||||
const siteIcon = ref<string>("");
|
||||
const siteIcon = ref<string>('')
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 测试按钮文字
|
||||
const testButtonText = ref("测试");
|
||||
const testButtonText = ref('测试')
|
||||
|
||||
// 测试按钮可用性
|
||||
const testButtonDisable = ref(false);
|
||||
const testButtonDisable = ref(false)
|
||||
|
||||
// 更新按钮文字
|
||||
const updateButtonText = ref("更新");
|
||||
const updateButtonText = ref('更新')
|
||||
|
||||
// 更新按钮可用性
|
||||
const updateButtonDisable = ref(false);
|
||||
const updateButtonDisable = ref(false)
|
||||
|
||||
// 更新站点Cookie UA弹窗
|
||||
const siteCookieDialog = ref(false);
|
||||
const siteCookieDialog = ref(false)
|
||||
|
||||
// 站点编辑弹窗
|
||||
const siteInfoDialog = ref(false);
|
||||
const siteInfoDialog = ref(false)
|
||||
|
||||
// 用户名密码表单
|
||||
const userPwForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 查询站点图标
|
||||
const getSiteIcon = async () => {
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get("site/icon/" + props.site?.id)).data.icon;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
siteIcon.value = (await api.get(`site/icon/${props.site?.id}`)).data.icon
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试站点连通性
|
||||
const testSite = async () => {
|
||||
async function testSite() {
|
||||
try {
|
||||
testButtonText.value = "测试中 ...";
|
||||
testButtonDisable.value = true;
|
||||
const result: { [key: string]: any } = await api.get("site/test/" + props.site?.id);
|
||||
if (result.success) {
|
||||
$toast.success(`${props.site?.name} 连通性测试成功,可正常使用!`);
|
||||
} else {
|
||||
$toast.error(`${props.site?.name} 连通性测试失败:${result.message}`);
|
||||
}
|
||||
testButtonText.value = "测试";
|
||||
testButtonDisable.value = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
testButtonText.value = '测试中 ...'
|
||||
testButtonDisable.value = true
|
||||
|
||||
const result: { [key: string]: any } = await api.get(`site/test/${props.site?.id}`)
|
||||
if (result.success)
|
||||
$toast.success(`${props.site?.name} 连通性测试成功,可正常使用!`)
|
||||
else
|
||||
$toast.error(`${props.site?.name} 连通性测试失败:${result.message}`)
|
||||
|
||||
testButtonText.value = '测试'
|
||||
testButtonDisable.value = false
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开更新站点Cookie UA弹窗
|
||||
const handleSiteUpdate = async () => {
|
||||
siteCookieDialog.value = true;
|
||||
};
|
||||
async function handleSiteUpdate() {
|
||||
siteCookieDialog.value = true
|
||||
}
|
||||
|
||||
// 打开站点编辑弹窗
|
||||
const handleSiteInfo = async () => {
|
||||
siteInfoDialog.value = true;
|
||||
};
|
||||
async function handleSiteInfo() {
|
||||
siteInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 调用API,更新站点Cookie UA
|
||||
const updateSiteCookie = async () => {
|
||||
async function updateSiteCookie() {
|
||||
try {
|
||||
if (!userPwForm.value.username || !userPwForm.value.password) {
|
||||
return;
|
||||
}
|
||||
if (!userPwForm.value.username || !userPwForm.value.password)
|
||||
return
|
||||
|
||||
// 更新按钮状态
|
||||
siteCookieDialog.value = false;
|
||||
updateButtonText.value = "更新中 ...";
|
||||
updateButtonDisable.value = true;
|
||||
siteCookieDialog.value = false
|
||||
updateButtonText.value = '更新中 ...'
|
||||
updateButtonDisable.value = true
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
"site/cookie/" + props.site?.id,
|
||||
`site/cookie/${props.site?.id}`,
|
||||
{
|
||||
params: {
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success(`${props.site?.name} 更新Cookie & UA 成功!`);
|
||||
} else {
|
||||
$toast.error(`${props.site?.name} 更新失败:${result.message}`);
|
||||
}
|
||||
updateButtonText.value = "更新";
|
||||
updateButtonDisable.value = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
},
|
||||
)
|
||||
|
||||
// 调用API更新站点信息
|
||||
const updateSiteInfo = async () => {
|
||||
try {
|
||||
// 更新按钮状态
|
||||
siteInfoDialog.value = false;
|
||||
if (result.success)
|
||||
$toast.success(`${props.site?.name} 更新Cookie & UA 成功!`)
|
||||
else
|
||||
$toast.error(`${props.site?.name} 更新失败:${result.message}`)
|
||||
|
||||
const result: { [key: string]: any } = await api.put("site", siteForm);
|
||||
if (result.success) {
|
||||
$toast.success(`${props.site?.name} 更新成功!`);
|
||||
} else {
|
||||
$toast.error(`${props.site?.name} 更新失败:${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
$toast.error(`${props.site?.name} 更新失败!`);
|
||||
console.error(error);
|
||||
updateButtonText.value = '更新'
|
||||
updateButtonDisable.value = false
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 站点编辑表单数据
|
||||
const siteForm = reactive({
|
||||
// ID
|
||||
id: props.site?.id,
|
||||
|
||||
// 站点名称
|
||||
name: props.site?.name,
|
||||
|
||||
// 站点主域名Key
|
||||
domain: props.site?.domain,
|
||||
|
||||
// 站点地址
|
||||
url: props.site?.url,
|
||||
|
||||
// 站点优先级
|
||||
pri: props.site?.pri,
|
||||
|
||||
// RSS地址
|
||||
rss: props.site?.rss,
|
||||
|
||||
// Cookie
|
||||
cookie: props.site?.cookie,
|
||||
|
||||
// User-Agent
|
||||
ua: props.site?.ua,
|
||||
|
||||
// 是否使用代理
|
||||
proxy: props.site?.proxy ? true : false,
|
||||
proxy: !!props.site?.proxy,
|
||||
|
||||
// 过滤规则
|
||||
filter: props.site?.filter,
|
||||
|
||||
// 是否演染
|
||||
render: props.site?.render ? true : false,
|
||||
render: !!props.site?.render,
|
||||
|
||||
// 是否公开站点
|
||||
public: props.site?.public,
|
||||
|
||||
// 备注
|
||||
note: props.site?.note,
|
||||
|
||||
// 流控单位周期
|
||||
limit_interval: props.site?.limit_interval,
|
||||
|
||||
// 流控次数
|
||||
limit_count: props.site?.limit_count,
|
||||
|
||||
// 流控间隔
|
||||
limit_seconds: props.site?.limit_seconds,
|
||||
|
||||
// 是否启用
|
||||
is_active: props.site?.is_active,
|
||||
});
|
||||
})
|
||||
|
||||
// 调用API更新站点信息
|
||||
async function updateSiteInfo() {
|
||||
try {
|
||||
// 更新按钮状态
|
||||
siteInfoDialog.value = false
|
||||
|
||||
const result: { [key: string]: any } = await api.put('site', siteForm)
|
||||
if (result.success)
|
||||
$toast.success(`${props.site?.name} 更新成功!`)
|
||||
else
|
||||
$toast.error(`${props.site?.name} 更新失败:${result.message}`)
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error(`${props.site?.name} 更新失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 状态下拉项
|
||||
const statusItems = [
|
||||
{ title: "启用", value: true },
|
||||
{ title: "停用", value: false },
|
||||
];
|
||||
{ title: '启用', value: true },
|
||||
{ title: '停用', value: false },
|
||||
]
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon();
|
||||
});
|
||||
getSiteIcon()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -189,37 +213,70 @@ onMounted(() => {
|
||||
@click="siteInfoDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VAvatar
|
||||
class="absolute right-2 bottom-2 rounded"
|
||||
variant="flat"
|
||||
rounded="0"
|
||||
>
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">{{ props.site?.name }}</VCardTitle>
|
||||
<VCardTitle class="font-bold">
|
||||
{{ props.site?.name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ props.site?.url }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<ExistIcon v-if="siteForm.is_active" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip text="浏览器仿真" v-if="siteForm.render">
|
||||
<VTooltip
|
||||
v-if="siteForm.render"
|
||||
text="浏览器仿真"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
<VIcon
|
||||
color="primary"
|
||||
class="me-2"
|
||||
v-bind="props"
|
||||
icon="mdi-apple-safari"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip text="代理" v-if="siteForm.proxy">
|
||||
<VTooltip
|
||||
v-if="siteForm.proxy"
|
||||
text="代理"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
<VIcon
|
||||
color="primary"
|
||||
class="me-2"
|
||||
v-bind="props"
|
||||
icon="mdi-network-outline"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip text="流控" v-if="siteForm.limit_interval">
|
||||
<VTooltip
|
||||
v-if="siteForm.limit_interval"
|
||||
text="流控"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
<VIcon
|
||||
color="primary"
|
||||
class="me-2"
|
||||
v-bind="props"
|
||||
icon="mdi-speedometer"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip text="过滤" v-if="siteForm.filter">
|
||||
<VTooltip
|
||||
v-if="siteForm.filter"
|
||||
text="过滤"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VIcon
|
||||
color="primary"
|
||||
@@ -233,44 +290,56 @@ onMounted(() => {
|
||||
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
@click.stop="handleSiteUpdate"
|
||||
:disabled="updateButtonDisable"
|
||||
v-if="!props.site?.public"
|
||||
:disabled="updateButtonDisable"
|
||||
@click.stop="handleSiteUpdate"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh"></VIcon>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
{{ updateButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleSiteInfo">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-square-edit-outline"></VIcon>
|
||||
<VIcon icon="mdi-square-edit-outline" />
|
||||
</template>
|
||||
编辑
|
||||
</VBtn>
|
||||
<VBtn @click.stop="testSite" :disabled="testButtonDisable">
|
||||
<VBtn
|
||||
:disabled="testButtonDisable"
|
||||
@click.stop="testSite"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-network-outline"></VIcon>
|
||||
<VIcon icon="mdi-network-outline" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="600">
|
||||
<VDialog
|
||||
v-model="siteCookieDialog"
|
||||
max-width="600"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.username"
|
||||
label="用户名"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
label="密码"
|
||||
@@ -278,8 +347,8 @@ onMounted(() => {
|
||||
:append-inner-icon="
|
||||
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
|
||||
"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -289,25 +358,38 @@ onMounted(() => {
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSiteCookie"> 开始更新 </VBtn>
|
||||
<VBtn @click="updateSiteCookie">
|
||||
开始更新
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog v-model="siteInfoDialog" max-width="1000" persistent scrollable>
|
||||
<VDialog
|
||||
v-model="siteInfoDialog"
|
||||
max-width="1000"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`编辑站点 - ${props.site?.name}`">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.url"
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.pri"
|
||||
label="优先级"
|
||||
@@ -315,34 +397,56 @@ onMounted(() => {
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VSelect
|
||||
v-model="siteForm.is_active"
|
||||
:items="statusItems"
|
||||
label="状态"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextarea v-model="siteForm.cookie" label="站点Cookie" />
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="siteForm.ua" label="站点User-Agent" />
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
@@ -351,20 +455,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="siteForm.proxy" label="代理" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.proxy"
|
||||
label="代理"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="siteForm.render" label="仿真" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn @click="siteInfoDialog = false"> 取消 </VBtn>
|
||||
<VBtn @click="siteInfoDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSiteInfo"> 确定 </VBtn>
|
||||
<VBtn @click="updateSiteInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,209 +1,225 @@
|
||||
<script lang="ts" setup>
|
||||
import { calculateTimeDifference } from "@/@core/utils";
|
||||
import { formatSeason } from "@/@core/utils/formatters";
|
||||
import { numberValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import { Site, Subscribe } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<Subscribe>,
|
||||
});
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 是否显示卡片
|
||||
const cardState = ref(true);
|
||||
const cardState = ref(true)
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false);
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 订阅弹窗
|
||||
const subscribeInfoDialog = ref(false);
|
||||
const subscribeInfoDialog = ref(false)
|
||||
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([]);
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([]);
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
// 订阅编辑表单
|
||||
const subscribeForm = reactive({
|
||||
id: props.media?.id,
|
||||
|
||||
// 搜索关键字
|
||||
keyword: props.media?.keyword,
|
||||
|
||||
// 过滤规则
|
||||
filter: props.media?.filter,
|
||||
|
||||
// 包含
|
||||
include: props.media?.include,
|
||||
|
||||
// 排除
|
||||
exclude: props.media?.exclude,
|
||||
|
||||
// 总集数
|
||||
total_episode: props.media?.total_episode,
|
||||
|
||||
// 开始集数
|
||||
start_episode: props.media?.start_episode,
|
||||
|
||||
// 订阅站点
|
||||
sites: props.media?.sites,
|
||||
})
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(
|
||||
`${
|
||||
props.media?.last_update
|
||||
? `${calculateTimeDifference(props.media?.last_update || "")}前`
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
? `${calculateTimeDifference(props.media?.last_update || '')}前`
|
||||
: ''
|
||||
}`,
|
||||
)
|
||||
|
||||
// 图片加载完成响应
|
||||
const imageLoadHandler = () => {
|
||||
imageLoaded.value = true;
|
||||
};
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
const getIcon = () => {
|
||||
if (props.media?.type === "电影") {
|
||||
return "mdi-movie";
|
||||
} else if (props.media?.type === "电视剧") {
|
||||
return "mdi-television-classic";
|
||||
} else {
|
||||
return "mdi-help-circle";
|
||||
}
|
||||
};
|
||||
function getIcon() {
|
||||
if (props.media?.type === '电影')
|
||||
return 'mdi-movie'
|
||||
else if (props.media?.type === '电视剧')
|
||||
return 'mdi-television-classic'
|
||||
else
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算百分比
|
||||
const getPercentage = () => {
|
||||
if (props.media?.total_episode === 0) {
|
||||
return 0;
|
||||
}
|
||||
function getPercentage() {
|
||||
if (props.media?.total_episode === 0)
|
||||
return 0
|
||||
|
||||
return Math.round(
|
||||
(((props.media?.total_episode || 0) - (props.media?.lack_episode || 0)) /
|
||||
(props.media?.total_episode || 1)) *
|
||||
100
|
||||
);
|
||||
};
|
||||
(((props.media?.total_episode || 0) - (props.media?.lack_episode || 0))
|
||||
/ (props.media?.total_episode || 1))
|
||||
* 100,
|
||||
)
|
||||
}
|
||||
|
||||
// 计算文本颜色
|
||||
const getTextColor = () => {
|
||||
return imageLoaded.value ? "white" : "";
|
||||
};
|
||||
function getTextColor() {
|
||||
return imageLoaded.value ? 'white' : ''
|
||||
}
|
||||
|
||||
// 计算文本类
|
||||
const getTextClass = () => {
|
||||
return imageLoaded.value ? "text-white" : "";
|
||||
};
|
||||
function getTextClass() {
|
||||
return imageLoaded.value ? 'text-white' : ''
|
||||
}
|
||||
|
||||
// 删除订阅
|
||||
const removeSubscribe = async () => {
|
||||
async function removeSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/${props.media?.id}`
|
||||
);
|
||||
if (result.success) {
|
||||
cardState.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
`subscribe/${props.media?.id}`,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
cardState.value = false
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索订阅
|
||||
const searchSubscribe = async () => {
|
||||
async function searchSubscribe() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`subscribe/search/${props.media?.id}`
|
||||
);
|
||||
`subscribe/search/${props.media?.id}`,
|
||||
)
|
||||
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.name} 提交搜索请求成功!`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (result.success)
|
||||
$toast.success(`${props.media?.name} 提交搜索请求成功!`)
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API修改订阅
|
||||
const updateSubscribeInfo = async () => {
|
||||
subscribeInfoDialog.value = false;
|
||||
async function updateSubscribeInfo() {
|
||||
subscribeInfoDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put(`subscribe`, subscribeForm);
|
||||
const result: { [key: string]: any } = await api.put('subscribe', subscribeForm)
|
||||
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.name} 更新成功!`);
|
||||
} else {
|
||||
$toast.error(`${props.media?.name} 更新失败:${result.message}!`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (result.success)
|
||||
$toast.success(`${props.media?.name} 更新成功!`)
|
||||
else
|
||||
$toast.error(`${props.media?.name} 更新失败:${result.message}!`)
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表数据
|
||||
const loadSites = async () => {
|
||||
async function loadSites() {
|
||||
try {
|
||||
const data: Site[] = await api.get("site");
|
||||
const data: Site[] = await api.get('site')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
siteList.value = data.filter((item) => item.is_active);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
siteList.value = data.filter(item => item.is_active)
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表选择框数据
|
||||
const getSiteList = async () => {
|
||||
async function getSiteList() {
|
||||
// 加载订阅站点列表
|
||||
if (!siteList.value.length) {
|
||||
await loadSites();
|
||||
}
|
||||
if (!siteList.value.length)
|
||||
await loadSites()
|
||||
|
||||
const maps = siteList.value.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
selectSitesOptions.value = maps.flat();
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
selectSitesOptions.value = maps.flat()
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
const editSubscribeDialog = async () => {
|
||||
await getSiteList();
|
||||
subscribeInfoDialog.value = true;
|
||||
};
|
||||
async function editSubscribeDialog() {
|
||||
await getSiteList()
|
||||
subscribeInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: "编辑",
|
||||
title: '编辑',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: "mdi-file-edit-outline",
|
||||
prependIcon: 'mdi-file-edit-outline',
|
||||
click: editSubscribeDialog,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "搜索",
|
||||
title: '搜索',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: "mdi-magnify",
|
||||
prependIcon: 'mdi-magnify',
|
||||
click: searchSubscribe,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "取消订阅",
|
||||
title: '取消订阅',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: "mdi-trash-can-outline",
|
||||
color: "error",
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: removeSubscribe,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 订阅编辑表单
|
||||
const subscribeForm = reactive({
|
||||
id: props.media?.id,
|
||||
// 搜索关键字
|
||||
keyword: props.media?.keyword,
|
||||
// 过滤规则
|
||||
filter: props.media?.filter,
|
||||
// 包含
|
||||
include: props.media?.include,
|
||||
// 排除
|
||||
exclude: props.media?.exclude,
|
||||
// 总集数
|
||||
total_episode: props.media?.total_episode,
|
||||
// 开始集数
|
||||
start_episode: props.media?.start_episode,
|
||||
// 订阅站点
|
||||
sites: props.media?.sites,
|
||||
});
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :key="props.media?.id" v-if="cardState" @click="editSubscribeDialog">
|
||||
<VCard
|
||||
v-if="cardState"
|
||||
:key="props.media?.id"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.media?.backdrop || props.media?.poster"
|
||||
@@ -215,7 +231,11 @@ const subscribeForm = reactive({
|
||||
</template>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon size="1.9rem" :color="getTextColor()" :icon="getIcon()" />
|
||||
<VIcon
|
||||
size="1.9rem"
|
||||
:color="getTextColor()"
|
||||
:icon="getIcon()"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
@@ -224,20 +244,26 @@ const subscribeForm = reactive({
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
:color="getTextColor()"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
:key="i"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon"></VIcon>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title"></VListItemTitle>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
@@ -247,51 +273,63 @@ const subscribeForm = reactive({
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<p class="clamp-text mb-0" :class="getTextClass()">
|
||||
<p
|
||||
class="clamp-text mb-0"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.media?.description }}
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn icon="mdi-star" :color="getTextColor()" class="me-1" />
|
||||
<span class="text-subtitle-2 me-4" :class="getTextClass()">{{
|
||||
<IconBtn
|
||||
icon="mdi-star"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{
|
||||
props.media?.vote
|
||||
}}</span>
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
v-if="props.media?.total_episode"
|
||||
/>
|
||||
<span
|
||||
v-if="props.media?.season"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
v-if="props.media?.season"
|
||||
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}</span
|
||||
>
|
||||
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}</span>
|
||||
<IconBtn
|
||||
v-if="props.media?.username"
|
||||
icon="mdi-account"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
v-if="props.media?.username"
|
||||
/>
|
||||
<span
|
||||
v-if="props.media?.username"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
v-if="props.media?.username"
|
||||
>
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
|
||||
v-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
|
||||
>
|
||||
<VIcon icon="mdi-download" class="me-1" /> {{ lastUpdateText }}
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/> {{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
@@ -301,23 +339,42 @@ const subscribeForm = reactive({
|
||||
/>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog v-model="subscribeInfoDialog" max-width="1000" persistent scrollable>
|
||||
<VDialog
|
||||
v-model="subscribeInfoDialog"
|
||||
max-width="1000"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`编辑订阅 - ${props.media?.name}`">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="subscribeForm.keyword" label="搜索关键词" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.keyword"
|
||||
label="搜索关键词"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3" v-if="props.media?.type == '电视剧'">
|
||||
<VCol
|
||||
v-if="props.media?.type === '电视剧'"
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3" v-if="props.media?.type == '电视剧'">
|
||||
<VCol
|
||||
v-if="props.media?.type === '电视剧'"
|
||||
cols="12"
|
||||
md="3"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
@@ -326,13 +383,19 @@ const subscribeForm = reactive({
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.include"
|
||||
label="包含(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.exclude"
|
||||
label="排除(关键字、正则式)"
|
||||
@@ -347,16 +410,20 @@ const subscribeForm = reactive({
|
||||
chips
|
||||
label="订阅站点"
|
||||
multiple
|
||||
></VSelect>
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn @click="subscribeInfoDialog = false"> 取消 </VBtn>
|
||||
<VBtn @click="subscribeInfoDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSubscribeInfo"> 确定 </VBtn>
|
||||
<VBtn @click="updateSubscribeInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatFileSize } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import { doneNProgress, startNProgress } from "@/api/nprogress";
|
||||
import { Context } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useConfirm } from "vuetify-use-dialog";
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { Context } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -12,135 +13,152 @@ const props = defineProps({
|
||||
more: Array as PropType<Context[]>,
|
||||
width: String,
|
||||
height: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm();
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 更多来源界面
|
||||
const showMoreTorrents = ref(false);
|
||||
const showMoreTorrents = ref(false)
|
||||
|
||||
// 种子信息
|
||||
const torrent = ref(props.torrent?.torrent_info);
|
||||
const torrent = ref(props.torrent?.torrent_info)
|
||||
|
||||
// 媒体信息
|
||||
const media = ref(props.torrent?.media_info);
|
||||
const media = ref(props.torrent?.media_info)
|
||||
|
||||
// 识别元数据
|
||||
const meta = ref(props.torrent?.meta_info);
|
||||
const meta = ref(props.torrent?.meta_info)
|
||||
|
||||
// 站点图标
|
||||
const siteIcon = ref("");
|
||||
const siteIcon = ref('')
|
||||
|
||||
// 查询站点图标
|
||||
const getSiteIcon = async () => {
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get("site/icon/" + torrent?.value?.site)).data.icon;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 询问并添加下载
|
||||
const handleAddDownload = async (
|
||||
_site: any = undefined,
|
||||
async function handleAddDownload(_site: any = undefined,
|
||||
_media: any = undefined,
|
||||
_torrent: any = undefined
|
||||
) => {
|
||||
_torrent: any = undefined) {
|
||||
if (!_media || !_torrent || !_site) {
|
||||
_site = torrent.value?.site_name;
|
||||
_media = media.value;
|
||||
_torrent = torrent.value;
|
||||
_site = torrent.value?.site_name
|
||||
_media = media.value
|
||||
_torrent = torrent.value
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: "确认",
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||
confirmationText: "确认",
|
||||
cancellationText: "取消",
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (!isConfirmed) return;
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
addDownload(_media, _torrent);
|
||||
};
|
||||
addDownload(_media, _torrent)
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
const addDownload = async (_media: any, _torrent: any) => {
|
||||
startNProgress();
|
||||
async function addDownload(_media: any, _torrent: any) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post("download", {
|
||||
const result: { [key: string]: any } = await api.post('download', {
|
||||
media_in: _media,
|
||||
torrent_in: _torrent,
|
||||
});
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`);
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`);
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
doneNProgress();
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 打开种子详情页面
|
||||
const openTorrentDetail = () => {
|
||||
window.open(torrent.value?.page_url, "_blank");
|
||||
};
|
||||
function openTorrentDetail() {
|
||||
window.open(torrent.value?.page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
const downloadTorrentFile = async () => {
|
||||
window.open(torrent.value?.enclosure, "_blank");
|
||||
};
|
||||
async function downloadTorrentFile() {
|
||||
window.open(torrent.value?.enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
const getVolumeFactorClass = (downloadVolume: number, uploadVolume: number) => {
|
||||
if (downloadVolume === 0) {
|
||||
return "text-white bg-lime-500";
|
||||
} else if (downloadVolume < 1) {
|
||||
return "text-white bg-green-500";
|
||||
} else if (uploadVolume != 1) {
|
||||
return "text-white bg-sky-500";
|
||||
} else {
|
||||
return "text-white bg-gray-500";
|
||||
}
|
||||
};
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon();
|
||||
});
|
||||
getSiteIcon()
|
||||
})
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: "查看详情",
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: "mdi-information",
|
||||
prependIcon: 'mdi-information',
|
||||
click: openTorrentDetail,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "下载种子",
|
||||
title: '下载种子',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: "mdi-download",
|
||||
prependIcon: 'mdi-download',
|
||||
click: downloadTorrentFile,
|
||||
},
|
||||
},
|
||||
]);
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard :width="props.width" :height="props.height" @click="handleAddDownload">
|
||||
<template #image v-if="!showMoreTorrents">
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="handleAddDownload"
|
||||
>
|
||||
<template
|
||||
v-if="!showMoreTorrents"
|
||||
#image
|
||||
>
|
||||
<VAvatar
|
||||
class="absolute right-2 bottom-2 rounded"
|
||||
variant="flat"
|
||||
rounded="0"
|
||||
>
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
@@ -153,19 +171,25 @@ const dropdownItems = ref([
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
color="white"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
variant="plain"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon"></VIcon>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title"></VListItemTitle>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
@@ -177,23 +201,28 @@ const dropdownItems = ref([
|
||||
{{ torrent?.title }}
|
||||
</VCardText>
|
||||
<VCardText>{{ torrent?.description }}</VCardText>
|
||||
<VCardItem class="pb-3 pt-0 pe-12" v-if="torrent?.labels">
|
||||
<VCardItem
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
v-for="label in torrent?.labels"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>{{ label }}</VChip
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.edition"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ meta?.edition }}</VChip
|
||||
>
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_pix"
|
||||
variant="elevated"
|
||||
@@ -240,26 +269,27 @@ const dropdownItems = ref([
|
||||
</VCardItem>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
@click.stop="showMoreTorrents = !showMoreTorrents"
|
||||
v-if="props.more && props.more.length > 0"
|
||||
@click.stop="showMoreTorrents = !showMoreTorrents"
|
||||
>
|
||||
<template #append>
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'"></VIcon>
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
||||
</template>
|
||||
更多来源
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<VExpandTransition>
|
||||
<div v-show="showMoreTorrents">
|
||||
<VDivider></VDivider>
|
||||
<VDivider />
|
||||
<VChipGroup class="p-3">
|
||||
<VChip
|
||||
v-for="item in props.more"
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
@click.stop="
|
||||
handleAddDownload(
|
||||
item.torrent_info?.site_name,
|
||||
item.media_info,
|
||||
item.torrent_info
|
||||
item.torrent_info,
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -269,17 +299,16 @@ const dropdownItems = ref([
|
||||
:content="`↑${item.torrent_info?.seeders}`"
|
||||
inline
|
||||
size="small"
|
||||
></VBadge>
|
||||
/>
|
||||
<VBadge
|
||||
v-if="
|
||||
item.torrent_info?.downloadvolumefactor !== 1
|
||||
|| item.torrent_info?.uploadvolumefactor !== 1
|
||||
"
|
||||
:content="item.torrent_info?.volume_factor"
|
||||
inline
|
||||
size="small"
|
||||
v-if="
|
||||
item.torrent_info?.downloadvolumefactor !== 1 ||
|
||||
item.torrent_info?.uploadvolumefactor !== 1
|
||||
"
|
||||
>
|
||||
</VBadge>
|
||||
/>
|
||||
</template>
|
||||
{{ item.torrent_info.site_name }}
|
||||
</VChip>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import VerticalNavSectionTitle from "@/@layouts/components/VerticalNavSectionTitle.vue";
|
||||
import VerticalNavLayout from "@layouts/components/VerticalNavLayout.vue";
|
||||
import VerticalNavLink from "@layouts/components/VerticalNavLink.vue";
|
||||
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
|
||||
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
|
||||
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
|
||||
|
||||
// Components
|
||||
import Footer from "@/layouts/components/Footer.vue";
|
||||
import NavbarThemeSwitcher from "@/layouts/components/NavbarThemeSwitcher.vue";
|
||||
import SearchBar from "@/layouts/components/SearchBar.vue";
|
||||
import ShortcutBar from "@/layouts/components/ShortcutBar.vue";
|
||||
import UserProfile from "@/layouts/components/UserProfile.vue";
|
||||
import Footer from '@/layouts/components/Footer.vue'
|
||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +17,10 @@ import UserProfile from "@/layouts/components/UserProfile.vue";
|
||||
<template #navbar="{ toggleVerticalOverlayNavActive }">
|
||||
<div class="d-flex h-100 align-center mx-1">
|
||||
<!-- 👉 Vertical Nav Toggle -->
|
||||
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
|
||||
<IconBtn
|
||||
class="ms-n2 d-lg-none"
|
||||
@click="toggleVerticalOverlayNavActive(true)"
|
||||
>
|
||||
<VIcon icon="mdi-menu" />
|
||||
</IconBtn>
|
||||
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
// 搜索词
|
||||
const searchWord = ref<string>("");
|
||||
const searchWord = ref<string>('')
|
||||
|
||||
// 搜索弹窗
|
||||
const searchDialog = ref(false);
|
||||
const searchDialog = ref(false)
|
||||
|
||||
// Search
|
||||
const search = () => {
|
||||
if (!searchWord.value) {
|
||||
return;
|
||||
}
|
||||
searchDialog.value = false;
|
||||
function search() {
|
||||
if (!searchWord.value)
|
||||
return
|
||||
|
||||
searchDialog.value = false
|
||||
router.push({
|
||||
path: "/browse/media/search",
|
||||
path: '/browse/media/search',
|
||||
query: {
|
||||
title: searchWord.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Search Button -->
|
||||
<div class="d-flex align-center cursor-pointer" style="user-select: none">
|
||||
<VDialog v-model="searchDialog" max-width="600" transition="dialog-top-transition">
|
||||
<div
|
||||
class="d-flex align-center cursor-pointer"
|
||||
style="user-select: none"
|
||||
>
|
||||
<VDialog
|
||||
v-model="searchDialog"
|
||||
max-width="600"
|
||||
transition="dialog-top-transition"
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<IconBtn class="d-lg-none" v-bind="props">
|
||||
<IconBtn
|
||||
class="d-lg-none"
|
||||
v-bind="props"
|
||||
>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
@@ -38,14 +48,22 @@ const search = () => {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="searchWord" label="电影、电视剧名称" />
|
||||
<VTextField
|
||||
v-model="searchWord"
|
||||
label="电影、电视剧名称"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn @click="search" @keydown.enter="search"> 搜索 </VBtn>
|
||||
<VBtn
|
||||
@click="search"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
搜索
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
@@ -63,10 +81,10 @@ const search = () => {
|
||||
append-inner-icon="mdi-magnify"
|
||||
single-line
|
||||
hide-details
|
||||
@click:append-inner="search"
|
||||
@keydown.enter="search"
|
||||
flat
|
||||
rounded
|
||||
@click:append-inner="search"
|
||||
@keydown.enter="search"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import NameTestView from "@/views/system/NameTestView.vue";
|
||||
import NetTestView from "@/views/system/NetTestView.vue";
|
||||
import NameTestView from '@/views/system/NameTestView.vue'
|
||||
import NetTestView from '@/views/system/NetTestView.vue'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false);
|
||||
const appsMenu = ref(false)
|
||||
|
||||
// 名称测试弹窗
|
||||
const nameTestDialog = ref(false);
|
||||
const nameTestDialog = ref(false)
|
||||
|
||||
// 网络测试弹窗
|
||||
const netTestDialog = ref(false);
|
||||
const netTestDialog = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,7 +25,10 @@ const netTestDialog = ref(false);
|
||||
>
|
||||
<!-- Menu Activator -->
|
||||
<template #activator="{ props }">
|
||||
<IconBtn class="me-2" v-bind="props">
|
||||
<IconBtn
|
||||
class="me-2"
|
||||
v-bind="props"
|
||||
>
|
||||
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
@@ -41,25 +44,44 @@ const netTestDialog = ref(false);
|
||||
</VCardItem>
|
||||
<div class="ps ps--active-y">
|
||||
<VRow class="ma-0 mt-n1">
|
||||
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e">
|
||||
<VListItem @click="nameTestDialog = true" class="pa-4">
|
||||
<VAvatar size="48" variant="tonal">
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="nameTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">识别</h6>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
识别
|
||||
</h6>
|
||||
<span class="text-sm">名称识别测试</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
@click="() => {}"
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem @click="netTestDialog = true" class="pa-4">
|
||||
<VAvatar size="48" variant="tonal">
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="netTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-network-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">网络</h6>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
网络
|
||||
</h6>
|
||||
<span class="text-sm">测试网速连通性</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
@@ -68,7 +90,10 @@ const netTestDialog = ref(false);
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<VDialog v-model="nameTestDialog" max-width="800">
|
||||
<VDialog
|
||||
v-model="nameTestDialog"
|
||||
max-width="800"
|
||||
>
|
||||
<VCard title="名称识别测试">
|
||||
<DialogCloseBtn @click="nameTestDialog = false" />
|
||||
<VCardItem>
|
||||
@@ -77,7 +102,10 @@ const netTestDialog = ref(false);
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 网络测试弹窗 -->
|
||||
<VDialog v-model="netTestDialog" max-width="600">
|
||||
<VDialog
|
||||
v-model="netTestDialog"
|
||||
max-width="600"
|
||||
>
|
||||
<VCard title="网络测试">
|
||||
<DialogCloseBtn @click="netTestDialog = false" />
|
||||
<VCardItem>
|
||||
|
||||
@@ -1,30 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import router from "@/router";
|
||||
import { useStore } from "vuex";
|
||||
import { useStore } from 'vuex'
|
||||
import router from '@/router'
|
||||
|
||||
// Vuex Store
|
||||
const store = useStore();
|
||||
const store = useStore()
|
||||
|
||||
// 执行注销操作
|
||||
const logout = () => {
|
||||
function logout() {
|
||||
// 清除登录状态信息
|
||||
store.dispatch("auth/clearToken");
|
||||
store.dispatch('auth/clearToken')
|
||||
|
||||
// 重定向到登录页面或其他适当的页面
|
||||
router.push("/login");
|
||||
};
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const accountInfo: any = inject("accountInfo");
|
||||
const accountInfo: any = inject('accountInfo')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VBadge dot location="bottom right" offset-x="3" offset-y="3" color="success" bordered>
|
||||
<VAvatar class="cursor-pointer" color="primary" variant="tonal">
|
||||
<VBadge
|
||||
dot
|
||||
location="bottom right"
|
||||
offset-x="3"
|
||||
offset-y="3"
|
||||
color="success"
|
||||
bordered
|
||||
>
|
||||
<VAvatar
|
||||
class="cursor-pointer"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<VImg :src="accountInfo.avatar" />
|
||||
|
||||
<!-- SECTION Menu -->
|
||||
<VMenu activator="parent" width="230" location="bottom end" offset="14px">
|
||||
<VMenu
|
||||
activator="parent"
|
||||
width="230"
|
||||
location="bottom end"
|
||||
offset="14px"
|
||||
>
|
||||
<VList>
|
||||
<!-- 👉 User Avatar & Name -->
|
||||
<VListItem>
|
||||
@@ -37,7 +53,10 @@ const accountInfo: any = inject("accountInfo");
|
||||
offset-y="3"
|
||||
color="success"
|
||||
>
|
||||
<VAvatar color="primary" variant="tonal">
|
||||
<VAvatar
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<VImg :src="accountInfo.avatar" />
|
||||
</VAvatar>
|
||||
</VBadge>
|
||||
@@ -52,9 +71,16 @@ const accountInfo: any = inject("accountInfo");
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Profile -->
|
||||
<VListItem link to="account-setting">
|
||||
<VListItem
|
||||
link
|
||||
to="account-setting"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="mdi-account-outline"
|
||||
size="22"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>设定</VListItemTitle>
|
||||
@@ -66,7 +92,11 @@ const accountInfo: any = inject("accountInfo");
|
||||
target="_blank"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="mdi-help-circle-outline"
|
||||
size="22"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>帮助</VListItemTitle>
|
||||
@@ -78,7 +108,11 @@ const accountInfo: any = inject("accountInfo");
|
||||
<!-- 👉 Logout -->
|
||||
<VListItem @click="logout">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-logout" size="22" />
|
||||
<VIcon
|
||||
class="me-2"
|
||||
icon="mdi-logout"
|
||||
size="22"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>注销</VListItemTitle>
|
||||
|
||||
19
src/main.ts
19
src/main.ts
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable import/order */
|
||||
import { createApp } from 'vue'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import App from '@/App.vue'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import App from '@/App.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import { loadFonts } from '@/plugins/webfontloader'
|
||||
import router from '@/router'
|
||||
@@ -9,10 +11,8 @@ import store from '@/store'
|
||||
import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import { createApp } from 'vue'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import 'vue-toast-notification/dist/theme-default.css'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
|
||||
loadFonts()
|
||||
|
||||
// Nprogress
|
||||
@@ -22,14 +22,7 @@ configureNProgress()
|
||||
const app = createApp(App)
|
||||
|
||||
// Use plugins Mount vue app
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(ToastPlugin)
|
||||
.use(VuetifyUseDialog)
|
||||
.mount('#app')
|
||||
|
||||
app.use(vuetify).use(router).use(store).use(ToastPlugin).use(VuetifyUseDialog).mount('#app')
|
||||
|
||||
// 导航守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import NoDataFound from '@/components/NoDataFound.vue';
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NoDataFound
|
||||
errorCode="404"
|
||||
errorTitle="页面不存在 ⚠️"
|
||||
errorDescription="您想要访问的页面不存在,请检查地址是否正确。"
|
||||
error-code="404"
|
||||
error-title="页面不存在 ⚠️"
|
||||
error-description="您想要访问的页面不存在,请检查地址是否正确。"
|
||||
>
|
||||
<template #button>
|
||||
<VBtn
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import AccountSettingAccount from "@/views/account-setting/AccountSettingAccount.vue";
|
||||
import AccountSettingNotification from "@/views/account-setting/AccountSettingNotification.vue";
|
||||
import AccountSettingRule from "@/views/account-setting/AccountSettingRule.vue";
|
||||
import AccountSettingSite from "@/views/account-setting/AccountSettingSite.vue";
|
||||
import AccountSettingWords from "@/views/account-setting/AccountSettingWords.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute } from 'vue-router'
|
||||
import AccountSettingAccount from '@/views/account-setting/AccountSettingAccount.vue'
|
||||
import AccountSettingNotification from '@/views/account-setting/AccountSettingNotification.vue'
|
||||
import AccountSettingRule from '@/views/account-setting/AccountSettingRule.vue'
|
||||
import AccountSettingSite from '@/views/account-setting/AccountSettingSite.vue'
|
||||
import AccountSettingWords from '@/views/account-setting/AccountSettingWords.vue'
|
||||
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
|
||||
const activeTab = ref(route.params.tab);
|
||||
const activeTab = ref(route.params.tab)
|
||||
|
||||
// tabs
|
||||
const tabs = [
|
||||
{ title: "用户", icon: "mdi-account", tab: "account" },
|
||||
{ title: "站点", icon: "mdi-web", tab: "site" },
|
||||
{ title: "规则", icon: "mdi-filter-cog", tab: "filter" },
|
||||
{ title: "通知", icon: "mdi-bell", tab: "notification" },
|
||||
{ title: "自定义词表", icon: "mdi-file-word-box", tab: "words" },
|
||||
];
|
||||
{ title: '用户', icon: 'mdi-account', tab: 'account' },
|
||||
{ title: '站点', icon: 'mdi-web', tab: 'site' },
|
||||
{ title: '规则', icon: 'mdi-filter-cog', tab: 'filter' },
|
||||
{ title: '通知', icon: 'mdi-bell', tab: 'notification' },
|
||||
{ title: '自定义词表', icon: 'mdi-file-word-box', tab: 'words' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab" show-arrows>
|
||||
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
|
||||
<VIcon size="20" start :icon="item.icon" />
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
show-arrows
|
||||
>
|
||||
<VTab
|
||||
v-for="item in tabs"
|
||||
:key="item.icon"
|
||||
:value="item.tab"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
start
|
||||
:icon="item.icon"
|
||||
/>
|
||||
{{ item.title }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VDivider />
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition">
|
||||
<VWindow
|
||||
v-model="activeTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
>
|
||||
<!-- Account -->
|
||||
<VWindowItem value="account">
|
||||
<AccountSettingAccount />
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import MediaCardListView from "@/views/discover/MediaCardListView.vue";
|
||||
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: Array as PropType<string[]> | PropType<string>,
|
||||
});
|
||||
})
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
|
||||
// 面包屑标题定义
|
||||
const titles: { [key: string]: any } = {
|
||||
tmdb: {
|
||||
trending: "流行趋势",
|
||||
movies: "热门电影",
|
||||
tvs: "热门电视剧",
|
||||
trending: '流行趋势',
|
||||
movies: '热门电影',
|
||||
tvs: '热门电视剧',
|
||||
},
|
||||
douban: {
|
||||
movies: "最新电影",
|
||||
tvs: "最新电视剧",
|
||||
tv_weekly_chinese: "华语剧集榜",
|
||||
tv_weekly_global: "全球剧集榜",
|
||||
movie_top250: "电影TOP250",
|
||||
movies: '最新电影',
|
||||
tvs: '最新电视剧',
|
||||
tv_weekly_chinese: '华语剧集榜',
|
||||
tv_weekly_global: '全球剧集榜',
|
||||
movie_top250: '电影TOP250',
|
||||
},
|
||||
media: {
|
||||
search: "搜索",
|
||||
search: '搜索',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 计算API路径
|
||||
const getApiPath = (types: string[] | string) => {
|
||||
if (Array.isArray(types)) {
|
||||
return types.join("/");
|
||||
} else {
|
||||
return types;
|
||||
}
|
||||
};
|
||||
function getApiPath(types: string[] | string) {
|
||||
if (Array.isArray(types))
|
||||
return types.join('/')
|
||||
else
|
||||
return types
|
||||
}
|
||||
|
||||
// 面包屑标题
|
||||
const getTitle = (types: string[] | string, title: any = "") => {
|
||||
function getTitle(types: string[] | string, title: any = '') {
|
||||
if (Array.isArray(types)) {
|
||||
if (title) {
|
||||
return [titles[types[0]][types[1]], title];
|
||||
}
|
||||
return ["推荐", titles[types[0]][types[1]]];
|
||||
} else {
|
||||
return ["发现"];
|
||||
if (title)
|
||||
return [titles[types[0]][types[1]], title]
|
||||
|
||||
return ['推荐', titles[types[0]][types[1]]]
|
||||
}
|
||||
};
|
||||
else {
|
||||
return ['发现']
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VBreadcrumbs :items="getTitle(props.type || '', route.query?.title)"></VBreadcrumbs>
|
||||
<MediaCardListView :apipath="getApiPath(props.type || '')" :params="route.query" />
|
||||
<VBreadcrumbs :items="getTitle(props.type || '', route.query?.title)" />
|
||||
<MediaCardListView
|
||||
:apipath="getApiPath(props.type || '')"
|
||||
:params="route.query"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import FullCalendarView from '@/views/subscribe/FullCalendarView.vue';
|
||||
|
||||
import FullCalendarView from '@/views/subscribe/FullCalendarView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FullCalendarView/>
|
||||
<FullCalendarView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import AnalyticsMediaStatistic from "@/views/dashboard/AnalyticsMediaStatistic.vue";
|
||||
import AnalyticsProcesses from "@/views/dashboard/AnalyticsProcesses.vue";
|
||||
import AnalyticsScheduler from "@/views/dashboard/AnalyticsScheduler.vue";
|
||||
import AnalyticsSpeed from "@/views/dashboard/AnalyticsSpeed.vue";
|
||||
import AnalyticsStorage from "@/views/dashboard/AnalyticsStorage.vue";
|
||||
import AnalyticsWeeklyOverview from "@/views/dashboard/AnalyticsWeeklyOverview.vue";
|
||||
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
|
||||
import AnalyticsProcesses from '@/views/dashboard/AnalyticsProcesses.vue'
|
||||
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
|
||||
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
|
||||
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsStorage />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="8">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<AnalyticsMediaStatistic />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsWeeklyOverview />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsSpeed />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<AnalyticsScheduler />
|
||||
</VCol>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import DownloadingListView from "@/views/reorganize/DownloadingListView.vue";
|
||||
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import TransferHistoryView from '@/views/reorganize/TransferHistoryView.vue';
|
||||
|
||||
import TransferHistoryView from '@/views/reorganize/TransferHistoryView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,105 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { requiredValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import router from "@/router";
|
||||
import logo from "@images/logo.svg?raw";
|
||||
import type { VForm } from "vuetify/components/VForm";
|
||||
import { useStore } from "vuex";
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useStore } from 'vuex'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import router from '@/router'
|
||||
import logo from '@images/logo.svg?raw'
|
||||
|
||||
// Vuex Store
|
||||
const store = useStore();
|
||||
const store = useStore()
|
||||
|
||||
// 表单
|
||||
const form = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
username: '',
|
||||
password: '',
|
||||
remember: true,
|
||||
});
|
||||
const refForm = ref<InstanceType<typeof VForm> | null>(null);
|
||||
})
|
||||
|
||||
const refForm = ref<InstanceType<typeof VForm> | null>(null)
|
||||
|
||||
// 密码输入
|
||||
const isPasswordVisible = ref(false);
|
||||
const isPasswordVisible = ref(false)
|
||||
|
||||
// 错误信息
|
||||
const errorMessage = ref("");
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 背景图片
|
||||
const backgroundImageUrl = ref("");
|
||||
const backgroundImageUrl = ref('')
|
||||
|
||||
// 背景图片加载状态
|
||||
const isImageLoaded = ref(false);
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 获取背景图片
|
||||
const fetchBackgroundImage = async () => {
|
||||
async function fetchBackgroundImage() {
|
||||
api
|
||||
.get("/login/tmdb")
|
||||
.get('/login/tmdb')
|
||||
.then((response: any) => {
|
||||
backgroundImageUrl.value = response.message;
|
||||
backgroundImageUrl.value = response.message
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 登录获取token事件
|
||||
const login = () => {
|
||||
errorMessage.value = "";
|
||||
function login() {
|
||||
errorMessage.value = ''
|
||||
|
||||
// 进行表单校验
|
||||
if (!form.value.username || !form.value.password) {
|
||||
return;
|
||||
}
|
||||
if (!form.value.username || !form.value.password)
|
||||
return
|
||||
|
||||
// 用户名密码
|
||||
const formData = new FormData();
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append("username", form.value.username);
|
||||
formData.append("password", form.value.password);
|
||||
formData.append('username', form.value.username)
|
||||
formData.append('password', form.value.password)
|
||||
|
||||
// 请求token
|
||||
api
|
||||
.post("/login/access-token", formData, {
|
||||
.post('/login/access-token', formData, {
|
||||
headers: {
|
||||
Accept: "application/json", // 设置 Accept 类型
|
||||
Accept: 'application/json', // 设置 Accept 类型
|
||||
},
|
||||
})
|
||||
.then((response: any) => {
|
||||
// 获取token
|
||||
const token = response.access_token;
|
||||
const token = response.access_token
|
||||
|
||||
// 更新token和remember状态到Vuex Store
|
||||
store.dispatch("auth/updateToken", token);
|
||||
store.dispatch("auth/updateRemember", form.value.remember);
|
||||
store.dispatch('auth/updateToken', token)
|
||||
store.dispatch('auth/updateRemember', form.value.remember)
|
||||
|
||||
// 跳转到首页
|
||||
router.push("/");
|
||||
router.push('/')
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 登录失败,显示错误提示
|
||||
if (!error.response) errorMessage.value = "登录失败,请检查网络连接";
|
||||
if (!error.response)
|
||||
errorMessage.value = '登录失败,请检查网络连接'
|
||||
else if (error.response.status === 401)
|
||||
errorMessage.value = "登录失败,请检查用户名和密码是否正确";
|
||||
errorMessage.value = '登录失败,请检查用户名和密码是否正确'
|
||||
else if (error.response.status === 403)
|
||||
errorMessage.value = "登录失败,您没有权限访问";
|
||||
else if (error.response.status === 500) errorMessage.value = "登录失败,服务器错误";
|
||||
errorMessage.value = '登录失败,您没有权限访问'
|
||||
else if (error.response.status === 500)
|
||||
errorMessage.value = '登录失败,服务器错误'
|
||||
else
|
||||
errorMessage.value = `登录失败 ${error.response.status},请检查用户名和密码是否正确`;
|
||||
});
|
||||
};
|
||||
errorMessage.value = `登录失败 ${error.response.status},请检查用户名和密码是否正确`
|
||||
})
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
onMounted(() => {
|
||||
// 从Vuex Store中获取token和remember状态
|
||||
const token = store.state.auth.token;
|
||||
const remember = store.state.auth.remember;
|
||||
const token = store.state.auth.token
|
||||
const remember = store.state.auth.remember
|
||||
|
||||
// 如果token存在,且保持登录状态为true,则跳转到首页
|
||||
if (token && remember) {
|
||||
router.push("/");
|
||||
} else {
|
||||
// 获取背景图片
|
||||
fetchBackgroundImage();
|
||||
router.push('/')
|
||||
}
|
||||
});
|
||||
else {
|
||||
// 获取背景图片
|
||||
fetchBackgroundImage()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,8 +111,8 @@ onMounted(() => {
|
||||
aspect-ratio="4/3"
|
||||
:src="backgroundImageUrl"
|
||||
class="w-full h-full overflow-hidden"
|
||||
@load="isImageLoaded = true"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4">
|
||||
<VCard
|
||||
@@ -130,7 +134,10 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}" ref="refForm">
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent="() => {}"
|
||||
>
|
||||
<VRow>
|
||||
<!-- username -->
|
||||
<VCol cols="12">
|
||||
@@ -155,19 +162,30 @@ onMounted(() => {
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
|
||||
<div v-if="errorMessage" class="text-error mt-1">
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="text-error mt-1"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- remember me checkbox -->
|
||||
<div
|
||||
class="d-flex align-center justify-space-between flex-wrap mt-1 mb-4"
|
||||
>
|
||||
<VCheckbox v-model="form.remember" label="保持登录" required />
|
||||
<div class="d-flex align-center justify-space-between flex-wrap mt-1 mb-4">
|
||||
<VCheckbox
|
||||
v-model="form.remember"
|
||||
label="保持登录"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login"> 登录 </VBtn>
|
||||
<VBtn
|
||||
block
|
||||
type="submit"
|
||||
@click="login"
|
||||
>
|
||||
登录
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PluginCardListView from "@/views/setting/PluginCardListView.vue";
|
||||
import PluginCardListView from '@/views/setting/PluginCardListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MediaCardSlideView apipath="tmdb/trending">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/tmdb/trending" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/tmdb/trending"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>流行趋势</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,10 +26,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="tmdb/movies">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/tmdb/movies" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/tmdb/movies"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>热门电影</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -28,10 +46,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="tmdb/tvs">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/tmdb/tvs" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/tmdb/tvs"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>热门电视剧</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -39,10 +66,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="douban/movies">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/douban/movies" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/douban/movies"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>最新电影</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -50,10 +86,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="douban/tvs">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/douban/tvs" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/douban/tvs"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>最新电视剧</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,10 +106,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="douban/movie_top250">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/douban/movie_top250" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/douban/movie_top250"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>电影TOP250</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -72,10 +126,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="douban/tv_weekly_chinese">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/douban/tv_weekly_chinese" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/douban/tv_weekly_chinese"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>国产剧集榜</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,10 +146,19 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue';
|
||||
|
||||
<MediaCardSlideView apipath="douban/tv_weekly_global">
|
||||
<template #title="{ loaded }">
|
||||
<div class="slider-header mt-3 ms-1" v-if="loaded">
|
||||
<RouterLink to="/browse/douban/tv_weekly_global" class="slider-title">
|
||||
<div
|
||||
v-if="loaded"
|
||||
class="slider-header mt-3 ms-1"
|
||||
>
|
||||
<RouterLink
|
||||
to="/browse/douban/tv_weekly_global"
|
||||
class="slider-title"
|
||||
>
|
||||
<span>全球剧集榜</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
<VIcon
|
||||
icon="mdi-arrow-right-circle-outline"
|
||||
class="ms-1"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import TorrentCardListView from "@/views/discover/TorrentCardListView.vue";
|
||||
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
|
||||
|
||||
// 路由参数
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
|
||||
// 查询TMDBID或标题
|
||||
const keyword = route.query?.keyword?.toString() ?? "";
|
||||
const keyword = route.query?.keyword?.toString() ?? ''
|
||||
|
||||
// 查询类型
|
||||
const type = route.query?.type?.toString() ?? "";
|
||||
const type = route.query?.type?.toString() ?? ''
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TorrentCardListView :keyword="keyword" :type="type" />
|
||||
<TorrentCardListView
|
||||
:keyword="keyword"
|
||||
:type="type"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import SiteListView from "@/views/site/SiteCardListView.vue";
|
||||
import SiteListView from '@/views/site/SiteCardListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue';
|
||||
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SubscribeListView type="电影"/>
|
||||
<SubscribeListView type="电影" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue';
|
||||
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SubscribeListView type="电视剧"/>
|
||||
<SubscribeListView type="电视剧" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@ const alertTypeIcon = {
|
||||
const modifiedAliases = Object.assign(aliases, alertTypeIcon)
|
||||
|
||||
export const iconify = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
component: (props: any) => h(Icon, props),
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as labsComponents from 'vuetify/labs/components'
|
||||
import defaults from './defaults'
|
||||
import { icons } from './icons'
|
||||
import theme from './theme'
|
||||
|
||||
// Styles
|
||||
import '@core/scss/template/libs/vuetify/index.scss'
|
||||
import 'vuetify/styles'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
scrollBehavior() {
|
||||
// 始终滚动到顶部
|
||||
return { top: 0 }
|
||||
},
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Module } from 'vuex';
|
||||
import type { Module } from 'vuex'
|
||||
|
||||
// 定义状态类型
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
remember: boolean;
|
||||
token: string | null
|
||||
remember: boolean
|
||||
}
|
||||
|
||||
// 定义根状态类型
|
||||
interface RootState {
|
||||
auth: AuthState;
|
||||
auth: AuthState
|
||||
}
|
||||
|
||||
|
||||
// 导出模块
|
||||
const authModule: Module<AuthState, RootState> = {
|
||||
namespaced: true,
|
||||
@@ -21,30 +20,30 @@ const authModule: Module<AuthState, RootState> = {
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token: string) {
|
||||
state.token = token;
|
||||
state.token = token
|
||||
},
|
||||
clearToken(state) {
|
||||
state.token = null;
|
||||
state.token = null
|
||||
},
|
||||
setRemember(state, remember: boolean) {
|
||||
state.remember = remember;
|
||||
state.remember = remember
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
updateToken({ commit }, token: string) {
|
||||
commit('setToken', token);
|
||||
commit('setToken', token)
|
||||
},
|
||||
clearToken({ commit },) {
|
||||
commit('clearToken');
|
||||
clearToken({ commit }) {
|
||||
commit('clearToken')
|
||||
},
|
||||
updateRemember({ commit }, remember: boolean) {
|
||||
commit('setRemember', remember);
|
||||
commit('setRemember', remember)
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
getToken: state => state.token,
|
||||
getRemember: state => state.remember,
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
export default authModule;
|
||||
export default authModule
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStore } from 'vuex';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
import authModule from './auth';
|
||||
import { createStore } from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
import authModule from './auth'
|
||||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
@@ -14,6 +14,6 @@ const store = createStore({
|
||||
key: 'moviepilot', // 存储的键名
|
||||
}),
|
||||
],
|
||||
});
|
||||
})
|
||||
|
||||
export default store;
|
||||
export default store
|
||||
|
||||
@@ -1,159 +1,173 @@
|
||||
<script lang="ts" setup>
|
||||
import { requiredValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import { User } from "@/api/types";
|
||||
import avatar1 from "@images/avatars/avatar-1.png";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
const isNewPasswordVisible = ref(false);
|
||||
const isConfirmPasswordVisible = ref(false);
|
||||
const isPasswordVisible = ref(false);
|
||||
const newPassword = ref("");
|
||||
const confirmPassword = ref("");
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { User } from '@/api/types'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
|
||||
const isNewPasswordVisible = ref(false)
|
||||
const isConfirmPasswordVisible = ref(false)
|
||||
const isPasswordVisible = ref(false)
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
const refInputEl = ref<HTMLElement>();
|
||||
const refInputEl = ref<HTMLElement>()
|
||||
|
||||
// 新增用户窗口
|
||||
const addUserDialog = ref(false);
|
||||
const addUserDialog = ref(false)
|
||||
|
||||
// 新增用户表单
|
||||
const userForm = reactive({
|
||||
name: "",
|
||||
password: "",
|
||||
email: "",
|
||||
});
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
// 当前用户信息
|
||||
const accountInfo = ref<User>({
|
||||
id: 0,
|
||||
name: "",
|
||||
password: "",
|
||||
email: "",
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
avatar: "",
|
||||
});
|
||||
avatar: '',
|
||||
})
|
||||
|
||||
// 所有用户信息
|
||||
const allUsers = ref<User[]>([]);
|
||||
const allUsers = ref<User[]>([])
|
||||
|
||||
// changeAvatar function
|
||||
const changeAvatar = (file: Event) => {
|
||||
const fileReader = new FileReader();
|
||||
const { files } = file.target as HTMLInputElement;
|
||||
function changeAvatar(file: Event) {
|
||||
const fileReader = new FileReader()
|
||||
const { files } = file.target as HTMLInputElement
|
||||
|
||||
if (files && files.length > 0) {
|
||||
fileReader.readAsDataURL(files[0]);
|
||||
fileReader.readAsDataURL(files[0])
|
||||
fileReader.onload = () => {
|
||||
if (typeof fileReader.result === "string") {
|
||||
accountInfo.value.avatar = fileReader.result;
|
||||
saveAccountInfo();
|
||||
if (typeof fileReader.result === 'string') {
|
||||
accountInfo.value.avatar = fileReader.result
|
||||
saveAccountInfo()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// reset avatar image
|
||||
const resetAvatar = () => {
|
||||
accountInfo.value.avatar = avatar1;
|
||||
};
|
||||
function resetAvatar() {
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
|
||||
// 调用API,加载当前用户数据
|
||||
const loadAccountInfo = async () => {
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const user: User = await api.get(`user/current`);
|
||||
accountInfo.value = user;
|
||||
if (!accountInfo.value.avatar) accountInfo.value.avatar = avatar1;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const user: User = await api.get('user/current')
|
||||
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar)
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
const saveAccountInfo = async () => {
|
||||
async function saveAccountInfo() {
|
||||
if (newPassword.value || confirmPassword.value) {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
$toast.error("两次输入的密码不一致");
|
||||
return;
|
||||
$toast.error('两次输入的密码不一致')
|
||||
|
||||
return
|
||||
}
|
||||
accountInfo.value.password = newPassword.value;
|
||||
accountInfo.value.password = newPassword.value
|
||||
}
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put(`user`, accountInfo.value);
|
||||
if (result.success) {
|
||||
$toast.success("用户信息保存成功!");
|
||||
} else {
|
||||
$toast.error(`用户信息保存失败:${result.message}!`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const result: { [key: string]: any } = await api.put('user', accountInfo.value)
|
||||
if (result.success)
|
||||
$toast.success('用户信息保存成功!')
|
||||
else
|
||||
$toast.error(`用户信息保存失败:${result.message}!`)
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API,查询所有用户
|
||||
const loadAllUsers = async () => {
|
||||
async function loadAllUsers() {
|
||||
try {
|
||||
const result: User[] = await api.get(`/user`);
|
||||
allUsers.value = result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const result: User[] = await api.get('/user')
|
||||
|
||||
allUsers.value = result
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (user: User) => {
|
||||
async function deleteUser(user: User) {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`user/${user.name}`);
|
||||
const result: { [key: string]: any } = await api.delete(`user/${user.name}`)
|
||||
if (result.success) {
|
||||
$toast.success("用户删除成功!");
|
||||
loadAllUsers();
|
||||
} else {
|
||||
$toast.error(`用户删除失败:${result.message}!`);
|
||||
$toast.success('用户删除成功!')
|
||||
loadAllUsers()
|
||||
}
|
||||
else {
|
||||
$toast.error(`用户删除失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 冻结用户
|
||||
const deactivateUser = async (user: User) => {
|
||||
async function deactivateUser(user: User) {
|
||||
try {
|
||||
user.is_active = !user.is_active;
|
||||
const result: { [key: string]: any } = await api.put(`user`, user);
|
||||
user.is_active = !user.is_active
|
||||
|
||||
const result: { [key: string]: any } = await api.put('user', user)
|
||||
if (result.success) {
|
||||
$toast.success("用户冻结成功!");
|
||||
loadAllUsers();
|
||||
} else {
|
||||
$toast.error(`用户冻结失败:${result.message}!`);
|
||||
$toast.success('用户冻结成功!')
|
||||
loadAllUsers()
|
||||
}
|
||||
else {
|
||||
$toast.error(`用户冻结失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
const addUser = async () => {
|
||||
async function addUser() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(`user`, userForm);
|
||||
const result: { [key: string]: any } = await api.post('user', userForm)
|
||||
if (result.success) {
|
||||
$toast.success("用户新增成功!");
|
||||
loadAllUsers();
|
||||
addUserDialog.value = false;
|
||||
} else {
|
||||
$toast.error(`用户新增失败:${result.message}!`);
|
||||
$toast.success('用户新增成功!')
|
||||
loadAllUsers()
|
||||
addUserDialog.value = false
|
||||
}
|
||||
else {
|
||||
$toast.error(`用户新增失败:${result.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载当前用户数据
|
||||
onMounted(() => {
|
||||
loadAccountInfo();
|
||||
loadAllUsers();
|
||||
});
|
||||
loadAccountInfo()
|
||||
loadAllUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -162,13 +176,24 @@ onMounted(() => {
|
||||
<VCard title="个人信息">
|
||||
<VCardText class="d-flex">
|
||||
<!-- 👉 Avatar -->
|
||||
<VAvatar rounded="lg" size="100" class="me-6" :image="accountInfo.avatar" />
|
||||
<VAvatar
|
||||
rounded="lg"
|
||||
size="100"
|
||||
class="me-6"
|
||||
:image="accountInfo.avatar"
|
||||
/>
|
||||
|
||||
<!-- 👉 Upload Photo -->
|
||||
<form class="d-flex flex-column justify-center gap-5">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VBtn color="primary" @click="refInputEl?.click()">
|
||||
<VIcon icon="mdi-cloud-upload-outline" class="d-sm-none" />
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="refInputEl?.click()"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-cloud-upload-outline"
|
||||
class="d-sm-none"
|
||||
/>
|
||||
<span class="d-none d-sm-block">上传头像</span>
|
||||
</VBtn>
|
||||
|
||||
@@ -179,15 +204,25 @@ onMounted(() => {
|
||||
accept=".jpeg,.png,.jpg,GIF"
|
||||
hidden
|
||||
@input="changeAvatar"
|
||||
/>
|
||||
>
|
||||
|
||||
<VBtn type="reset" color="error" variant="tonal" @click="resetAvatar">
|
||||
<VBtn
|
||||
type="reset"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="resetAvatar"
|
||||
>
|
||||
<span class="d-none d-sm-block">重置</span>
|
||||
<VIcon icon="mdi-refresh" class="d-sm-none" />
|
||||
<VIcon
|
||||
icon="mdi-refresh"
|
||||
class="d-sm-none"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<p class="text-body-1 mb-0">允许 JPG、GIF 或 PNG 格式, 最大尽寸 800K。</p>
|
||||
<p class="text-body-1 mb-0">
|
||||
允许 JPG、GIF 或 PNG 格式, 最大尽寸 800K。
|
||||
</p>
|
||||
</form>
|
||||
</VCardText>
|
||||
|
||||
@@ -198,16 +233,33 @@ onMounted(() => {
|
||||
<VForm class="mt-6">
|
||||
<VRow>
|
||||
<!-- 👉 Name -->
|
||||
<VCol md="6" cols="12">
|
||||
<VTextField readonly v-model="accountInfo.name" label="用户名" />
|
||||
<VCol
|
||||
md="6"
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountInfo.name"
|
||||
readonly
|
||||
label="用户名"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Email -->
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="accountInfo.email" label="邮箱" type="email" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="accountInfo.email"
|
||||
label="邮箱"
|
||||
type="email"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<!-- 👉 new password -->
|
||||
<VTextField
|
||||
v-model="newPassword"
|
||||
@@ -216,12 +268,15 @@ onMounted(() => {
|
||||
isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
|
||||
"
|
||||
label="新密码"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
autocomplete="new-password"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<!-- 👉 confirm password -->
|
||||
<VTextField
|
||||
v-model="confirmPassword"
|
||||
@@ -237,8 +292,13 @@ onMounted(() => {
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Form Actions -->
|
||||
<VCol cols="12" class="d-flex flex-wrap gap-4">
|
||||
<VBtn @click="saveAccountInfo">保存</VBtn>
|
||||
<VCol
|
||||
cols="12"
|
||||
class="d-flex flex-wrap gap-4"
|
||||
>
|
||||
<VBtn @click="saveAccountInfo">
|
||||
保存
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
@@ -246,7 +306,10 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" v-if="accountInfo.is_superuser">
|
||||
<VCol
|
||||
v-if="accountInfo.is_superuser"
|
||||
cols="12"
|
||||
>
|
||||
<!-- 👉 Accounts -->
|
||||
<VCard title="所有用户">
|
||||
<template #append>
|
||||
@@ -257,38 +320,70 @@ onMounted(() => {
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">用户名</th>
|
||||
<th scope="col">邮箱</th>
|
||||
<th scope="col">状态</th>
|
||||
<th scope="col">管理员</th>
|
||||
<th scope="col" class="w-5"></th>
|
||||
<th scope="col">
|
||||
用户名
|
||||
</th>
|
||||
<th scope="col">
|
||||
邮箱
|
||||
</th>
|
||||
<th scope="col">
|
||||
状态
|
||||
</th>
|
||||
<th scope="col">
|
||||
管理员
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="w-5"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in allUsers" :key="user.name">
|
||||
<tr
|
||||
v-for="user in allUsers"
|
||||
:key="user.name"
|
||||
>
|
||||
<td>
|
||||
{{ user.name }}
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<VChip color="success" text-color="white" v-if="user.is_active"
|
||||
>激活</VChip
|
||||
<VChip
|
||||
v-if="user.is_active"
|
||||
color="success"
|
||||
text-color="white"
|
||||
>
|
||||
<VChip color="error" text-color="white" v-else>冻结</VChip>
|
||||
激活
|
||||
</VChip>
|
||||
<VChip
|
||||
v-else
|
||||
color="error"
|
||||
text-color="white"
|
||||
>
|
||||
冻结
|
||||
</VChip>
|
||||
</td>
|
||||
<td>{{ user.is_superuser ? "是" : "否" }}</td>
|
||||
<td>
|
||||
<IconBtn v-show="!user.is_superuser">
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="deactivateUser(user)">
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="deactivateUser(user)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-lock"></VIcon>
|
||||
<VIcon icon="mdi-lock" />
|
||||
</template>
|
||||
<VListItemTitle>{{
|
||||
user.is_active ? "冻结" : "解冻"
|
||||
}}</VListItemTitle>
|
||||
<VListItemTitle>
|
||||
{{
|
||||
user.is_active ? "冻结" : "解冻"
|
||||
}}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@@ -296,7 +391,7 @@ onMounted(() => {
|
||||
@click="deleteUser(user)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-delete"></VIcon>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</template>
|
||||
<VListItemTitle>删除</VListItemTitle>
|
||||
</VListItem>
|
||||
@@ -311,20 +406,30 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog v-model="addUserDialog" max-width="800" persistent>
|
||||
<VDialog
|
||||
v-model="addUserDialog"
|
||||
max-width="800"
|
||||
persistent
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="新增用户">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userForm.name"
|
||||
label="用户名"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userForm.password"
|
||||
label="密码"
|
||||
@@ -336,16 +441,26 @@ onMounted(() => {
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="userForm.email" label="邮箱" />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userForm.email"
|
||||
label="邮箱"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="addUserDialog = false"> 取消 </VBtn>
|
||||
<VBtn @click="addUserDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="addUser"> 确定 </VBtn>
|
||||
<VBtn @click="addUser">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,44 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { NotificationSwitch } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { NotificationSwitch } from '@/api/types'
|
||||
|
||||
const messagemTypes = ref<NotificationSwitch[]>([]);
|
||||
const messagemTypes = ref<NotificationSwitch[]>([])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 调用API查询消息开关
|
||||
const loadNotificationSwitchs = async () => {
|
||||
async function loadNotificationSwitchs() {
|
||||
try {
|
||||
const result: NotificationSwitch[] = await api.get("message/switchs");
|
||||
messagemTypes.value = result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const result: NotificationSwitch[] = await api.get('message/switchs')
|
||||
|
||||
messagemTypes.value = result
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存消息开关
|
||||
const saveNotificationSwitchs = async () => {
|
||||
async function saveNotificationSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"message/switchs",
|
||||
messagemTypes.value
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("保存通知消息设置成功");
|
||||
} else {
|
||||
$toast.error("保存通知消息设置失败!");
|
||||
}
|
||||
messagemTypes.value = messagemTypes.value;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'message/switchs',
|
||||
messagemTypes.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存通知消息设置成功')
|
||||
else
|
||||
$toast.error('保存通知消息设置失败!')
|
||||
|
||||
// messagemTypes.value = messagemTypes.value
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadNotificationSwitchs();
|
||||
});
|
||||
loadNotificationSwitchs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,14 +52,25 @@ onMounted(() => {
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">消息类型</th>
|
||||
<th scope="col">微信</th>
|
||||
<th scope="col">Telegram</th>
|
||||
<th scope="col">Slack</th>
|
||||
<th scope="col">
|
||||
消息类型
|
||||
</th>
|
||||
<th scope="col">
|
||||
微信
|
||||
</th>
|
||||
<th scope="col">
|
||||
Telegram
|
||||
</th>
|
||||
<th scope="col">
|
||||
Slack
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="message in messagemTypes" :key="message.mtype">
|
||||
<tr
|
||||
v-for="message in messagemTypes"
|
||||
:key="message.mtype"
|
||||
>
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
@@ -70,7 +85,12 @@ onMounted(() => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td colspan="4" class="text-center">没有设置任何通知渠道</td>
|
||||
<td
|
||||
colspan="4"
|
||||
class="text-center"
|
||||
>
|
||||
没有设置任何通知渠道
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
@@ -79,7 +99,12 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveNotificationSwitchs"> 保存 </VBtn>
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSwitchs"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -1,152 +1,164 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import FilterRuleCard from "@/components/cards/FilterRuleCard.vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
|
||||
// 优先级
|
||||
pri: string;
|
||||
pri: string
|
||||
|
||||
// 已选规则
|
||||
rules: string[];
|
||||
rules: string[]
|
||||
|
||||
// 是否可见
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>("seeder");
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: "站点优先", value: "site" },
|
||||
{ title: "做种数优先", value: "seeder" },
|
||||
];
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 规则卡片列表
|
||||
const filterCards = ref<FilterCard[]>([])
|
||||
|
||||
// 查询已设置过滤规则
|
||||
const queryCustomFilters = async () => {
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get("system/setting/FilterRules");
|
||||
const result: { [key: string]: any } = await api.get('system/setting/FilterRules')
|
||||
if (result.success) {
|
||||
// 保存的是个字符串,需要分割成数组
|
||||
const groups = result.data?.value.split(">");
|
||||
const groups = result.data?.value.split('>')
|
||||
|
||||
// 生成规则卡片
|
||||
filterCards.value = groups?.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split("&"),
|
||||
rules: group.split('&'),
|
||||
visible: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询种子优先规则
|
||||
const queryTorrentPriority = async () => {
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
"system/setting/TorrentsPriority"
|
||||
);
|
||||
selectedTorrentPriority.value = result.data?.value;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/TorrentsPriority',
|
||||
)
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的识别词
|
||||
const saveCustomFilters = async () => {
|
||||
async function saveCustomFilters() {
|
||||
try {
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter((card) => card.rules.length > 0)
|
||||
.map((card) => card.rules.join("&"))
|
||||
.join(">");
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 保存
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"system/setting/FilterRules",
|
||||
value
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("过滤规则保存成功");
|
||||
} else {
|
||||
$toast.error("过滤规则保存失败!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/FilterRules',
|
||||
value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('过滤规则保存成功')
|
||||
else
|
||||
$toast.error('过滤规则保存失败!')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存种子优先规则
|
||||
const saveTorrentPriority = async () => {
|
||||
async function saveTorrentPriority() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"system/setting/TorrentsPriority",
|
||||
selectedTorrentPriority.value
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("优先规则保存成功");
|
||||
} else {
|
||||
$toast.error("优先规则保存失败!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
'system/setting/TorrentsPriority',
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
// 规则卡片列表
|
||||
const filterCards = ref<FilterCard[]>([]);
|
||||
if (result.success)
|
||||
$toast.success('优先规则保存成功')
|
||||
else
|
||||
$toast.error('优先规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则卡片的值
|
||||
const updateFilterCardValue = (pri: string, rules: string[]) => {
|
||||
const card = filterCards.value.find((card) => card.pri === pri);
|
||||
if (card) {
|
||||
card.rules = rules;
|
||||
}
|
||||
};
|
||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
||||
const card = filterCards.value.find(card => card.pri === pri)
|
||||
if (card)
|
||||
card.rules = rules
|
||||
}
|
||||
|
||||
// 移除卡片
|
||||
const filterCardClose = (pri: string) => {
|
||||
function filterCardClose(pri: string) {
|
||||
// 将卡片从列表中删除,并更新剩余卡片的序号
|
||||
const index = filterCards.value.findIndex((card) => card.pri === pri);
|
||||
const index = filterCards.value.findIndex(card => card.pri === pri)
|
||||
if (index !== -1) {
|
||||
// 创建新的数组,然后使用 splice 方法来删除元素
|
||||
const updatedCards = [...filterCards.value];
|
||||
updatedCards.splice(index, 1);
|
||||
const updatedCards = [...filterCards.value]
|
||||
|
||||
updatedCards.splice(index, 1)
|
||||
|
||||
// 更新剩余卡片的序号
|
||||
updatedCards.forEach((card, i) => {
|
||||
card.pri = (i + 1).toString();
|
||||
});
|
||||
card.pri = (i + 1).toString()
|
||||
})
|
||||
|
||||
// 更新 filterCards.value
|
||||
filterCards.value = updatedCards;
|
||||
filterCards.value = updatedCards
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
const addFilterCard = () => {
|
||||
function addFilterCard() {
|
||||
// 优先级
|
||||
const pri = (filterCards.value.length + 1).toString();
|
||||
const pri = (filterCards.value.length + 1).toString()
|
||||
|
||||
// 新卡片
|
||||
const newCard: FilterCard = { pri, rules: [], visible: true };
|
||||
const newCard: FilterCard = { pri, rules: [], visible: true }
|
||||
|
||||
// 添加到列表
|
||||
filterCards.value.push(newCard);
|
||||
};
|
||||
filterCards.value.push(newCard)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryTorrentPriority();
|
||||
queryCustomFilters();
|
||||
});
|
||||
queryTorrentPriority()
|
||||
queryCustomFilters()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -168,8 +180,18 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveCustomFilters" class="me-2"> 保存 </VBtn>
|
||||
<VBtn @click="addFilterCard" color="success" variant="tonal">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-2"
|
||||
@click="saveCustomFilters"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="tonal"
|
||||
@click="addFilterCard"
|
||||
>
|
||||
<VIcon icon="mdi-plus" />
|
||||
<span class="d-none d-sm-block">增加规则</span>
|
||||
</VBtn>
|
||||
@@ -184,16 +206,22 @@ onMounted(() => {
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
></VSelect>
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTorrentPriority"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
<style type="scss">
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -1,88 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { Site } from "@/api/types";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 选中站点
|
||||
const selectedSites = ref<number[]>([]);
|
||||
const selectedSites = ref<number[]>([])
|
||||
|
||||
// 所有站点
|
||||
const allSites = ref<Site[]>([]);
|
||||
const allSites = ref<Site[]>([])
|
||||
|
||||
// 站点重置
|
||||
const isConfirmResetSites = ref(false);
|
||||
const isConfirmResetSites = ref(false)
|
||||
|
||||
// 站点重置按钮文本
|
||||
const resetSitesText = ref("重置站点数据");
|
||||
const resetSitesText = ref('重置站点数据')
|
||||
|
||||
// 站点重置按钮可用状态
|
||||
const resetSitesDisabled = ref(false);
|
||||
const resetSitesDisabled = ref(false)
|
||||
|
||||
// 查询所有站点
|
||||
const querySites = async () => {
|
||||
async function querySites() {
|
||||
try {
|
||||
const data: Site[] = await api.get("site");
|
||||
const data: Site[] = await api.get('site')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
allSites.value = data.filter((item) => item.is_active);
|
||||
querySelectedSites();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
allSites.value = data.filter(item => item.is_active)
|
||||
querySelectedSites()
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户选中的站点
|
||||
const querySelectedSites = async () => {
|
||||
async function querySelectedSites() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get("system/setting/IndexerSites");
|
||||
selectedSites.value = result.data?.value;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
|
||||
|
||||
selectedSites.value = result.data?.value
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户选中的站点
|
||||
const saveSelectedSites = async () => {
|
||||
async function saveSelectedSites() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"system/setting/IndexerSites",
|
||||
selectedSites.value
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("索引站点保存成功");
|
||||
} else {
|
||||
$toast.error("索引站点保存失败!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/IndexerSites',
|
||||
selectedSites.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('索引站点保存成功')
|
||||
else
|
||||
$toast.error('索引站点保存失败!')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置站点
|
||||
const resetSites = async () => {
|
||||
async function resetSites() {
|
||||
try {
|
||||
resetSitesDisabled.value = true;
|
||||
resetSitesText.value = "正在重置...";
|
||||
const result: { [key: string]: any } = await api.get("site/reset");
|
||||
resetSitesDisabled.value = true
|
||||
resetSitesText.value = '正在重置...'
|
||||
|
||||
const result: { [key: string]: any } = await api.get('site/reset')
|
||||
if (result.success) {
|
||||
$toast.success("站点重置成功,请等待CookieCloud同步完成!");
|
||||
querySites();
|
||||
} else {
|
||||
$toast.error("站点重置失败!");
|
||||
$toast.success('站点重置成功,请等待CookieCloud同步完成!')
|
||||
querySites()
|
||||
}
|
||||
resetSitesDisabled.value = false;
|
||||
resetSitesText.value = "重置站点数据";
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
else {
|
||||
$toast.error('站点重置失败!')
|
||||
}
|
||||
resetSitesDisabled.value = false
|
||||
resetSitesText.value = '重置站点数据'
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites();
|
||||
});
|
||||
querySites()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -92,12 +100,16 @@ onMounted(() => {
|
||||
<VCardSubtitle> 只有选中的站点才会在搜索和订阅中使用 </VCardSubtitle>
|
||||
|
||||
<VCardItem>
|
||||
<VChipGroup v-model="selectedSites" column multiple>
|
||||
<VChipGroup
|
||||
v-model="selectedSites"
|
||||
column
|
||||
multiple
|
||||
>
|
||||
<VChip
|
||||
filter
|
||||
variant="outlined"
|
||||
v-for="site in allSites"
|
||||
:key="site.id"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="site.id"
|
||||
>
|
||||
{{ site.name }}
|
||||
@@ -106,7 +118,12 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveSelectedSites"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
@@ -1,80 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 自定义识别词
|
||||
const customIdentifiers = ref("");
|
||||
const customIdentifiers = ref('')
|
||||
|
||||
// 自定义制作组
|
||||
const customReleaseGroups = ref("");
|
||||
const customReleaseGroups = ref('')
|
||||
|
||||
// 查询已设置的识别词
|
||||
const queryCustomIdentifiers = async () => {
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
"system/setting/CustomIdentifiers"
|
||||
);
|
||||
customIdentifiers.value = result.data?.value.join("\n");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/CustomIdentifiers',
|
||||
)
|
||||
|
||||
customIdentifiers.value = result.data?.value.join('\n')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的制作组
|
||||
const queryCustomReleaseGroups = async () => {
|
||||
async function queryCustomReleaseGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
"system/setting/CustomReleaseGroups"
|
||||
);
|
||||
customReleaseGroups.value = result.data?.value.join("\n");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/CustomReleaseGroups',
|
||||
)
|
||||
|
||||
customReleaseGroups.value = result.data?.value.join('\n')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的识别词
|
||||
const saveCustomIdentifiers = async () => {
|
||||
async function saveCustomIdentifiers() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"system/setting/CustomIdentifiers",
|
||||
customIdentifiers.value.split("\n")
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("自定义识别词保存成功");
|
||||
} else {
|
||||
$toast.error("自定义识别词保存失败!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/CustomIdentifiers',
|
||||
customIdentifiers.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义识别词保存成功')
|
||||
else
|
||||
$toast.error('自定义识别词保存失败!')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义制作组
|
||||
const saveCustomReleaseGroups = async () => {
|
||||
async function saveCustomReleaseGroups() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"system/setting/CustomReleaseGroups",
|
||||
customReleaseGroups.value.split("\n")
|
||||
);
|
||||
if (result.success) {
|
||||
$toast.success("自定义制作组/字幕组保存成功");
|
||||
} else {
|
||||
$toast.error("自定义制作组/字幕组保存失败!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
'system/setting/CustomReleaseGroups',
|
||||
customReleaseGroups.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义制作组/字幕组保存成功')
|
||||
else
|
||||
$toast.error('自定义制作组/字幕组保存失败!')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomIdentifiers();
|
||||
queryCustomReleaseGroups();
|
||||
});
|
||||
queryCustomIdentifiers()
|
||||
queryCustomReleaseGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -90,11 +96,15 @@ onMounted(() => {
|
||||
屏蔽词
|
||||
被替换词 => 替换词
|
||||
前定位词 <> 后定位词 >> 偏移量(EP)"
|
||||
>
|
||||
</VTextarea>
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers"> 保存 </VBtn>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomIdentifiers"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
@@ -106,11 +116,15 @@ onMounted(() => {
|
||||
v-model="customReleaseGroups"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
|
||||
>
|
||||
</VTextarea>
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups"> 保存 </VBtn>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomReleaseGroups"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
@@ -1,54 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import api from "@/api";
|
||||
import { MediaStatistic } from "@/api/types";
|
||||
import api from '@/api'
|
||||
import type { MediaStatistic } from '@/api/types'
|
||||
|
||||
const statistics = ref([
|
||||
{
|
||||
title: "",
|
||||
stats: "",
|
||||
icon: "",
|
||||
color: "",
|
||||
title: '',
|
||||
stats: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
// 调用API加载媒体统计数据
|
||||
const loadMediaStatistic = async () => {
|
||||
async function loadMediaStatistic() {
|
||||
try {
|
||||
const res: MediaStatistic = await api.get("dashboard/statistic");
|
||||
const res: MediaStatistic = await api.get('dashboard/statistic')
|
||||
|
||||
statistics.value = [
|
||||
{
|
||||
title: "电影",
|
||||
title: '电影',
|
||||
stats: res.movie_count.toLocaleString(),
|
||||
icon: "mdi-movie-roll",
|
||||
color: "primary",
|
||||
icon: 'mdi-movie-roll',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
title: "电视剧",
|
||||
title: '电视剧',
|
||||
stats: res.tv_count.toLocaleString(),
|
||||
icon: "mdi-television-box",
|
||||
color: "success",
|
||||
icon: 'mdi-television-box',
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
title: "剧集",
|
||||
title: '剧集',
|
||||
stats: res.episode_count.toLocaleString(),
|
||||
icon: "mdi-television-classic",
|
||||
color: "warning",
|
||||
icon: 'mdi-television-classic',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
title: "用户",
|
||||
title: '用户',
|
||||
stats: res.user_count.toLocaleString(),
|
||||
icon: "mdi-account",
|
||||
color: "info",
|
||||
icon: 'mdi-account',
|
||||
color: 'info',
|
||||
},
|
||||
];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
]
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMediaStatistic();
|
||||
});
|
||||
loadMediaStatistic()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,11 +61,24 @@ onMounted(() => {
|
||||
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="item in statistics" :key="item.title" cols="6" sm="3">
|
||||
<VCol
|
||||
v-for="item in statistics"
|
||||
:key="item.title"
|
||||
cols="6"
|
||||
sm="3"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<div class="me-3">
|
||||
<VAvatar :color="item.color" rounded size="42" class="elevation-1">
|
||||
<VIcon size="24" :icon="item.icon" />
|
||||
<VAvatar
|
||||
:color="item.color"
|
||||
rounded
|
||||
size="42"
|
||||
class="elevation-1"
|
||||
>
|
||||
<VIcon
|
||||
size="24"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatSeconds } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import { Process } from "@/api/types";
|
||||
import { formatSeconds } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Process } from '@/api/types'
|
||||
|
||||
// 表头
|
||||
const headers = ["进程ID", "进程名称", "运行时间", "内存占用"];
|
||||
const headers = ['进程ID', '进程名称', '运行时间', '内存占用']
|
||||
|
||||
// 数据列表
|
||||
const processList = ref<Process[]>([]);
|
||||
const processList = ref<Process[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null;
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 调用API加载数据
|
||||
const loadProcessList = async () => {
|
||||
async function loadProcessList() {
|
||||
try {
|
||||
const res: Process[] = await api.get("dashboard/processes");
|
||||
processList.value = res;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const res: Process[] = await api.get('dashboard/processes')
|
||||
|
||||
processList.value = res
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProcessList();
|
||||
loadProcessList()
|
||||
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
loadProcessList();
|
||||
}, 5000);
|
||||
});
|
||||
loadProcessList()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -51,20 +54,38 @@ onUnmounted(() => {
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="header in headers" :key="header" :id="header">
|
||||
<th
|
||||
v-for="header in headers"
|
||||
:id="header"
|
||||
:key="header"
|
||||
>
|
||||
{{ header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in processList" :key="row.pid">
|
||||
<td class="text-sm" v-text="row.pid" />
|
||||
<tr
|
||||
v-for="row in processList"
|
||||
:key="row.pid"
|
||||
>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="row.pid"
|
||||
/>
|
||||
<!-- name -->
|
||||
<td>
|
||||
<h6 class="text-sm font-weight-medium">{{ row.name }}</h6>
|
||||
<h6 class="text-sm font-weight-medium">
|
||||
{{ row.name }}
|
||||
</h6>
|
||||
</td>
|
||||
<td class="text-sm" v-text="formatSeconds(row.run_time)" />
|
||||
<td class="text-sm" v-text="`${row.memory} MB`" />
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="formatSeconds(row.run_time)"
|
||||
/>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="`${row.memory} MB`"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import api from "@/api";
|
||||
import { ScheduleInfo } from "@/api/types";
|
||||
import api from '@/api'
|
||||
import type { ScheduleInfo } from '@/api/types'
|
||||
|
||||
// 定时服务列表
|
||||
const schedulerList = ref<ScheduleInfo[]>([]);
|
||||
const schedulerList = ref<ScheduleInfo[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null;
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 调用API加载定时服务列表
|
||||
const loadSchedulerList = async () => {
|
||||
async function loadSchedulerList() {
|
||||
try {
|
||||
const res: ScheduleInfo[] = await api.get("dashboard/schedule");
|
||||
schedulerList.value = res;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
||||
|
||||
schedulerList.value = res
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSchedulerList();
|
||||
loadSchedulerList()
|
||||
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
loadSchedulerList();
|
||||
}, 60000);
|
||||
});
|
||||
loadSchedulerList()
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,10 +45,21 @@ onUnmounted(() => {
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList class="card-list" height="250">
|
||||
<VListItem v-for="item in schedulerList" :key="item.id">
|
||||
<VList
|
||||
class="card-list"
|
||||
height="250"
|
||||
>
|
||||
<VListItem
|
||||
v-for="item in schedulerList"
|
||||
:key="item.id"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar size="40" variant="tonal" color="" class="me-3">
|
||||
<VAvatar
|
||||
size="40"
|
||||
variant="tonal"
|
||||
color=""
|
||||
class="me-3"
|
||||
>
|
||||
{{ item.name[0] }}
|
||||
</VAvatar>
|
||||
</template>
|
||||
@@ -54,7 +68,9 @@ onUnmounted(() => {
|
||||
<span class="text-sm font-weight-medium">{{ item.name }}</span>
|
||||
</VListItemTitle>
|
||||
|
||||
<VListItemSubtitle class="text-xs"> {{ item.next_run }}</VListItemSubtitle>
|
||||
<VListItemSubtitle class="text-xs">
|
||||
{{ item.next_run }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div>
|
||||
@@ -65,7 +81,9 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</VListItem>
|
||||
<VListItem v-if="schedulerList.length === 0">
|
||||
<VListItemTitle class="text-center">没有后台服务</VListItemTitle>
|
||||
<VListItemTitle class="text-center">
|
||||
没有后台服务
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
@@ -1,76 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { formatFileSize } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import { DownloaderInfo } from "@/api/types";
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null;
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 下载器信息
|
||||
const downloadInfo = ref<DownloaderInfo>({
|
||||
// 下载速度
|
||||
download_speed: 0,
|
||||
|
||||
// 上传速度
|
||||
upload_speed: 0,
|
||||
|
||||
// 下载量
|
||||
download_size: 0,
|
||||
|
||||
// 上传量
|
||||
upload_size: 0,
|
||||
|
||||
// 剩余空间
|
||||
free_space: 0,
|
||||
});
|
||||
})
|
||||
|
||||
// 显示项
|
||||
const infoItems = ref([
|
||||
{
|
||||
avatar: "",
|
||||
title: "",
|
||||
amount: "",
|
||||
avatar: '',
|
||||
title: '',
|
||||
amount: '',
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
// 调用API查询下载器数据
|
||||
const loadDownloaderInfo = async () => {
|
||||
async function loadDownloaderInfo() {
|
||||
try {
|
||||
const res: DownloaderInfo = await api.get("dashboard/downloader");
|
||||
downloadInfo.value = res;
|
||||
const res: DownloaderInfo = await api.get('dashboard/downloader')
|
||||
|
||||
downloadInfo.value = res
|
||||
infoItems.value = [
|
||||
{
|
||||
avatar: "mdi-cloud-upload",
|
||||
title: "总上传量",
|
||||
avatar: 'mdi-cloud-upload',
|
||||
title: '总上传量',
|
||||
amount: formatFileSize(res.upload_size),
|
||||
},
|
||||
{
|
||||
avatar: "mdi-download-box",
|
||||
title: "总下载量",
|
||||
avatar: 'mdi-download-box',
|
||||
title: '总下载量',
|
||||
amount: formatFileSize(res.download_size),
|
||||
},
|
||||
{
|
||||
avatar: "mdi-content-save",
|
||||
title: "磁盘剩余空间",
|
||||
avatar: 'mdi-content-save',
|
||||
title: '磁盘剩余空间',
|
||||
amount: formatFileSize(res.free_space),
|
||||
},
|
||||
];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
]
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloaderInfo();
|
||||
loadDownloaderInfo()
|
||||
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
loadDownloaderInfo();
|
||||
}, 3000);
|
||||
});
|
||||
loadDownloaderInfo()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,13 +88,23 @@ onUnmounted(() => {
|
||||
|
||||
<VCardText class="pt-4">
|
||||
<div>
|
||||
<p class="text-h5 me-2">↑{{ formatFileSize(downloadInfo.upload_speed) }}/s</p>
|
||||
<p class="text-h4 me-2">↓{{ formatFileSize(downloadInfo.download_speed) }}/s</p>
|
||||
<p class="text-h5 me-2">
|
||||
↑{{ formatFileSize(downloadInfo.upload_speed) }}/s
|
||||
</p>
|
||||
<p class="text-h4 me-2">
|
||||
↓{{ formatFileSize(downloadInfo.download_speed) }}/s
|
||||
</p>
|
||||
</div>
|
||||
<VList class="card-list mt-9">
|
||||
<VListItem v-for="item in infoItems" :key="item.title">
|
||||
<VListItem
|
||||
v-for="item in infoItems"
|
||||
:key="item.title"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon rounded :icon="item.avatar" />
|
||||
<VIcon
|
||||
rounded
|
||||
:icon="item.avatar"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="text-sm font-weight-medium mb-1">
|
||||
|
||||
@@ -1,57 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { formatFileSize } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import trophy from "@images/misc/storage.png";
|
||||
import triangleDark from "@images/misc/triangle-dark.png";
|
||||
import triangleLight from "@images/misc/triangle-light.png";
|
||||
import { useTheme } from "vuetify";
|
||||
import { useTheme } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import trophy from '@images/misc/storage.png'
|
||||
import triangleDark from '@images/misc/triangle-dark.png'
|
||||
import triangleLight from '@images/misc/triangle-light.png'
|
||||
|
||||
const { global } = useTheme()
|
||||
|
||||
const { global } = useTheme();
|
||||
const triangleBg = computed(() =>
|
||||
global.name.value === "light" ? triangleLight : triangleDark
|
||||
);
|
||||
global.name.value === 'light' ? triangleLight : triangleDark,
|
||||
)
|
||||
|
||||
// 总存储空间
|
||||
const storage = ref(0);
|
||||
const storage = ref(0)
|
||||
|
||||
// 已使用存储空间
|
||||
const used = ref(0);
|
||||
const used = ref(0)
|
||||
|
||||
// 计算已使用存储空间百分比,精确到小数点后1位
|
||||
const usedPercent = computed(() => {
|
||||
return Math.round((used.value / storage.value) * 1000) / 10;
|
||||
});
|
||||
return Math.round((used.value / storage.value) * 1000) / 10
|
||||
})
|
||||
|
||||
// 调用API,查询存储空间
|
||||
const getStorage = async () => {
|
||||
async function getStorage() {
|
||||
try {
|
||||
const res: Storage = await api.get("dashboard/storage");
|
||||
storage.value = res.total_storage;
|
||||
used.value = res.used_storage;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const res: Storage = await api.get('dashboard/storage')
|
||||
|
||||
storage.value = res.total_storage
|
||||
used.value = res.used_storage
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStorage();
|
||||
});
|
||||
getStorage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="存储空间" subtitle="" class="position-relative">
|
||||
<VCard
|
||||
title="存储空间"
|
||||
subtitle=""
|
||||
class="position-relative"
|
||||
>
|
||||
<VCardText>
|
||||
<h5 class="text-2xl font-weight-medium text-primary">
|
||||
{{ formatFileSize(storage) }}
|
||||
</h5>
|
||||
<p class="mt-2">已使用 {{ usedPercent }}% 🚀</p>
|
||||
<p class="mt-1"><VProgressLinear :model-value="usedPercent" color="primary" /></p>
|
||||
<p class="mt-2">
|
||||
已使用 {{ usedPercent }}% 🚀
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
<VProgressLinear
|
||||
:model-value="usedPercent"
|
||||
color="primary"
|
||||
/>
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<!-- Triangle Background -->
|
||||
<VImg :src="triangleBg" class="triangle-bg flip-in-rtl" />
|
||||
<VImg
|
||||
:src="triangleBg"
|
||||
class="triangle-bg flip-in-rtl"
|
||||
/>
|
||||
|
||||
<!-- Trophy -->
|
||||
<VImg :src="trophy" class="trophy" />
|
||||
<VImg
|
||||
:src="trophy"
|
||||
class="trophy"
|
||||
/>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import api from "@/api";
|
||||
import { hexToRgb } from "@layouts/utils";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
import { useTheme } from "vuetify";
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { useTheme } from 'vuetify'
|
||||
import api from '@/api'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
|
||||
const vuetifyTheme = useTheme();
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const options = controlledComputed(
|
||||
() => vuetifyTheme.name.value,
|
||||
() => {
|
||||
const currentTheme = ref(vuetifyTheme.current.value.colors);
|
||||
const variableTheme = ref(vuetifyTheme.current.value.variables);
|
||||
const currentTheme = ref(vuetifyTheme.current.value.colors)
|
||||
const variableTheme = ref(vuetifyTheme.current.value.variables)
|
||||
|
||||
const disabledColor = `rgba(${hexToRgb(currentTheme.value["on-surface"])},${
|
||||
variableTheme.value["disabled-opacity"]
|
||||
})`;
|
||||
const borderColor = `rgba(${hexToRgb(String(variableTheme.value["border-color"]))},${
|
||||
variableTheme.value["border-opacity"]
|
||||
})`;
|
||||
const disabledColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])},${
|
||||
variableTheme.value['disabled-opacity']
|
||||
})`
|
||||
|
||||
const borderColor = `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
|
||||
variableTheme.value['border-opacity']
|
||||
})`
|
||||
|
||||
return {
|
||||
chart: {
|
||||
@@ -28,9 +29,9 @@ const options = controlledComputed(
|
||||
bar: {
|
||||
borderRadius: 9,
|
||||
distributed: true,
|
||||
columnWidth: "40%",
|
||||
endingShape: "rounded",
|
||||
startingShape: "rounded",
|
||||
columnWidth: '40%',
|
||||
endingShape: 'rounded',
|
||||
startingShape: 'rounded',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
@@ -54,12 +55,12 @@ const options = controlledComputed(
|
||||
dataLabels: { enabled: false },
|
||||
colors: [currentTheme.value.primary],
|
||||
states: {
|
||||
hover: { filter: { type: "none" } },
|
||||
active: { filter: { type: "none" } },
|
||||
hover: { filter: { type: 'none' } },
|
||||
active: { filter: { type: 'none' } },
|
||||
},
|
||||
xaxis: {
|
||||
categories: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
tickPlacement: "on",
|
||||
categories: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
tickPlacement: 'on',
|
||||
labels: { show: false },
|
||||
crosshairs: { opacity: 0 },
|
||||
axisTicks: { show: false },
|
||||
@@ -72,36 +73,38 @@ const options = controlledComputed(
|
||||
offsetX: -17,
|
||||
style: {
|
||||
colors: disabledColor,
|
||||
fontSize: "12px",
|
||||
fontSize: '12px',
|
||||
},
|
||||
|
||||
formatter: (value: number) =>
|
||||
`${value > 999 ? `${(value / 1000).toFixed(0)}` : value}`,
|
||||
value > 999 ? (value / 1000).toFixed(0) : value,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 图表数据
|
||||
const series = ref([{ data: [0, 0, 0, 0, 0, 0, 0] }]);
|
||||
const series = ref([{ data: [0, 0, 0, 0, 0, 0, 0] }])
|
||||
|
||||
// 总数
|
||||
const totalCount = computed(() => series.value[0].data.reduce((a, b) => a + b, 0));
|
||||
const totalCount = computed(() => series.value[0].data.reduce((a, b) => a + b, 0))
|
||||
|
||||
// 调用API接口获取数据近7天数据
|
||||
const getWeeklyData = async () => {
|
||||
async function getWeeklyData() {
|
||||
try {
|
||||
const res: number[] = await api.get("dashboard/transfer");
|
||||
series.value = [{ data: res }];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const res: number[] = await api.get('dashboard/transfer')
|
||||
|
||||
series.value = [{ data: res }]
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getWeeklyData();
|
||||
});
|
||||
getWeeklyData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -111,14 +114,26 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VueApexCharts type="bar" :options="options" :series="series" :height="160" />
|
||||
<VueApexCharts
|
||||
type="bar"
|
||||
:options="options"
|
||||
:series="series"
|
||||
:height="160"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center mb-3">
|
||||
<h5 class="text-h5 me-4">{{ totalCount }}</h5>
|
||||
<h5 class="text-h5 me-4">
|
||||
{{ totalCount }}
|
||||
</h5>
|
||||
<p>最近一周入库了 {{ totalCount }} 部影片 😎</p>
|
||||
</div>
|
||||
|
||||
<VBtn block to="/history"> 查看详情 </VBtn>
|
||||
<VBtn
|
||||
block
|
||||
to="/history"
|
||||
>
|
||||
查看详情
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -1,59 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { MediaInfo } from "@/api/types";
|
||||
import MediaCard from "@/components/cards/MediaCard.vue";
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
import MediaCard from '@/components/cards/MediaCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
params: Object as PropType<{ [key: string]: any }>,
|
||||
});
|
||||
})
|
||||
|
||||
// 判断是否有滚动条
|
||||
const hasScroll = () => {
|
||||
function hasScroll() {
|
||||
return (
|
||||
document.body.scrollHeight -
|
||||
(window.innerHeight || document.documentElement.clientHeight) >
|
||||
2
|
||||
);
|
||||
};
|
||||
document.body.scrollHeight
|
||||
- (window.innerHeight || document.documentElement.clientHeight)
|
||||
> 2
|
||||
)
|
||||
}
|
||||
|
||||
// 当前页码
|
||||
const page = ref(1);
|
||||
const page = ref(1)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false);
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false);
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<MediaInfo[]>([]);
|
||||
const currData = ref<MediaInfo[]>([]);
|
||||
const dataList = ref<MediaInfo[]>([])
|
||||
const currData = ref<MediaInfo[]>([])
|
||||
|
||||
// 拼装参数
|
||||
const getParams = () => {
|
||||
function getParams() {
|
||||
let params = {
|
||||
page: page.value,
|
||||
};
|
||||
if (props.params) {
|
||||
params = { ...params, ...props.params };
|
||||
}
|
||||
return params;
|
||||
};
|
||||
if (props.params)
|
||||
params = { ...params, ...props.params }
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async ({ done }: { done: any }) => {
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
if (!props.apipath) {
|
||||
return;
|
||||
}
|
||||
if (!props.apipath)
|
||||
return
|
||||
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done("ok");
|
||||
return;
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 设置加载中
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
@@ -61,83 +66,97 @@ const fetchData = async ({ done }: { done: any }) => {
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: getParams(),
|
||||
});
|
||||
})
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true;
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done("ok");
|
||||
return;
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value];
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++;
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// 加载一次
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: getParams(),
|
||||
});
|
||||
})
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true;
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done("ok");
|
||||
return;
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value];
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++;
|
||||
page.value++
|
||||
}
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
|
||||
// 返回加载成功
|
||||
done("ok");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// 返回加载失败
|
||||
done("error");
|
||||
done('ok')
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
/>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:onLoad="fetchData"
|
||||
:on-load="fetchData"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<template #loading />
|
||||
<div class="grid gap-4 grid-media-card mx-3" v-if="dataList.length > 0" tabindex="0">
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<MediaCard
|
||||
v-for="data in dataList"
|
||||
:key="data.tmdb_id || data.douban_id"
|
||||
:media="data"
|
||||
>
|
||||
</MediaCard>
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="500"
|
||||
error-title="出错啦!"
|
||||
error-description="无法获取到媒体信息,请检查网络连接。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
@@ -1,57 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { MediaInfo } from "@/api/types";
|
||||
import MediaCard from "@/components/cards/MediaCard.vue";
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
import MediaCard from '@/components/cards/MediaCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 组件加载完成
|
||||
const componentLoaded = ref(false);
|
||||
const componentLoaded = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<MediaInfo[]>([]);
|
||||
const dataList = ref<MediaInfo[]>([])
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
if (!props.apipath) {
|
||||
return;
|
||||
}
|
||||
dataList.value = await api.get(props.apipath);
|
||||
componentLoaded.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (!props.apipath)
|
||||
return
|
||||
|
||||
dataList.value = await api.get(props.apipath)
|
||||
componentLoaded.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onMounted(fetchData);
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot name="title" :loaded="componentLoaded"></slot>
|
||||
<slot
|
||||
name="title"
|
||||
:loaded="componentLoaded"
|
||||
/>
|
||||
<VSlideGroup show-arrows="false">
|
||||
<template #prev>
|
||||
<VBtn class="rounded-circle shadow-none" icon="mdi-chevron-left" color="grey" />
|
||||
<VBtn
|
||||
class="rounded-circle shadow-none"
|
||||
icon="mdi-chevron-left"
|
||||
color="grey"
|
||||
/>
|
||||
</template>
|
||||
<VSlideGroupItem v-for="data in dataList" :key="data.tmdb_id">
|
||||
<VSlideGroupItem
|
||||
v-for="data in dataList"
|
||||
:key="data.tmdb_id"
|
||||
>
|
||||
<MediaCard
|
||||
:key="data.tmdb_id || data.douban_id"
|
||||
:media="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
:key="data.tmdb_id || data.douban_id"
|
||||
/>
|
||||
</VSlideGroupItem>
|
||||
<template #next>
|
||||
<VBtn class="rounded-circle shadow-none" icon="mdi-chevron-right" color="grey" />
|
||||
<VBtn
|
||||
class="rounded-circle shadow-none"
|
||||
icon="mdi-chevron-right"
|
||||
color="grey"
|
||||
/>
|
||||
</template>
|
||||
</VSlideGroup>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.v-slide-group .v-card {
|
||||
@apply m-2;
|
||||
}
|
||||
|
||||
@@ -1,279 +1,295 @@
|
||||
<script lang="ts" setup>
|
||||
import { isIntersected } from "@/@core/utils";
|
||||
import api from "@/api";
|
||||
import { Context } from "@/api/types";
|
||||
import TorrentCard from "@/components/cards/TorrentCard.vue";
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import store from "@/store";
|
||||
import { isIntersected } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
// 关键字或TMDBID
|
||||
keyword: String,
|
||||
|
||||
// 类型
|
||||
type: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Context[]>([]);
|
||||
const dataList = ref<Context[]>([])
|
||||
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = computed(() => {
|
||||
return groupByTitleAndSize(dataList.value);
|
||||
});
|
||||
return groupByTitleAndSize(dataList.value)
|
||||
})
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false);
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 加载进度文本
|
||||
const progressText = ref("");
|
||||
const progressText = ref('')
|
||||
|
||||
// 加载进度
|
||||
const progressValue = ref(0);
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>();
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
// 过滤表单
|
||||
const filterForm = reactive({
|
||||
// 站点
|
||||
site: [] as string[],
|
||||
|
||||
// 季
|
||||
season: [] as string[],
|
||||
|
||||
// 制作组
|
||||
releaseGroup: [] as string[],
|
||||
|
||||
// 视频编码
|
||||
videoCode: [] as string[],
|
||||
|
||||
// 促销状态
|
||||
freeState: [] as string[],
|
||||
|
||||
// 质量
|
||||
edition: [] as string[],
|
||||
});
|
||||
})
|
||||
|
||||
// 获取站点过滤选项
|
||||
const getSiteFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (data.torrent_info?.site_name && !options.includes(data.torrent_info?.site_name)) {
|
||||
options.push(data.torrent_info?.site_name);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
if (data.torrent_info?.site_name && !options.includes(data.torrent_info?.site_name))
|
||||
options.push(data.torrent_info?.site_name)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 获取季过滤选项
|
||||
const getSeasonFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (
|
||||
data.meta_info.season_episode &&
|
||||
!options.includes(data.meta_info.season_episode)
|
||||
) {
|
||||
options.push(data.meta_info.season_episode);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
data.meta_info.season_episode
|
||||
&& !options.includes(data.meta_info.season_episode)
|
||||
)
|
||||
options.push(data.meta_info.season_episode)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 获取制作组过滤选项
|
||||
const getReleaseGroupFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (data.meta_info.resource_team && !options.includes(data.meta_info.resource_team)) {
|
||||
options.push(data.meta_info.resource_team);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
if (data.meta_info.resource_team && !options.includes(data.meta_info.resource_team))
|
||||
options.push(data.meta_info.resource_team)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 获取视频编码过滤选项
|
||||
const getVideoCodeFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (data.meta_info.video_encode && !options.includes(data.meta_info.video_encode)) {
|
||||
options.push(data.meta_info.video_encode);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
if (data.meta_info.video_encode && !options.includes(data.meta_info.video_encode))
|
||||
options.push(data.meta_info.video_encode)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 获取促销状态过滤选项
|
||||
const getFreeStateFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (
|
||||
data.torrent_info.volume_factor &&
|
||||
!options.includes(data.torrent_info.volume_factor)
|
||||
) {
|
||||
options.push(data.torrent_info.volume_factor);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
data.torrent_info.volume_factor
|
||||
&& !options.includes(data.torrent_info.volume_factor)
|
||||
)
|
||||
options.push(data.torrent_info.volume_factor)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 获取质量过滤选项
|
||||
const getEditionFilterOptions = computed(() => {
|
||||
const options: string[] = [];
|
||||
const options: string[] = []
|
||||
|
||||
dataList.value.forEach((data) => {
|
||||
if (data.meta_info.edition && !options.includes(data.meta_info.edition)) {
|
||||
options.push(data.meta_info.edition);
|
||||
}
|
||||
});
|
||||
return options;
|
||||
});
|
||||
if (data.meta_info.edition && !options.includes(data.meta_info.edition))
|
||||
options.push(data.meta_info.edition)
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 按过滤项过滤卡片
|
||||
const filterTorrentsCard = (data: Context) => {
|
||||
function filterTorrentsCard(data: Context) {
|
||||
// 当前分组的所有数据
|
||||
const items: Context[] =
|
||||
groupedDataList.value.get(`${data.torrent_info.title}_${data.torrent_info.size}`) ??
|
||||
[];
|
||||
const items: Context[]
|
||||
= groupedDataList.value.get(`${data.torrent_info.title}_${data.torrent_info.size}`)
|
||||
?? []
|
||||
|
||||
// 站点名称、促销状态
|
||||
let site_names = [];
|
||||
let volume_factors = [];
|
||||
const site_names = []
|
||||
const volume_factors = []
|
||||
for (const { torrent_info } of items) {
|
||||
site_names.push(torrent_info.site_name);
|
||||
volume_factors.push(torrent_info.volume_factor);
|
||||
site_names.push(torrent_info.site_name)
|
||||
volume_factors.push(torrent_info.volume_factor)
|
||||
}
|
||||
|
||||
const { meta_info } = data;
|
||||
const { meta_info } = data
|
||||
|
||||
// 季、制作组、视频编码
|
||||
const { season_episode, resource_team, video_encode } = meta_info;
|
||||
const { season_episode, resource_team, video_encode } = meta_info
|
||||
|
||||
// 站点过滤
|
||||
if (filterForm.site.length > 0 && !isIntersected(filterForm.site, site_names)) {
|
||||
return false;
|
||||
}
|
||||
if (filterForm.site.length > 0 && !isIntersected(filterForm.site, site_names))
|
||||
return false
|
||||
|
||||
// 促销状态过滤
|
||||
if (
|
||||
filterForm.freeState.length > 0 &&
|
||||
!isIntersected(filterForm.freeState, volume_factors)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
filterForm.freeState.length > 0
|
||||
&& !isIntersected(filterForm.freeState, volume_factors)
|
||||
)
|
||||
return false
|
||||
|
||||
// 季过滤
|
||||
if (filterForm.season.length > 0 && !filterForm.season.includes(season_episode)) {
|
||||
return false;
|
||||
}
|
||||
if (filterForm.season.length > 0 && !filterForm.season.includes(season_episode))
|
||||
return false
|
||||
|
||||
// 制作组过滤
|
||||
if (
|
||||
filterForm.releaseGroup.length > 0 &&
|
||||
!filterForm.releaseGroup.includes(resource_team || "")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
filterForm.releaseGroup.length > 0
|
||||
&& !filterForm.releaseGroup.includes(resource_team || '')
|
||||
)
|
||||
return false
|
||||
|
||||
// 视频编码过滤
|
||||
if (
|
||||
filterForm.videoCode.length > 0 &&
|
||||
!filterForm.videoCode.includes(video_encode || "")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
filterForm.videoCode.length > 0
|
||||
&& !filterForm.videoCode.includes(video_encode || '')
|
||||
)
|
||||
return false
|
||||
|
||||
// 质量过滤
|
||||
if (filterForm.edition.length > 0 && !filterForm.edition.includes(meta_info.edition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
return !(filterForm.edition.length > 0 && !filterForm.edition.includes(meta_info.edition))
|
||||
}
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
let keyword = props.keyword ?? "";
|
||||
let mtype = props.type ?? "";
|
||||
const keyword = props.keyword ?? ''
|
||||
const mtype = props.type ?? ''
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
dataList.value = await api.get("search/last");
|
||||
} else {
|
||||
startLoadingProgress();
|
||||
dataList.value = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
startLoadingProgress()
|
||||
|
||||
// 优先按TMDBID精确查询
|
||||
if (props.keyword?.startsWith("tmdb:") || props.keyword?.startsWith("douban:")) {
|
||||
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
|
||||
dataList.value = await api.get(`search/media/${props.keyword}`, {
|
||||
params: {
|
||||
mtype,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${props.keyword}`);
|
||||
})
|
||||
}
|
||||
stopLoadingProgress();
|
||||
else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${props.keyword}`)
|
||||
}
|
||||
stopLoadingProgress()
|
||||
}
|
||||
isRefreshed.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
isRefreshed.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 按标题和大小分组
|
||||
const groupByTitleAndSize = (contextArray: Context[]): Map<string, Context[]> => {
|
||||
const groupMap = new Map<string, Context[]>();
|
||||
function groupByTitleAndSize(contextArray: Context[]): Map<string, Context[]> {
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
|
||||
for (const context of contextArray) {
|
||||
const { torrent_info } = context;
|
||||
const key = `${torrent_info.title}_${torrent_info.size}`;
|
||||
const { torrent_info } = context
|
||||
const key = `${torrent_info.title}_${torrent_info.size}`
|
||||
|
||||
if (groupMap.has(key)) {
|
||||
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
const group = groupMap.get(key);
|
||||
group?.push(context);
|
||||
} else {
|
||||
const group = groupMap.get(key)
|
||||
|
||||
group?.push(context)
|
||||
}
|
||||
else {
|
||||
// 创建新的分组,并将当前上下文信息添加到分组中
|
||||
groupMap.set(key, [context]);
|
||||
groupMap.set(key, [context])
|
||||
}
|
||||
}
|
||||
|
||||
return groupMap;
|
||||
};
|
||||
return groupMap
|
||||
}
|
||||
|
||||
// 获取每个分组的第一个数据
|
||||
const getFirstContexts = computed(() => {
|
||||
const firstContexts: Context[] = [];
|
||||
const firstContexts: Context[] = []
|
||||
|
||||
groupedDataList.value.forEach((group) => {
|
||||
if (group.length > 0) {
|
||||
firstContexts.push(group[0]);
|
||||
}
|
||||
});
|
||||
if (group.length > 0)
|
||||
firstContexts.push(group[0])
|
||||
})
|
||||
|
||||
return firstContexts;
|
||||
});
|
||||
return firstContexts
|
||||
})
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
const startLoadingProgress = () => {
|
||||
progressText.value = "正在搜索,请稍候...";
|
||||
const token = store.state.auth.token;
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '正在搜索,请稍候...'
|
||||
|
||||
const token = store.state.auth.token
|
||||
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`
|
||||
);
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
const progress = JSON.parse(event.data);
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text;
|
||||
progressValue.value = progress.value;
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
const stopLoadingProgress = () => {
|
||||
progressEventSource.value?.close();
|
||||
};
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData);
|
||||
onBeforeMount(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="bg-transparent mb-3 pt-2 shadow-none">
|
||||
<VRow>
|
||||
<VCol v-if="getSiteFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getSiteFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.site"
|
||||
:items="getSiteFilterOptions"
|
||||
@@ -284,7 +300,11 @@ onBeforeMount(fetchData);
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="getSeasonFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getSeasonFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.season"
|
||||
:items="getSeasonFilterOptions"
|
||||
@@ -295,7 +315,11 @@ onBeforeMount(fetchData);
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="getReleaseGroupFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getReleaseGroupFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.releaseGroup"
|
||||
:items="getReleaseGroupFilterOptions"
|
||||
@@ -306,7 +330,11 @@ onBeforeMount(fetchData);
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="getEditionFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getEditionFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.edition"
|
||||
:items="getEditionFilterOptions"
|
||||
@@ -317,7 +345,11 @@ onBeforeMount(fetchData);
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="getVideoCodeFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getVideoCodeFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.videoCode"
|
||||
:items="getVideoCodeFilterOptions"
|
||||
@@ -328,7 +360,11 @@ onBeforeMount(fetchData);
|
||||
multiple
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="getFreeStateFilterOptions.length > 0" cols="6" md="">
|
||||
<VCol
|
||||
v-if="getFreeStateFilterOptions.length > 0"
|
||||
cols="6"
|
||||
md=""
|
||||
>
|
||||
<VSelect
|
||||
v-model="filterForm.freeState"
|
||||
:items="getFreeStateFilterOptions"
|
||||
@@ -342,14 +378,14 @@ onBeforeMount(fetchData);
|
||||
</VRow>
|
||||
</VCard>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed && !props.keyword"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
/>
|
||||
<div
|
||||
class="top-centered mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
v-if="!isRefreshed && props.keyword"
|
||||
class="top-centered mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
class="mb-3"
|
||||
@@ -357,10 +393,13 @@ onBeforeMount(fetchData);
|
||||
:model-value="progressValue"
|
||||
size="64"
|
||||
width="7"
|
||||
></VProgressCircular>
|
||||
/>
|
||||
<span>{{ progressText }}</span>
|
||||
</div>
|
||||
<div class="grid gap-3 grid-torrent-card items-start" v-if="dataList.length > 0">
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-torrent-card items-start"
|
||||
>
|
||||
<TorrentCard
|
||||
v-for="data in getFirstContexts"
|
||||
v-show="filterTorrentsCard(data)"
|
||||
@@ -378,11 +417,10 @@ onBeforeMount(fetchData);
|
||||
error-code="404"
|
||||
error-title="没有资源"
|
||||
error-description="没有搜索到符合条件的资源。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -1,82 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import type { DownloadingInfo } from "@/api/types";
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import DownloadingCard from "@/components/cards/DownloadingCard.vue";
|
||||
import PullRefresh from "pull-refresh-vue3";
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import api from '@/api'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null;
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<DownloadingInfo[]>([]);
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
dataList.value = await api.get("download");
|
||||
isRefreshed.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新状态
|
||||
const loading = ref(false);
|
||||
const dataList = ref<DownloadingInfo[]>([])
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false);
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('download')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
loading.value = true;
|
||||
fetchData();
|
||||
loading.value = false;
|
||||
};
|
||||
function onRefresh() {
|
||||
loading.value = true
|
||||
fetchData()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
fetchData();
|
||||
fetchData()
|
||||
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
fetchData();
|
||||
}, 3000);
|
||||
});
|
||||
fetchData()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
<PullRefresh v-model="loading" @refresh="onRefresh">
|
||||
<div class="grid gap-3 grid-downloading-card" v-if="dataList.length > 0">
|
||||
<DownloadingCard v-for="data in dataList" :key="data.hash" :info="data" />
|
||||
/>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-downloading-card"
|
||||
>
|
||||
<DownloadingCard
|
||||
v-for="data in dataList"
|
||||
:key="data.hash"
|
||||
:info="data"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有任务"
|
||||
error-description="正在下载的任务将会显示在这里。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,198 +1,209 @@
|
||||
<script setup lang="ts">
|
||||
import { numberValidator, requiredValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import type { TransferHistory } from "@/api/types";
|
||||
import { ref } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { useConfirm } from "vuetify-use-dialog";
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { TransferHistory } from '@/api/types'
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm();
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast();
|
||||
const $toast = useToast()
|
||||
|
||||
// 重新整理对话框
|
||||
const redoDialog = ref(false);
|
||||
const redoDialog = ref(false)
|
||||
|
||||
// TMDB编号
|
||||
const redoTmdbId = ref("");
|
||||
const redoTmdbId = ref('')
|
||||
|
||||
// 当前操作记录
|
||||
const currentHistory = ref<TransferHistory>();
|
||||
const currentHistory = ref<TransferHistory>()
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: "标题", key: "title", sortable: false },
|
||||
{ title: "目录", key: "src", sortable: false },
|
||||
{ title: "转移方式", key: "mode", sortable: false },
|
||||
{ title: "时间", key: "date", sortable: false },
|
||||
{ title: "状态", key: "status", sortable: false },
|
||||
{ title: "失败原因", key: "errmsg", sortable: false },
|
||||
{ title: "", key: "actions", sortable: false },
|
||||
];
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
{ title: '目录', key: 'src', sortable: false },
|
||||
{ title: '转移方式', key: 'mode', sortable: false },
|
||||
{ title: '时间', key: 'date', sortable: false },
|
||||
{ title: '状态', key: 'status', sortable: false },
|
||||
{ title: '失败原因', key: 'errmsg', sortable: false },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TransferHistory[]>([]);
|
||||
const dataList = ref<TransferHistory[]>([])
|
||||
|
||||
// 搜索
|
||||
const search = ref("");
|
||||
const search = ref('')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const loading = ref(false)
|
||||
|
||||
// 总条数
|
||||
const totalItems = ref(0);
|
||||
const totalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const itemsPerPage = ref(25);
|
||||
const itemsPerPage = ref(25)
|
||||
|
||||
// 当前页码
|
||||
const currentPage = ref(1);
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async ({
|
||||
async function fetchData({
|
||||
page,
|
||||
itemsPerPage,
|
||||
}: {
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
}) => {
|
||||
loading.value = true;
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
}) {
|
||||
loading.value = true
|
||||
try {
|
||||
currentPage.value = page;
|
||||
const result: { [key: string]: any } = await api.get("history/transfer", {
|
||||
currentPage.value = page
|
||||
|
||||
const result: { [key: string]: any } = await api.get('history/transfer', {
|
||||
params: {
|
||||
page,
|
||||
count: itemsPerPage,
|
||||
title: search.value,
|
||||
},
|
||||
});
|
||||
dataList.value = result.data.list;
|
||||
totalItems.value = result.data.total;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
})
|
||||
|
||||
dataList.value = result.data.list
|
||||
totalItems.value = result.data.total
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
const getIcon = (type: string) => {
|
||||
if (type === "电影") {
|
||||
return "mdi-movie";
|
||||
} else if (type === "电视剧") {
|
||||
return "mdi-television-classic";
|
||||
} else {
|
||||
return "mdi-help-circle";
|
||||
}
|
||||
};
|
||||
function getIcon(type: string) {
|
||||
if (type === '电影')
|
||||
return 'mdi-movie'
|
||||
else if (type === '电视剧')
|
||||
return 'mdi-television-classic'
|
||||
else
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算颜色
|
||||
const getStatusColor = (status: boolean) => {
|
||||
return status ? "success" : "error";
|
||||
};
|
||||
function getStatusColor(status: boolean) {
|
||||
return status ? 'success' : 'error'
|
||||
}
|
||||
|
||||
// 转移方式字典
|
||||
const TransferDict: { [key: string]: string } = {
|
||||
copy: "复制",
|
||||
move: "移动",
|
||||
link: "硬链接",
|
||||
softlink: "软链接",
|
||||
};
|
||||
copy: '复制',
|
||||
move: '移动',
|
||||
link: '硬链接',
|
||||
softlink: '软链接',
|
||||
}
|
||||
|
||||
// 删除历史记录
|
||||
const removeHistory = async (item: TransferHistory) => {
|
||||
async function removeHistory(item: TransferHistory) {
|
||||
try {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: "确认",
|
||||
title: '确认',
|
||||
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
|
||||
confirmationText: "同步删除文件",
|
||||
cancellationText: "仅删除历史记录",
|
||||
confirmationText: '同步删除文件',
|
||||
cancellationText: '仅删除历史记录',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
});
|
||||
let deleteFile = false;
|
||||
if (isConfirmed) {
|
||||
deleteFile = true;
|
||||
}
|
||||
})
|
||||
|
||||
let deleteFile = false
|
||||
if (isConfirmed)
|
||||
deleteFile = true
|
||||
|
||||
// 调用删除API
|
||||
const result: { [key: string]: any } = await api.delete("history/transfer", {
|
||||
const result: { [key: string]: any } = await api.delete('history/transfer', {
|
||||
data: {
|
||||
...item,
|
||||
delete_file: deleteFile,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
});
|
||||
} else {
|
||||
$toast.error(`删除失败: ${result.msg}`);
|
||||
})
|
||||
}
|
||||
else {
|
||||
$toast.error(`删除失败: ${result.msg}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新整理
|
||||
const rehandleHistory = async () => {
|
||||
async function rehandleHistory() {
|
||||
try {
|
||||
if (!redoTmdbId.value) {
|
||||
return;
|
||||
}
|
||||
redoDialog.value = false;
|
||||
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`);
|
||||
if (!redoTmdbId.value)
|
||||
return
|
||||
|
||||
redoDialog.value = false
|
||||
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
|
||||
|
||||
// 调用API接口重新转移
|
||||
const requestData = {
|
||||
...currentHistory.value,
|
||||
};
|
||||
}
|
||||
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
"history/transfer",
|
||||
'history/transfer',
|
||||
requestData,
|
||||
{
|
||||
params: {
|
||||
new_tmdbid: parseInt(redoTmdbId.value),
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
});
|
||||
} else {
|
||||
$toast.error(`重新整理失败: ${result.message}!`);
|
||||
})
|
||||
}
|
||||
else {
|
||||
$toast.error(`重新整理失败: ${result.message}!`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: "重新整理",
|
||||
title: '重新整理',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: "mdi-redo-variant",
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoDialog.value = true;
|
||||
currentHistory.value = item;
|
||||
redoDialog.value = true
|
||||
currentHistory.value = item
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "删除",
|
||||
title: '删除',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: "mdi-trash-can-outline",
|
||||
color: "error",
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: removeHistory,
|
||||
},
|
||||
},
|
||||
]);
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -226,17 +237,17 @@ const dropdownItems = ref([
|
||||
:items-length="totalItems"
|
||||
:search="search"
|
||||
:loading="loading"
|
||||
@update:options="fetchData"
|
||||
density="compact"
|
||||
item-value="id"
|
||||
return-object
|
||||
fixed-header
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
@update:options="fetchData"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex">
|
||||
<VAvatar><VIcon :icon="getIcon(item.raw.type || '')"></VIcon></VAvatar>
|
||||
<VAvatar><VIcon :icon="getIcon(item.raw.type || '')" /></VAvatar>
|
||||
<div class="d-flex flex-column ms-1">
|
||||
<span class="d-block whitespace-nowrap text-high-emphasis">
|
||||
{{ item.raw.title }} {{ item.raw.seasons }}{{ item.raw.episodes }}
|
||||
@@ -246,15 +257,24 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
</template>
|
||||
<template #item.src="{ item }">
|
||||
<small>{{ item.raw.src }} <br />=> {{ item.raw.dest }}</small>
|
||||
<small>{{ item.raw.src }} <br>=> {{ item.raw.dest }}</small>
|
||||
</template>
|
||||
<template #item.mode="{ item }">
|
||||
<VChip variant="outlined" color="primary" size="small">{{
|
||||
TransferDict[item.raw.mode]
|
||||
}}</VChip>
|
||||
<VChip
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
TransferDict[item.raw.mode]
|
||||
}}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip :color="getStatusColor(item.raw.status)" size="small">
|
||||
<VChip
|
||||
:color="getStatusColor(item.raw.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.raw.status ? "成功" : "失败" }}
|
||||
</VChip>
|
||||
</template>
|
||||
@@ -267,28 +287,36 @@ const dropdownItems = ref([
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
:key="i"
|
||||
@click="menu.props.click(item.raw)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon"></VIcon>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title"></VListItemTitle>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<template #no-data> 没有数据 </template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
<VDialog v-model="redoDialog" max-width="600">
|
||||
<VDialog
|
||||
v-model="redoDialog"
|
||||
max-width="600"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="重新整理">
|
||||
<VCardText>
|
||||
@@ -305,13 +333,18 @@ const dropdownItems = ref([
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn @click="rehandleHistory" @keydown.enter="rehandleHistory"> 确定 </VBtn>
|
||||
<VBtn
|
||||
@click="rehandleHistory"
|
||||
@keydown.enter="rehandleHistory"
|
||||
>
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.v-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import { Plugin } from "@/api/types";
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import PluginAppCard from "@/components/cards/PluginAppCard.vue";
|
||||
import PluginCard from "@/components/cards/PluginCard.vue";
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
|
||||
import PluginCard from '@/components/cards/PluginCard.vue'
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Plugin[]>([]);
|
||||
const dataList = ref<Plugin[]>([])
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false);
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// APP市场窗口
|
||||
const PluginAppDialog = ref(false);
|
||||
const PluginAppDialog = ref(false)
|
||||
|
||||
// 获取已安装的插件列表
|
||||
const getInstalledPluginList = computed(() => {
|
||||
return dataList.value.filter((item) => item.installed);
|
||||
});
|
||||
return dataList.value.filter(item => item.installed)
|
||||
})
|
||||
|
||||
// 获取未安装的插件列表
|
||||
const getUninstalledPluginList = computed(() => {
|
||||
return dataList.value.filter((item) => !item.installed);
|
||||
});
|
||||
return dataList.value.filter(item => !item.installed)
|
||||
})
|
||||
|
||||
// 关闭插件市场窗口
|
||||
const pluginDialogClose = () => {
|
||||
PluginAppDialog.value = false;
|
||||
};
|
||||
function pluginDialogClose() {
|
||||
PluginAppDialog.value = false
|
||||
}
|
||||
|
||||
// 新安装了插件
|
||||
const pluginInstalled = () => {
|
||||
fetchData();
|
||||
pluginDialogClose();
|
||||
};
|
||||
function pluginInstalled() {
|
||||
fetchData()
|
||||
pluginDialogClose()
|
||||
}
|
||||
|
||||
// 获取插件列表数据
|
||||
const fetchData = async () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get("plugin");
|
||||
isRefreshed.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dataList.value = await api.get('plugin')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData);
|
||||
onBeforeMount(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
<div class="grid gap-3 grid-plugin-card" v-if="dataList.length > 0">
|
||||
/>
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-plugin-card"
|
||||
>
|
||||
<PluginCard
|
||||
v-for="data in getInstalledPluginList"
|
||||
:key="data.id"
|
||||
@@ -69,8 +73,7 @@ onBeforeMount(fetchData);
|
||||
error-code="404"
|
||||
error-title="没有安装插件"
|
||||
error-description="点击右下角按钮,前往插件市场安装插件。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
<!-- App市场 -->
|
||||
<VDialog
|
||||
v-model="PluginAppDialog"
|
||||
@@ -80,8 +83,12 @@ onBeforeMount(fetchData);
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBtn icon="mdi-plus" v-bind="props" size="x-large" class="fixed right-5 bottom-5">
|
||||
</VBtn>
|
||||
<VBtn
|
||||
icon="mdi-plus"
|
||||
v-bind="props"
|
||||
size="x-large"
|
||||
class="fixed right-5 bottom-5"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Dialog Content -->
|
||||
@@ -94,8 +101,14 @@ onBeforeMount(fetchData);
|
||||
<VSpacer />
|
||||
|
||||
<VToolbarItems>
|
||||
<VBtn size="x-large" @click="pluginDialogClose">
|
||||
<VIcon color="white" icon="mdi-close" />
|
||||
<VBtn
|
||||
size="x-large"
|
||||
@click="pluginDialogClose"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="mdi-close"
|
||||
/>
|
||||
</VBtn>
|
||||
</VToolbarItems>
|
||||
</VToolbar>
|
||||
@@ -114,14 +127,13 @@ onBeforeMount(fetchData);
|
||||
error-code="404"
|
||||
error-title="没有未安装插件"
|
||||
error-description="所有可用插件均已安装。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api';
|
||||
import { Site } from '@/api/types';
|
||||
import SiteCard from '@/components/cards/SiteCard.vue';
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Site[]>([])
|
||||
@@ -11,11 +11,12 @@ const dataList = ref<Site[]>([])
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 获取站点列表数据
|
||||
const fetchData = async () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('site')
|
||||
isRefreshed.value = true
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -26,14 +27,14 @@ onBeforeMount(fetchData)
|
||||
|
||||
<template>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
/>
|
||||
<div
|
||||
class="grid gap-3 grid-site-card"
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-site-card"
|
||||
>
|
||||
<SiteCard
|
||||
v-for="data in dataList"
|
||||
@@ -46,11 +47,10 @@ onBeforeMount(fetchData)
|
||||
error-code="404"
|
||||
error-title="没有站点"
|
||||
error-description="已添加并支持的站点将会在这里显示。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -1,102 +1,99 @@
|
||||
<script lang="ts" setup>
|
||||
import { parseDate } from "@/@core/utils/formatters";
|
||||
import api from "@/api";
|
||||
import type { MediaInfo, Subscribe, TmdbEpisode } from "@/api/types";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import type { CalendarOptions } from '@fullcalendar/core'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import type { Ref } from 'vue'
|
||||
import type { MediaInfo, Subscribe, TmdbEpisode } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { parseDate } from '@/@core/utils/formatters'
|
||||
|
||||
// 日历属性
|
||||
const calendarOptions = ref({
|
||||
height: "auto",
|
||||
locale: "zh-cn",
|
||||
themeSystem: "standard",
|
||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||
height: 'auto',
|
||||
locale: 'zh-cn',
|
||||
themeSystem: 'standard',
|
||||
buttonText: {
|
||||
today: "今天",
|
||||
month: "月",
|
||||
week: "周",
|
||||
day: "日",
|
||||
list: "列表",
|
||||
today: '今天',
|
||||
month: '月',
|
||||
week: '周',
|
||||
day: '日',
|
||||
list: '列表',
|
||||
},
|
||||
plugins: [
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin, // needed for dateClick
|
||||
],
|
||||
initialView: "dayGridMonth",
|
||||
initialView: 'dayGridMonth',
|
||||
weekends: false,
|
||||
headerToolbar: {
|
||||
left: "prev",
|
||||
center: "title",
|
||||
right: "next",
|
||||
left: 'prev',
|
||||
center: 'title',
|
||||
right: 'next',
|
||||
},
|
||||
views: {
|
||||
week: {
|
||||
titleFormat: { day: "numeric" },
|
||||
titleFormat: { day: 'numeric' },
|
||||
},
|
||||
},
|
||||
events: [],
|
||||
});
|
||||
})
|
||||
|
||||
// 调用API查询所有订阅
|
||||
const getSubscribes = async () => {
|
||||
async function getSubscribes() {
|
||||
try {
|
||||
const subscribes: Subscribe[] = await api.get("subscribe");
|
||||
const subscribes: Subscribe[] = await api.get('subscribe')
|
||||
|
||||
const events = await Promise.all(
|
||||
subscribes.map(async (subscribe) => {
|
||||
// 如果是电影直接返回
|
||||
if (subscribe.type === "电影") {
|
||||
if (subscribe.type === '电影') {
|
||||
// 调用API查询TMDB详情
|
||||
const movie: MediaInfo = await api.get(`tmdb/${subscribe.tmdbid}`, {
|
||||
params: { tmdbid: subscribe.tmdbid, type_name: subscribe.type },
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
title: subscribe.name,
|
||||
start: parseDate(movie.release_date || ""),
|
||||
start: parseDate(movie.release_date || ''),
|
||||
allDay: false,
|
||||
posterPath: subscribe.poster,
|
||||
mediaType: subscribe.type,
|
||||
};
|
||||
} else {
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 调用API查询集信息
|
||||
const episodes: TmdbEpisode[] = await api.get(
|
||||
`tmdb/${subscribe.tmdbid}/${subscribe.season}`
|
||||
);
|
||||
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
|
||||
)
|
||||
|
||||
return episodes.map((episode) => {
|
||||
return {
|
||||
title: subscribe.name,
|
||||
subtitle: `第 ${episode.episode_number} 集`,
|
||||
start: parseDate(episode.air_date || ""),
|
||||
start: parseDate(episode.air_date || ''),
|
||||
allDay: false,
|
||||
posterPath: subscribe.poster,
|
||||
mediaType: subscribe.type,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
);
|
||||
calendarOptions.value.events = events.flat();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}),
|
||||
)
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
const getIcon = (type: string) => {
|
||||
if (type === "电影") {
|
||||
return "mdi-movie-roll";
|
||||
} else if (type === "电视剧") {
|
||||
return "mdi-television-box";
|
||||
} else {
|
||||
return "mdi-help-circle";
|
||||
calendarOptions.value.events = events.flat()
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时调用API查询所有订阅
|
||||
onMounted(() => {
|
||||
getSubscribes();
|
||||
});
|
||||
getSubscribes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -117,7 +114,9 @@ onMounted(() => {
|
||||
</div>
|
||||
<VDivider :vertical="$vuetify.display.mdAndUp" />
|
||||
<div>
|
||||
<VCardSubtitle class="pa-2 font-bold">{{ arg.event.title }}</VCardSubtitle>
|
||||
<VCardSubtitle class="pa-2 font-bold">
|
||||
{{ arg.event.title }}
|
||||
</VCardSubtitle>
|
||||
<VCardText class="pa-0 px-2">
|
||||
{{ arg.event.extendedProps.subtitle }}
|
||||
</VCardText>
|
||||
|
||||
@@ -1,73 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import api from "@/api";
|
||||
import type { Subscribe } from "@/api/types";
|
||||
import NoDataFound from "@/components/NoDataFound.vue";
|
||||
import SubscribeCard from "@/components/cards/SubscribeCard.vue";
|
||||
import PullRefresh from 'pull-refresh-vue3';
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
});
|
||||
})
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Subscribe[]>([]);
|
||||
const dataList = ref<Subscribe[]>([])
|
||||
|
||||
// 获取订阅列表数据
|
||||
const fetchData = async () => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get("subscribe");
|
||||
dataList.value = await api.get('subscribe')
|
||||
isRefreshed.value = true
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData);
|
||||
onBeforeMount(fetchData)
|
||||
|
||||
// 刷新状态
|
||||
const loading = ref(false);
|
||||
const loading = ref(false)
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
loading.value = true;
|
||||
fetchData();
|
||||
loading.value = false;
|
||||
};
|
||||
function onRefresh() {
|
||||
loading.value = true
|
||||
fetchData()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 过滤数据
|
||||
const filteredDataList = computed(() => {
|
||||
return dataList.value.filter((data) => data.type === props.type);
|
||||
});
|
||||
return dataList.value.filter(data => data.type === props.type)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VProgressCircular
|
||||
class="centered"
|
||||
v-if="!isRefreshed"
|
||||
class="centered"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></VProgressCircular>
|
||||
<PullRefresh v-model="loading" @refresh="onRefresh">
|
||||
<div class="grid gap-3 grid-subscribe-card"
|
||||
v-if="filteredDataList.length > 0">
|
||||
<SubscribeCard v-for="data in filteredDataList" :key="data.id" :media="data" />
|
||||
/>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="filteredDataList.length > 0"
|
||||
class="grid gap-3 grid-subscribe-card"
|
||||
>
|
||||
<SubscribeCard
|
||||
v-for="data in filteredDataList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有订阅"
|
||||
error-description="请通过搜索添加电影、电视剧订阅。"
|
||||
>
|
||||
</NoDataFound>
|
||||
/>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
|
||||
<style type="scss">
|
||||
<style lang="scss">
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { requiredValidator } from "@/@validators";
|
||||
import api from "@/api";
|
||||
import { Context } from "@/api/types";
|
||||
import { reactive, ref } from "vue";
|
||||
import { reactive, ref } from 'vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>();
|
||||
const nameTestResult = ref<Context>()
|
||||
|
||||
// 名称识别表单
|
||||
const nameTestForm = reactive({
|
||||
title: "",
|
||||
subtitle: "",
|
||||
});
|
||||
title: '',
|
||||
subtitle: '',
|
||||
})
|
||||
|
||||
// 识别按钮状态
|
||||
const nameTestLoading = ref(false);
|
||||
const nameTestLoading = ref(false)
|
||||
|
||||
// 识别按钮文本
|
||||
const nameTestText = ref("识别");
|
||||
const nameTestText = ref('识别')
|
||||
|
||||
// 是否显示结果
|
||||
const showResult = ref(false);
|
||||
const showResult = ref(false)
|
||||
|
||||
// 调用API识别
|
||||
const nameTest = async () => {
|
||||
if (!nameTestForm.title) {
|
||||
return;
|
||||
}
|
||||
async function nameTest() {
|
||||
if (!nameTestForm.title)
|
||||
return
|
||||
|
||||
try {
|
||||
nameTestLoading.value = true;
|
||||
nameTestText.value = "识别中...";
|
||||
showResult.value = false;
|
||||
nameTestResult.value = await api.get("media/recognize", {
|
||||
nameTestLoading.value = true
|
||||
nameTestText.value = '识别中...'
|
||||
showResult.value = false
|
||||
nameTestResult.value = await api.get('media/recognize', {
|
||||
params: {
|
||||
title: nameTestForm.title,
|
||||
subtitle: nameTestForm.subtitle,
|
||||
},
|
||||
});
|
||||
nameTestLoading.value = false;
|
||||
nameTestText.value = "重新识别";
|
||||
showResult.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
})
|
||||
nameTestLoading.value = false
|
||||
nameTestText.value = '重新识别'
|
||||
showResult.value = true
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -56,14 +58,25 @@ const nameTest = async () => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea v-model="nameTestForm.subtitle" label="副标题" rows="2" auto-grow />
|
||||
<VTextarea
|
||||
v-model="nameTestForm.subtitle"
|
||||
label="副标题"
|
||||
rows="2"
|
||||
auto-grow
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" class="text-center">
|
||||
<VBtn @click="nameTest" :disabled="nameTestLoading">
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<VBtn
|
||||
:disabled="nameTestLoading"
|
||||
@click="nameTest"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-text-recognition"></VIcon>
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</template>
|
||||
{{ nameTestText }}
|
||||
</VBtn>
|
||||
@@ -74,10 +87,13 @@ const nameTest = async () => {
|
||||
<div v-show="showResult">
|
||||
<VCol>
|
||||
<div
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
v-if="nameTestResult?.meta_info?.name"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div class="ma-auto" v-if="nameTestResult?.media_info?.poster_path">
|
||||
<div
|
||||
v-if="nameTestResult?.media_info?.poster_path"
|
||||
class="ma-auto"
|
||||
>
|
||||
<VImg
|
||||
width="10rem"
|
||||
aspect-ratio="2/3"
|
||||
@@ -108,28 +124,28 @@ const nameTest = async () => {
|
||||
<VCardItem>
|
||||
<!-- 类型 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
v-if="nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type"
|
||||
>
|
||||
{{
|
||||
nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type
|
||||
}}</VChip
|
||||
>
|
||||
}}
|
||||
</VChip>
|
||||
<!-- 二级分类 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.category"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
v-if="nameTestResult?.media_info?.category"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.category }}
|
||||
</VChip>
|
||||
<!-- TMDBID -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.tmdb_id"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
class="me-1 mb-1"
|
||||
v-if="nameTestResult?.media_info?.tmdb_id"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.tmdb_id }}
|
||||
</VChip>
|
||||
@@ -139,8 +155,8 @@ const nameTest = async () => {
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.edition }}</VChip
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.resource_pix"
|
||||
variant="elevated"
|
||||
@@ -172,7 +188,10 @@ const nameTest = async () => {
|
||||
</VCardItem>
|
||||
</div>
|
||||
</div>
|
||||
<VAlert icon="mdi-alert-circle-outline" v-if="!nameTestResult?.meta_info?.name">
|
||||
<VAlert
|
||||
v-if="!nameTestResult?.meta_info?.name"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
识别失败,无法识别到有效信息!
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
@@ -1,158 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import api from "@/api";
|
||||
import douban from "@images/logos/douban.png";
|
||||
import github from "@images/logos/github.png";
|
||||
import slack from "@images/logos/slack.png";
|
||||
import telegram from "@images/logos/telegram.webp";
|
||||
import tmdb from "@images/logos/tmdb.png";
|
||||
import wechat from "@images/logos/wechat.png";
|
||||
import api from '@/api'
|
||||
import douban from '@images/logos/douban.png'
|
||||
import github from '@images/logos/github.png'
|
||||
import slack from '@images/logos/slack.png'
|
||||
import telegram from '@images/logos/telegram.webp'
|
||||
import tmdb from '@images/logos/tmdb.png'
|
||||
import wechat from '@images/logos/wechat.png'
|
||||
|
||||
interface Status {
|
||||
OK: string;
|
||||
Fail: string;
|
||||
Normal: string;
|
||||
Doing?: string;
|
||||
OK: string
|
||||
Fail: string
|
||||
Normal: string
|
||||
Doing?: string
|
||||
}
|
||||
|
||||
interface Address {
|
||||
image: string;
|
||||
name: string;
|
||||
url: string;
|
||||
proxy: boolean;
|
||||
status: keyof Status;
|
||||
time: string;
|
||||
message: string;
|
||||
btndisable: boolean;
|
||||
image: string
|
||||
name: string
|
||||
url: string
|
||||
proxy: boolean
|
||||
status: keyof Status
|
||||
time: string
|
||||
message: string
|
||||
btndisable: boolean
|
||||
}
|
||||
|
||||
// 测试集
|
||||
const targets = ref<Address[]>([
|
||||
{
|
||||
image: tmdb,
|
||||
name: "api.themoviedb.org",
|
||||
url: "https://api.themoviedb.org/3/movie/550?api_key={TMDBAPIKEY}",
|
||||
name: 'api.themoviedb.org',
|
||||
url: 'https://api.themoviedb.org/3/movie/550?api_key={TMDBAPIKEY}',
|
||||
proxy: true,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: tmdb,
|
||||
name: "api.tmdb.org",
|
||||
url: "https://api.tmdb.org",
|
||||
name: 'api.tmdb.org',
|
||||
url: 'https://api.tmdb.org',
|
||||
proxy: true,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: tmdb,
|
||||
name: "www.themoviedb.org",
|
||||
url: "https://www.themoviedb.org",
|
||||
name: 'www.themoviedb.org',
|
||||
url: 'https://www.themoviedb.org',
|
||||
proxy: true,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: telegram,
|
||||
name: "api.telegram.org",
|
||||
url: "https://api.telegram.org",
|
||||
name: 'api.telegram.org',
|
||||
url: 'https://api.telegram.org',
|
||||
proxy: true,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: wechat,
|
||||
name: "qyapi.weixin.qq.com",
|
||||
url: "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
|
||||
name: 'qyapi.weixin.qq.com',
|
||||
url: 'https://qyapi.weixin.qq.com/cgi-bin/gettoken',
|
||||
proxy: false,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: douban,
|
||||
name: "frodo.douban.com",
|
||||
url: "https://frodo.douban.com",
|
||||
name: 'frodo.douban.com',
|
||||
url: 'https://frodo.douban.com',
|
||||
proxy: false,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: slack,
|
||||
name: "slack.com",
|
||||
url: "https://slack.com",
|
||||
name: 'slack.com',
|
||||
url: 'https://slack.com',
|
||||
proxy: false,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
name: "github.com",
|
||||
url: "https://github.com",
|
||||
name: 'github.com',
|
||||
url: 'https://github.com',
|
||||
proxy: true,
|
||||
status: "Normal",
|
||||
time: "",
|
||||
message: "未测试",
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
const resolveStatusColor: Status = {
|
||||
OK: "success",
|
||||
Fail: "error",
|
||||
Normal: "",
|
||||
Doing: "warning",
|
||||
};
|
||||
OK: 'success',
|
||||
Fail: 'error',
|
||||
Normal: '',
|
||||
Doing: 'warning',
|
||||
}
|
||||
|
||||
// 调用API测试网络连接
|
||||
const netTest = async (index: number) => {
|
||||
async function netTest(index: number) {
|
||||
try {
|
||||
const target = targets.value[index];
|
||||
target.btndisable = true;
|
||||
target.status = "Doing";
|
||||
target.message = "测试中...";
|
||||
const result: { [key: string]: any } = await api.get("system/nettest", {
|
||||
const target = targets.value[index]
|
||||
|
||||
target.btndisable = true
|
||||
target.status = 'Doing'
|
||||
target.message = '测试中...'
|
||||
|
||||
const result: { [key: string]: any } = await api.get('system/nettest', {
|
||||
params: {
|
||||
url: target.url,
|
||||
proxy: target.proxy,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
target.status = "OK";
|
||||
target.message = "正常";
|
||||
} else {
|
||||
target.status = "Fail";
|
||||
target.message = result.message;
|
||||
target.status = 'OK'
|
||||
target.message = '正常'
|
||||
}
|
||||
target.time = result.data?.time;
|
||||
target.btndisable = false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
else {
|
||||
target.status = 'Fail'
|
||||
target.message = result.message
|
||||
}
|
||||
target.time = result.data?.time
|
||||
target.btndisable = false
|
||||
}
|
||||
};
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时测试所有连接
|
||||
onMounted(async () => {
|
||||
for (let i = 0; i < targets.value.length; i++) {
|
||||
await netTest(i);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < targets.value.length; i++)
|
||||
await netTest(i)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VList lines="two" border rounded>
|
||||
<template v-for="(target, index) of targets" :key="target.name">
|
||||
<VList
|
||||
lines="two"
|
||||
border
|
||||
rounded
|
||||
>
|
||||
<template
|
||||
v-for="(target, index) of targets"
|
||||
:key="target.name"
|
||||
>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VAvatar :image="target.image" />
|
||||
@@ -171,7 +183,10 @@ onMounted(async () => {
|
||||
<span class="ms-4">{{ target.message }}</span>
|
||||
</VBadge>
|
||||
|
||||
<span class="text-xs text-wrap text-disabled" v-if="target.time">
|
||||
<span
|
||||
v-if="target.time"
|
||||
class="text-xs text-wrap text-disabled"
|
||||
>
|
||||
{{ target.time }} ms
|
||||
</span>
|
||||
</VListItemSubtitle>
|
||||
@@ -179,10 +194,9 @@ onMounted(async () => {
|
||||
<VBtn
|
||||
size="small"
|
||||
icon="mdi-connection"
|
||||
@click="netTest(index)"
|
||||
:disabled="target.btndisable"
|
||||
>
|
||||
</VBtn>
|
||||
@click="netTest(index)"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
<VDivider v-if="index !== targets.length - 1" />
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
|
||||
// ...
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'vuex'],
|
||||
vueTemplate: true,
|
||||
}),
|
||||
VitePWA({ registerType: 'autoUpdate', injectRegister: 'script', manifest: false}),
|
||||
VitePWA({ registerType: 'autoUpdate', injectRegister: 'script', manifest: false }),
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
@@ -38,7 +38,6 @@ export default defineConfig({
|
||||
'@images': fileURLToPath(new URL('./src/assets/images/', import.meta.url)),
|
||||
'@styles': fileURLToPath(new URL('./src/styles/', import.meta.url)),
|
||||
'@configured-variables': fileURLToPath(new URL('./src/styles/variables/_template.scss', import.meta.url)),
|
||||
'@axios': fileURLToPath(new URL('./src/plugins/axios', import.meta.url)),
|
||||
'apexcharts': fileURLToPath(new URL('node_modules/apexcharts-clevision', import.meta.url)),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user