mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-04-13 18:11:03 +08:00
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": ["@antfu", "prettier"],
|
||||
"extends": ["@antfu", "prettier", "plugin:storybook/recommended"],
|
||||
"rules": {
|
||||
"antfu/if-newline": ["off"],
|
||||
"no-console": ["off"]
|
||||
"no-console": ["off"],
|
||||
"vue/custom-event-name-casing": ["off"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
/pnpm-lock.yaml
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
router-type.d.ts
|
||||
|
||||
24
.storybook/main.ts
Normal file
24
.storybook/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
import Unocss from 'unocss/vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal(config) {
|
||||
config.plugins?.push(Unocss());
|
||||
// Add other configuration here depending on your use case
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
17
.storybook/preview.ts
Normal file
17
.storybook/preview.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Preview } from '@storybook/vue3';
|
||||
import '@unocss/reset/tailwind-compat.css';
|
||||
import 'uno.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
14
README.md
14
README.md
@@ -1,15 +1,3 @@
|
||||
# Auto_Bangumi_WebUI
|
||||
|
||||
基于[Auto_Bangumi](https://github.com/EstrellaXD/Auto_Bangumi)的 WebUI
|
||||
|
||||
目前适配了`Auto_Bangumi`的 v1 版本 api
|
||||
|
||||
主要功能为:
|
||||
|
||||
- 查看订阅番剧(仅 mikan 源)
|
||||
- 订阅其他来源的新番 rss
|
||||
- 订阅旧番 rss
|
||||
- debug
|
||||
- 重置`Auto_Bangumi`的数据
|
||||
- 查看日志
|
||||
- config 设置页
|
||||
使用 Vue3 + TypeScript 构建的 [Auto_Bangumi](https://github.com/EstrellaXD/Auto_Bangumi) 的 WebUI
|
||||
|
||||
53
package.json
53
package.json
@@ -4,40 +4,57 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm lint && vite build",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"dev": "vite",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"axios": "^0.27.2",
|
||||
"element-plus": "^2.3.4",
|
||||
"modern-normalize": "^1.1.0",
|
||||
"pinia": "^2.0.35",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
"lodash": "^4.17.21",
|
||||
"naive-ui": "^2.34.4",
|
||||
"pinia": "^2.1.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.38.5",
|
||||
"@types/node": "^18.16.0",
|
||||
"@unocss/preset-rem-to-px": "^0.51.8",
|
||||
"@unocss/reset": "^0.51.8",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"eslint": "^8.39.0",
|
||||
"@antfu/eslint-config": "^0.38.6",
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@storybook/addon-essentials": "^7.0.12",
|
||||
"@storybook/addon-interactions": "^7.0.12",
|
||||
"@storybook/addon-links": "^7.0.12",
|
||||
"@storybook/blocks": "^7.0.12",
|
||||
"@storybook/testing-library": "0.0.14-next.2",
|
||||
"@storybook/vue3": "^7.0.12",
|
||||
"@storybook/vue3-vite": "^7.0.12",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.14",
|
||||
"@unocss/preset-rem-to-px": "^0.51.13",
|
||||
"@unocss/reset": "^0.51.13",
|
||||
"@vitejs/plugin-vue": "^4.2.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.62.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.62.1",
|
||||
"storybook": "^7.0.12",
|
||||
"typescript": "^4.9.5",
|
||||
"unocss": "^0.51.8",
|
||||
"unocss": "^0.51.13",
|
||||
"unplugin-auto-import": "^0.10.3",
|
||||
"unplugin-vue-components": "^0.21.2",
|
||||
"vite": "^3.2.6",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"unplugin-vue-router": "^0.6.4",
|
||||
"vite": "^4.3.5",
|
||||
"vitest": "^0.30.1",
|
||||
"vue-tsc": "^0.38.9"
|
||||
"vue-tsc": "^1.6.4"
|
||||
}
|
||||
}
|
||||
|
||||
7002
pnpm-lock.yaml
generated
7002
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
30
src/App.vue
30
src/App.vue
@@ -1,15 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn';
|
||||
import {
|
||||
type GlobalThemeOverrides,
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
} from 'naive-ui';
|
||||
|
||||
const { getStatus, onUpdate } = programStore();
|
||||
getStatus();
|
||||
onUpdate();
|
||||
const theme: GlobalThemeOverrides = {
|
||||
Spin: {
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
|
||||
const { refresh } = useAuth();
|
||||
refresh();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<RouterView />
|
||||
</el-config-provider>
|
||||
<NConfigProvider :theme-overrides="theme">
|
||||
<NMessageProvider>
|
||||
<RouterView></RouterView>
|
||||
</NMessageProvider>
|
||||
</NConfigProvider>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style/transition';
|
||||
@import './style/global';
|
||||
</style>
|
||||
|
||||
41
src/api/auth.ts
Normal file
41
src/api/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { LoginSuccess, Logout, Update } from '#/auth';
|
||||
|
||||
export const apiAuth = {
|
||||
async login(username: string, password: string) {
|
||||
const formData = new URLSearchParams({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
const { data } = await axios.post<LoginSuccess>(
|
||||
'api/v1/auth/login',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
const { data } = await axios.get<LoginSuccess>('api/v1/auth/refresh_token');
|
||||
return data;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const { data } = await axios.get<Logout>('api/v1/auth/logout');
|
||||
return data.message === 'logout success';
|
||||
},
|
||||
|
||||
async update(username: string, password: string) {
|
||||
const { data } = await axios.post<Update>('api/v1/auth/update', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -1,25 +1,91 @@
|
||||
import axios from 'axios';
|
||||
import type { BangumiItem } from '#/bangumi';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
/**
|
||||
* 添加番剧订阅
|
||||
* @param type 'new' 添加新番, ’old‘ 添加旧番
|
||||
* @param rss_link
|
||||
*/
|
||||
function addBangumi(type: string, rss_link: string) {
|
||||
if (type === 'new') {
|
||||
return axios.post('api/v1/subscribe', { rss_link });
|
||||
} else if (type === 'old') {
|
||||
return axios.post('api/v1/collection', { rss_link });
|
||||
} else {
|
||||
console.error('type错误, type应为 new 或 old');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export const apiBangumi = {
|
||||
/**
|
||||
* 获取所有 bangumi 数据
|
||||
* @returns 所有 bangumi 数据
|
||||
*/
|
||||
async getAll() {
|
||||
const { data } = await axios.get<BangumiRule[]>('api/v1/bangumi/getAll');
|
||||
|
||||
/**
|
||||
* 获取订阅番剧数据
|
||||
*/
|
||||
const getABData = () => axios.get<BangumiItem[]>('api/v1/bangumi/getAll');
|
||||
return data;
|
||||
},
|
||||
|
||||
export { addBangumi, getABData };
|
||||
/**
|
||||
* 获取指定 bangumiId 的规则
|
||||
* @param bangumiId bangumi id
|
||||
* @returns 指定 bangumi 的规则
|
||||
*/
|
||||
async getRule(bangumiId: number) {
|
||||
const { data } = await axios.get<BangumiRule>(
|
||||
`api/v1/bangumi/getRule/${bangumiId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新指定 bangumiId 的规则
|
||||
* @param bangumiData - 需要更新的规则
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async updateRule(bangumiRule: BangumiRule) {
|
||||
const { data } = await axios.post<{
|
||||
msg: string;
|
||||
status: 'success';
|
||||
}>('api/v1/bangumi/updateRule', bangumiRule);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定 bangumiId 的数据库规则,会在重新匹配到后重建
|
||||
* @param bangumiId - 需要删除的 bangumi 的 id
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async deleteRule(bangumiId: number) {
|
||||
const { data } = await axios.delete(
|
||||
`api/v1/bangumi/deleteRule/${bangumiId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除指定 bangumiId 的规则。如果 file 为 true,则同时删除关联文件。
|
||||
* @param bangumiId - 需要删除规则的 bangumi 的 id。
|
||||
* @param file - 是否同时删除关联文件。
|
||||
* @returns axios 请求返回的数据
|
||||
*/
|
||||
async disableRule(bangumiId: number, file: boolean) {
|
||||
const { data } = await axios.delete<{
|
||||
status: 'success';
|
||||
msg: string;
|
||||
}>(`api/v1/bangumi/disableRule/${bangumiId}`, {
|
||||
params: {
|
||||
file,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 启用指定 bangumiId 的规则
|
||||
* @param bangumiId - 需要启用的 bangumi 的 id
|
||||
*/
|
||||
async enableRule(bangumiId: number) {
|
||||
const { data } = await axios.get<{
|
||||
status: 'success';
|
||||
msg: string;
|
||||
}>(`api/v1/bangumi/enableRule/${bangumiId}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置所有 bangumi 数据
|
||||
*/
|
||||
async resetAll() {
|
||||
const { data } = await axios.post<{
|
||||
status: 'ok';
|
||||
}>('api/v1/bangumi/resetAll');
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
25
src/api/check.ts
Normal file
25
src/api/check.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const apiCheck = {
|
||||
/**
|
||||
* 检测下载器
|
||||
*/
|
||||
async downloader() {
|
||||
const { data } = await axios.get('api/v1/check/downloader');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检测 RSS
|
||||
*/
|
||||
async rss() {
|
||||
const { data } = await axios.get('api/v1/check/rss');
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 检测所有
|
||||
*/
|
||||
async all() {
|
||||
const { data } = await axios.get('api/v1/check');
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -1,15 +1,23 @@
|
||||
import axios from 'axios';
|
||||
import type { Config } from '#/config';
|
||||
|
||||
export async function setConfig(newConfig: Config) {
|
||||
const { data } = await axios.post<{
|
||||
message: 'Success' | 'Failed to update config';
|
||||
}>('api/v1/updateConfig', newConfig);
|
||||
export const apiConfig = {
|
||||
/**
|
||||
* 获取 config 数据
|
||||
*/
|
||||
async getConfig() {
|
||||
const { data } = await axios.get<Config>('api/v1/getConfig');
|
||||
return data;
|
||||
},
|
||||
|
||||
return data.message === 'Success';
|
||||
}
|
||||
/**
|
||||
* 更新 config 数据
|
||||
* @param newConfig - 需要更新的 config
|
||||
*/
|
||||
async updateConfig(newConfig: Config) {
|
||||
const { data } = await axios.post<{
|
||||
message: 'Success' | 'Failed to update config';
|
||||
}>('api/v1/updateConfig', newConfig);
|
||||
|
||||
export async function getConfig() {
|
||||
const { data } = await axios.get<Config>('api/v1/getConfig');
|
||||
return data;
|
||||
}
|
||||
return data.message === 'Success';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 获取AB的日志
|
||||
*/
|
||||
async function getABLog() {
|
||||
const { data } = await axios.get('api/v1/log');
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
* 重置 AB 的数据,程序会在下一轮检索中重新添加 RSS 订阅信息。
|
||||
*/
|
||||
const resetRule = () => axios.get('api/v1/resetRule');
|
||||
|
||||
export { getABLog, resetRule };
|
||||
61
src/api/download.ts
Normal file
61
src/api/download.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
interface Status {
|
||||
status: 'Success';
|
||||
}
|
||||
|
||||
interface AnalysisError {
|
||||
status: 'Failed to parse link';
|
||||
}
|
||||
|
||||
export const apiDownload = {
|
||||
/**
|
||||
* 解析 RSS 链接
|
||||
* @param rss_link - RSS 链接
|
||||
*/
|
||||
async analysis(rss_link: string) {
|
||||
const fetchResult = createEventHook<BangumiRule>();
|
||||
const fetchError = createEventHook<AnalysisError>();
|
||||
|
||||
axios
|
||||
.post<any>('api/v1/download/analysis', {
|
||||
rss_link,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.status) {
|
||||
fetchError.trigger(data as AnalysisError);
|
||||
} else {
|
||||
fetchResult.trigger(data as BangumiRule);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
onResult: fetchResult.on,
|
||||
onError: fetchError.on,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 旧番
|
||||
* @param bangumiData - Bangumi 数据
|
||||
*/
|
||||
async collection(bangumiData: BangumiRule) {
|
||||
const { data } = await axios.post<Status>(
|
||||
'api/v1/download/collection',
|
||||
bangumiData
|
||||
);
|
||||
return data.status === 'Success';
|
||||
},
|
||||
|
||||
/**
|
||||
* 新番
|
||||
* @param bangumiData - Bangumi 数据
|
||||
*/
|
||||
async subscribe(bangumiData: BangumiRule) {
|
||||
const { data } = await axios.post<Status>(
|
||||
'api/v1/download/subscribe',
|
||||
bangumiData
|
||||
);
|
||||
return data.status === 'Success';
|
||||
},
|
||||
};
|
||||
11
src/api/log.ts
Normal file
11
src/api/log.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const apiLog = {
|
||||
async getLog() {
|
||||
const { data } = await axios.get<string>('api/v1/log');
|
||||
return data;
|
||||
},
|
||||
|
||||
async clearLog() {
|
||||
const { data } = await axios.get<{ status: 'ok' }>('api/v1/log/clear');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
};
|
||||
@@ -1,21 +1,47 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/** 重启 */
|
||||
export async function appRestart() {
|
||||
const { data } = await axios.get<{ status: 'ok' }>('api/v1/restart');
|
||||
return data.status === 'ok';
|
||||
interface Success {
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
/** 启动 */
|
||||
export const appStart = () => axios.get('api/v1/start');
|
||||
export const apiProgram = {
|
||||
/**
|
||||
* 重启
|
||||
*/
|
||||
async restart() {
|
||||
const { data } = await axios.get<Success>('api/v1/restart');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/** 停止 */
|
||||
export const appStop = () => axios.get('api/v1/stop');
|
||||
/**
|
||||
* 启动
|
||||
*/
|
||||
async start() {
|
||||
const { data } = await axios.get<Success>('api/v1/start');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/** 状态 */
|
||||
export async function appStatus() {
|
||||
const { data } = await axios.get<{ status: 'stop' | 'running' }>(
|
||||
'api/v1/status'
|
||||
);
|
||||
return data.status !== 'stop';
|
||||
}
|
||||
/**
|
||||
* 停止
|
||||
*/
|
||||
async stop() {
|
||||
const { data } = await axios.get<Success>('api/v1/stop');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
async status() {
|
||||
const { data } = await axios.get<{ status: 'running' | 'stop' }>(
|
||||
'api/v1/status'
|
||||
);
|
||||
return data.status === 'running';
|
||||
},
|
||||
|
||||
/**
|
||||
* 终止
|
||||
*/
|
||||
async shutdown() {
|
||||
const { data } = await axios.get<Success>('api/v1/shutdown');
|
||||
return data.status === 'ok';
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
221
src/auto-imports.d.ts
vendored
221
src/auto-imports.d.ts
vendored
@@ -5,33 +5,64 @@ declare global {
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const afterAll: typeof import('vitest')['afterAll']
|
||||
const afterEach: typeof import('vitest')['afterEach']
|
||||
const apiAuth: typeof import('./api/auth')['apiAuth']
|
||||
const apiBangumi: typeof import('./api/bangumi')['apiBangumi']
|
||||
const apiCheck: typeof import('./api/check')['apiCheck']
|
||||
const apiConfig: typeof import('./api/config')['apiConfig']
|
||||
const apiDownload: typeof import('./api/download')['apiDownload']
|
||||
const apiLog: typeof import('./api/log')['apiLog']
|
||||
const apiProgram: typeof import('./api/program')['apiProgram']
|
||||
const assert: typeof import('vitest')['assert']
|
||||
const bangumiStore: typeof import('./store/bangumi')['bangumiStore']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const axios: typeof import('./utils/axios')['axios']
|
||||
const beforeAll: typeof import('vitest')['beforeAll']
|
||||
const beforeEach: typeof import('vitest')['beforeEach']
|
||||
const chai: typeof import('vitest')['chai']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const configStore: typeof import('./store/config')['configStore']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineLoader: typeof import('vue-router/auto')['defineLoader']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const describe: typeof import('vitest')['describe']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const expect: typeof import('vitest')['expect']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const it: typeof import('vitest')['it']
|
||||
const logStore: typeof import('./store/log')['logStore']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -41,23 +72,41 @@ declare global {
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const programStore: typeof import('./store/program')['programStore']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
@@ -65,22 +114,182 @@ declare global {
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const suite: typeof import('vitest')['suite']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const test: typeof import('vitest')['test']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useApi: typeof import('./hooks/useApi')['useApi']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('./hooks/useAuth')['useAuth']
|
||||
const useBangumiStore: typeof import('./store/bangumi')['useBangumiStore']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfigStore: typeof import('./store/config')['useConfigStore']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useLogStore: typeof import('./store/log')['useLogStore']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMessage: typeof import('./hooks/useMessage')['useMessage']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePlayerStore: typeof import('./store/player')['usePlayerStore']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const useProgramStore: typeof import('./store/program')['useProgramStore']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const vi: typeof import('vitest')['vi']
|
||||
const vitest: typeof import('vitest')['vitest']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
|
||||
59
src/components.d.ts
vendored
59
src/components.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
@@ -7,32 +9,35 @@ export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
ConfigFormCol: typeof import('./components/ConfigFormCol.vue')['default']
|
||||
ConfigFormRow: typeof import('./components/ConfigFormRow.vue')['default']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
AbAdd: typeof import('./components/basic/ab-add.vue')['default']
|
||||
AbAddBangumi: typeof import('./components/ab-add-bangumi.vue')['default']
|
||||
AbBangumiCard: typeof import('./components/ab-bangumi-card.vue')['default']
|
||||
AbButton: typeof import('./components/basic/ab-button.vue')['default']
|
||||
AbChangeAccount: typeof import('./components/ab-change-account.vue')['default']
|
||||
AbCheckbox: typeof import('./components/basic/ab-checkbox.vue')['default']
|
||||
AbContainer: typeof import('./components/ab-container.vue')['default']
|
||||
AbEditRule: typeof import('./components/ab-edit-rule.vue')['default']
|
||||
AbFoldPanel: typeof import('./components/ab-fold-panel.vue')['default']
|
||||
AbLabel: typeof import('./components/ab-label.vue')['default']
|
||||
AbPageTitle: typeof import('./components/basic/ab-page-title.vue')['default']
|
||||
AbPopup: typeof import('./components/ab-popup.vue')['default']
|
||||
AbRule: typeof import('./components/ab-rule.vue')['default']
|
||||
AbSearch: typeof import('./components/basic/ab-search.vue')['default']
|
||||
AbSelect: typeof import('./components/basic/ab-select.vue')['default']
|
||||
AbSetting: typeof import('./components/ab-setting.vue')['default']
|
||||
AbSidebar: typeof import('./components/layout/ab-sidebar.vue')['default']
|
||||
AbStatus: typeof import('./components/basic/ab-status.vue')['default']
|
||||
AbStatusBar: typeof import('./components/ab-status-bar.vue')['default']
|
||||
AbSwitch: typeof import('./components/basic/ab-switch.vue')['default']
|
||||
AbTopbar: typeof import('./components/layout/ab-topbar.vue')['default']
|
||||
ConfigDownload: typeof import('./components/setting/config-download.vue')['default']
|
||||
ConfigManage: typeof import('./components/setting/config-manage.vue')['default']
|
||||
ConfigNormal: typeof import('./components/setting/config-normal.vue')['default']
|
||||
ConfigNotification: typeof import('./components/setting/config-notification.vue')['default']
|
||||
ConfigParser: typeof import('./components/setting/config-parser.vue')['default']
|
||||
ConfigPlayer: typeof import('./components/setting/config-player.vue')['default']
|
||||
ConfigProxy: typeof import('./components/setting/config-proxy.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ShowResults: typeof import('./components/ShowResults.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
label: string;
|
||||
prop?: string | string[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
|
||||
<el-form-item :label="label" :prop="prop ?? ''">
|
||||
<slot></slot>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-collapse-item>
|
||||
<template #title>
|
||||
<span font-bold text-base>{{ title }}</span>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<slot></slot>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
results: object | null;
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
function handleClose() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function open() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dia-log">
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="title"
|
||||
width="30%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div>
|
||||
<div class="results">
|
||||
{{ results === null ? 'null' : results }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
.dia-log {
|
||||
.results {
|
||||
padding: 1em;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
146
src/components/ab-add-bangumi.vue
Normal file
146
src/components/ab-add-bangumi.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const { getAll } = useBangumiStore();
|
||||
const show = defineModel('show', { default: false });
|
||||
|
||||
const rss = ref('');
|
||||
const message = useMessage();
|
||||
const rule = ref<BangumiRule>({
|
||||
added: false,
|
||||
deleted: false,
|
||||
dpi: '',
|
||||
eps_collect: false,
|
||||
filter: [],
|
||||
group_name: '',
|
||||
id: 0,
|
||||
official_title: '',
|
||||
offset: 0,
|
||||
poster_link: '',
|
||||
rss_link: [],
|
||||
rule_name: '',
|
||||
save_path: '',
|
||||
season: 1,
|
||||
season_raw: '',
|
||||
source: null,
|
||||
subtitle: '',
|
||||
title_raw: '',
|
||||
year: null,
|
||||
});
|
||||
const analysis = reactive({
|
||||
loading: false,
|
||||
next: false,
|
||||
});
|
||||
|
||||
const loading = reactive({
|
||||
collect: false,
|
||||
subscribe: false,
|
||||
});
|
||||
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
rss.value = '';
|
||||
setTimeout(() => {
|
||||
analysis.next = false;
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
async function analyser() {
|
||||
if (rss.value === '') {
|
||||
message.error('Please enter the RSS link!');
|
||||
} else {
|
||||
try {
|
||||
analysis.loading = true;
|
||||
const { onError, onResult } = await apiDownload.analysis(rss.value);
|
||||
onResult((data) => {
|
||||
rule.value = data;
|
||||
analysis.loading = false;
|
||||
analysis.next = true;
|
||||
console.log('rule', data);
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
message.error(err.status);
|
||||
analysis.loading = false;
|
||||
console.log('error', err);
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('Failed to analyser!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function collect() {
|
||||
if (rule.value) {
|
||||
try {
|
||||
loading.collect = true;
|
||||
const res = await apiDownload.collection(rule.value);
|
||||
loading.collect = false;
|
||||
if (res) {
|
||||
message.success('Collect Success!');
|
||||
getAll();
|
||||
show.value = false;
|
||||
} else {
|
||||
message.error('Collect Failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Collect Error!');
|
||||
}
|
||||
}
|
||||
}
|
||||
async function subscribe() {
|
||||
if (rule.value) {
|
||||
try {
|
||||
loading.subscribe = true;
|
||||
const res = await apiDownload.subscribe(rule.value);
|
||||
loading.subscribe = false;
|
||||
if (res) {
|
||||
message.success('Subscribe Success!');
|
||||
getAll();
|
||||
show.value = false;
|
||||
} else {
|
||||
message.error('Subscribe Failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('Subscribe Error!');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" title="Add Bangumi" css="w-360px">
|
||||
<div v-if="!analysis.next" space-y-12px>
|
||||
<ab-setting
|
||||
v-model:data="rss"
|
||||
label="RSS Link"
|
||||
type="input"
|
||||
:prop="{
|
||||
placeholder: 'Please enter the RSS link',
|
||||
}"
|
||||
:bottom-line="true"
|
||||
></ab-setting>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" :loading="analysis.loading" @click="analyser"
|
||||
>Analyse</ab-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<ab-rule v-model:rule="rule"></ab-rule>
|
||||
|
||||
<div flex="~ justify-end" space-x-10px>
|
||||
<ab-button size="small" :loading="loading.collect" @click="collect"
|
||||
>Collect</ab-button
|
||||
>
|
||||
<ab-button size="small" :loading="loading.subscribe" @click="subscribe"
|
||||
>Subscribe</ab-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</template>
|
||||
65
src/components/ab-bangumi-card.vue
Normal file
65
src/components/ab-bangumi-card.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { ErrorPicture, Write } from '@icon-park/vue-next';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
poster: string;
|
||||
name: string;
|
||||
season: number;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-150px is-btn @click="() => $emit('click')">
|
||||
<div rounded-4px overflow-hidden poster-shandow rel>
|
||||
<div w-full h-210px>
|
||||
<template v-if="poster !== ''">
|
||||
<img :src="`https://mikanani.me${poster}`" alt="poster" wh-full />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div wh-full f-cer border="1 white">
|
||||
<ErrorPicture theme="outline" size="24" fill="#333" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
abs
|
||||
f-cer
|
||||
z-1
|
||||
inset-0
|
||||
opacity-0
|
||||
transition-all
|
||||
duration-300
|
||||
hover:backdrop-blur-2px
|
||||
hover:bg-white
|
||||
hover:bg-opacity-30
|
||||
hover:opacity-100
|
||||
active:duration-0
|
||||
active:bg-opacity-60
|
||||
class="group"
|
||||
>
|
||||
<div
|
||||
text-white
|
||||
rounded="1/2"
|
||||
wh-44px
|
||||
f-cer
|
||||
bg-theme-row
|
||||
class="group-active:poster-pen-active"
|
||||
>
|
||||
<Write size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div px-4px py-8px>
|
||||
<div text-h3 truncate>{{ name }}</div>
|
||||
<div text-main>Season {{ season }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
37
src/components/ab-change-account.vue
Normal file
37
src/components/ab-change-account.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
const show = defineModel('show', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const { user, update } = useAuth();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" title="Change Account" css="w-365px">
|
||||
<div space-y-16px>
|
||||
<ab-label label="Username">
|
||||
<input
|
||||
v-model="user.username"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Password">
|
||||
<input
|
||||
v-model="user.password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" @click="update">Update</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</template>
|
||||
33
src/components/ab-container.vue
Normal file
33
src/components/ab-container.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div rounded-10px overflow-hidden>
|
||||
<div
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
fx-cer
|
||||
px-20px
|
||||
h-45px
|
||||
justify-between
|
||||
select-none
|
||||
>
|
||||
<div text-h2>{{ title }}</div>
|
||||
|
||||
<slot name="title-right"></slot>
|
||||
</div>
|
||||
|
||||
<div p-20px bg-white>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
96
src/components/ab-edit-rule.vue
Normal file
96
src/components/ab-edit-rule.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'disable', opts: { id: number; deleteFile: boolean }): void;
|
||||
(e: 'apply', rule: BangumiRule): void;
|
||||
(e: 'enable', id: number): void;
|
||||
}>();
|
||||
|
||||
const show = defineModel('show', { default: false });
|
||||
const rule = defineModel<BangumiRule>('rule', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const deleteRuleDialog = ref(false);
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
deleteRuleDialog.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const close = () => (show.value = false);
|
||||
|
||||
function emitDisable(deleteFile: boolean) {
|
||||
emit('disable', {
|
||||
id: rule.value.id,
|
||||
deleteFile,
|
||||
});
|
||||
}
|
||||
function emitApply() {
|
||||
emit('apply', rule.value);
|
||||
}
|
||||
|
||||
function emitEnable() {
|
||||
emit('enable', rule.value.id);
|
||||
}
|
||||
|
||||
const popupTitle = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return 'Enable Rule';
|
||||
} else {
|
||||
return 'Edit Rule';
|
||||
}
|
||||
});
|
||||
|
||||
const boxSize = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return 'w-300px';
|
||||
} else {
|
||||
return 'w-380px';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-popup v-model:show="show" :title="popupTitle" :css="boxSize">
|
||||
<div v-if="rule.deleted">
|
||||
<div>Do you want to enable this rule?</div>
|
||||
|
||||
<div line my-8px></div>
|
||||
|
||||
<div fx-cer justify-center space-x-10px>
|
||||
<ab-button size="small" type="warn" @click="() => emitEnable()"
|
||||
>Yes</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="() => close()">No</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else space-y-12px>
|
||||
<ab-rule v-model:rule="rule"></ab-rule>
|
||||
|
||||
<div fx-cer justify-end space-x-10px>
|
||||
<ab-button
|
||||
size="small"
|
||||
type="warn"
|
||||
@click="() => (deleteRuleDialog = true)"
|
||||
>Disable</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="emitApply">Apply</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ab-popup v-model:show="deleteRuleDialog" title="Delete">
|
||||
<div>Delete Local File?</div>
|
||||
<div line my-8px></div>
|
||||
|
||||
<div fx-cer justify-center space-x-10px>
|
||||
<ab-button size="small" type="warn" @click="() => emitDisable(true)"
|
||||
>Yes</ab-button
|
||||
>
|
||||
<ab-button size="small" @click="() => emitDisable(false)">No</ab-button>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</ab-popup>
|
||||
</template>
|
||||
41
src/components/ab-fold-panel.vue
Normal file
41
src/components/ab-fold-panel.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { Down, Up } from '@icon-park/vue-next';
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Disclosure v-slot="{ open }">
|
||||
<div rounded-10px overflow-hidden h-max>
|
||||
<DisclosureButton
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
fx-cer
|
||||
px-20px
|
||||
h-45px
|
||||
justify-between
|
||||
>
|
||||
<div text-h2>{{ title }}</div>
|
||||
|
||||
<Component :is="open ? Up : Down" size="24" />
|
||||
</DisclosureButton>
|
||||
|
||||
<div bg-white py-20px :class="[open ? 'px-20px' : 'px-8px']">
|
||||
<div v-show="!open" line my-12px></div>
|
||||
|
||||
<DisclosurePanel>
|
||||
<slot></slot>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure>
|
||||
</template>
|
||||
18
src/components/ab-label.vue
Normal file
18
src/components/ab-label.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
}>(),
|
||||
{
|
||||
label: '',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ items-start" justify-between>
|
||||
<div>{{ label }}</div>
|
||||
|
||||
<slot> </slot>
|
||||
</div>
|
||||
</template>
|
||||
69
src/components/ab-popup.vue
Normal file
69
src/components/ab-popup.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
} from '@headlessui/vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
maskClick?: boolean;
|
||||
css?: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
maskClick: true,
|
||||
css: '',
|
||||
}
|
||||
);
|
||||
|
||||
const show = defineModel('show', { default: false });
|
||||
|
||||
function close() {
|
||||
if (props.maskClick) {
|
||||
show.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot appear :show="show" as="template">
|
||||
<Dialog as="div" class="relative z-10" @close="close">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-center justify-center p-4 text-center"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel>
|
||||
<ab-container :title="title" :class="[css]">
|
||||
<slot></slot>
|
||||
</ab-container>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
64
src/components/ab-rule.vue
Normal file
64
src/components/ab-rule.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const rule = defineModel<BangumiRule>('rule', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const items: SettingItem<BangumiRule>[] = [
|
||||
{
|
||||
configKey: 'official_title',
|
||||
label: 'Officical Ttile',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'year',
|
||||
label: 'Year',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'season',
|
||||
label: 'Season',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'offset',
|
||||
label: 'Offset',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'filter',
|
||||
label: 'Exclude',
|
||||
type: 'dynamic-tags',
|
||||
bottomLine: true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="rule[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</template>
|
||||
45
src/components/ab-setting.vue
Normal file
45
src/components/ab-setting.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { NDynamicTags } from 'naive-ui';
|
||||
import type { AbSettingProps } from '#/components';
|
||||
|
||||
withDefaults(defineProps<AbSettingProps>(), {
|
||||
css: '',
|
||||
bottomLine: false,
|
||||
});
|
||||
|
||||
const data = defineModel<any>('data');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ab-label :label="label">
|
||||
<AbSwitch
|
||||
v-if="type === 'switch'"
|
||||
v-model:checked="data"
|
||||
v-bind="prop"
|
||||
:class="css"
|
||||
></AbSwitch>
|
||||
|
||||
<AbSelect
|
||||
v-else-if="type === 'select'"
|
||||
v-model="data"
|
||||
v-bind="prop"
|
||||
:class="css"
|
||||
></AbSelect>
|
||||
|
||||
<input
|
||||
v-else-if="type === 'input'"
|
||||
v-model="data"
|
||||
ab-input
|
||||
:class="css"
|
||||
v-bind="prop"
|
||||
/>
|
||||
|
||||
<div v-else-if="type === 'dynamic-tags'" max-w-200px>
|
||||
<NDynamicTags v-model:value="data" size="small"></NDynamicTags>
|
||||
</div>
|
||||
</ab-label>
|
||||
|
||||
<div v-if="bottomLine" line my-12px></div>
|
||||
</div>
|
||||
</template>
|
||||
80
src/components/ab-status-bar.vue
Normal file
80
src/components/ab-status-bar.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
|
||||
import { AddOne, More } from '@icon-park/vue-next';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
running: boolean;
|
||||
items: {
|
||||
id: number;
|
||||
icon: any;
|
||||
label: string;
|
||||
handle?: () => void | Promise<void>;
|
||||
}[];
|
||||
}>(),
|
||||
{
|
||||
running: false,
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits(['clickAdd']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu>
|
||||
<div rel>
|
||||
<div fx-cer space-x-16px>
|
||||
<AddOne
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
@click="() => $emit('clickAdd')"
|
||||
/>
|
||||
|
||||
<MenuButton bg-transparent is-btn btn-click>
|
||||
<More theme="outline" size="24" fill="#fff" />
|
||||
</MenuButton>
|
||||
|
||||
<ab-status :running="running" />
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
abs
|
||||
top-50px
|
||||
w-120px
|
||||
rounded-8px
|
||||
bg-white
|
||||
overflow-hidden
|
||||
shadow
|
||||
z-99
|
||||
>
|
||||
<MenuItem v-for="i in items" :key="i.id" v-slot="{ active }">
|
||||
<div
|
||||
w-full
|
||||
h-32px
|
||||
px-12px
|
||||
fx-cer
|
||||
justify-between
|
||||
is-btn
|
||||
hover:text-white
|
||||
hover:bg-primary
|
||||
class="group"
|
||||
:class="[active ? 'text-white bg-theme-row' : 'text-black']"
|
||||
@click="i.handle"
|
||||
>
|
||||
<div text-main>{{ i.label }}</div>
|
||||
|
||||
<div
|
||||
class="group-hover:text-white"
|
||||
:class="[active ? 'text-white' : 'text-primary']"
|
||||
>
|
||||
<Component :is="i.icon" size="16"></Component>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</template>
|
||||
22
src/components/basic/ab-add.stories.ts
Normal file
22
src/components/basic/ab-add.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbAdd from './ab-add.vue';
|
||||
|
||||
const meta: Meta<typeof AbAdd> = {
|
||||
title: 'basic/ab-add',
|
||||
component: AbAdd,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbAdd>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbAdd },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-add v-bind="args"></ab-add>',
|
||||
}),
|
||||
};
|
||||
41
src/components/basic/ab-add.vue
Normal file
41
src/components/basic/ab-add.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
rounded="1/2"
|
||||
wh-36px
|
||||
f-cer
|
||||
rel
|
||||
transition-colors
|
||||
class="box"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div class="line" abs></div>
|
||||
<div class="line" abs rotate-90></div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$normal: #493475;
|
||||
$hover: #756596;
|
||||
|
||||
.box {
|
||||
background: $normal;
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $normal;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
32
src/components/basic/ab-button.stories.ts
Normal file
32
src/components/basic/ab-button.stories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbButton from './ab-button.vue';
|
||||
|
||||
const meta: Meta<typeof AbButton> = {
|
||||
title: 'basic/ab-button',
|
||||
component: AbButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'warn'],
|
||||
},
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['big', 'normal', 'small'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbButton>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbButton },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-button v-bind="args">button</ab-button>',
|
||||
}),
|
||||
};
|
||||
70
src/components/basic/ab-button.vue
Normal file
70
src/components/basic/ab-button.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { NSpin } from 'naive-ui';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'primary' | 'warn';
|
||||
size?: 'big' | 'normal' | 'small';
|
||||
link?: string | null;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
type: 'primary',
|
||||
size: 'normal',
|
||||
link: null,
|
||||
loading: false,
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const buttonSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'rounded-10px text-h1 w-276px h-55px text-h1';
|
||||
case 'normal':
|
||||
return 'rounded-6px w-170px h-36px';
|
||||
case 'small':
|
||||
return 'rounded-6px w-86px h-28px text-main';
|
||||
}
|
||||
});
|
||||
|
||||
const loadingSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'large';
|
||||
case 'normal':
|
||||
return 'small';
|
||||
case 'small':
|
||||
return 18;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
text-white
|
||||
outline-none
|
||||
f-cer
|
||||
:class="[`type-${type}`, buttonSize]"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<NSpin :show="loading" :size="loadingSize">
|
||||
<slot></slot>
|
||||
</NSpin>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type {
|
||||
&-primary {
|
||||
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
|
||||
}
|
||||
|
||||
&-warn {
|
||||
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/components/basic/ab-checkbox.stories.ts
Normal file
22
src/components/basic/ab-checkbox.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbCheckbox from './ab-checkbox.vue';
|
||||
|
||||
const meta: Meta<typeof AbCheckbox> = {
|
||||
title: 'basic/ab-checkbox',
|
||||
component: AbCheckbox,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbCheckbox>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbCheckbox },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-checkbox v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
45
src/components/basic/ab-checkbox.vue
Normal file
45
src/components/basic/ab-checkbox.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { Switch } from '@headlessui/vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
small?: boolean;
|
||||
}>(),
|
||||
{
|
||||
small: false,
|
||||
}
|
||||
);
|
||||
|
||||
const checked = defineModel<boolean>({ default: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div flex items-center space-x-8px is-btn>
|
||||
<slot name="before"></slot>
|
||||
|
||||
<div
|
||||
rounded-4px
|
||||
rel
|
||||
f-cer
|
||||
bg-white
|
||||
border="3px #3c239f"
|
||||
:class="[small ? 'wh-16px' : 'wh-32px', !checked && 'group']"
|
||||
>
|
||||
<div
|
||||
rounded-2px
|
||||
transition-all
|
||||
duration-300
|
||||
:class="[
|
||||
small ? 'wh-8px' : 'wh-16px',
|
||||
checked ? 'bg-[#3c239f]' : 'bg-transparent',
|
||||
]"
|
||||
group-hover:bg="#cccad4"
|
||||
group-active:bg="#3c239f"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
22
src/components/basic/ab-page-title.stories.ts
Normal file
22
src/components/basic/ab-page-title.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbPageTitle from './ab-page-title.vue';
|
||||
|
||||
const meta: Meta<typeof AbPageTitle> = {
|
||||
title: 'basic/ab-PageTitle',
|
||||
component: AbPageTitle,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbPageTitle>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbPageTitle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-page-title v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
17
src/components/basic/ab-page-title.vue
Normal file
17
src/components/basic/ab-page-title.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>(),
|
||||
{
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fx-cer space-x-12px>
|
||||
<div text-h1>{{ title }}</div>
|
||||
<div w-160px h-3px bg-theme-row rounded-full></div>
|
||||
</div>
|
||||
</template>
|
||||
22
src/components/basic/ab-search.stories.ts
Normal file
22
src/components/basic/ab-search.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSearch from './ab-search.vue';
|
||||
|
||||
const meta: Meta<typeof AbSearch> = {
|
||||
title: 'basic/ab-search',
|
||||
component: AbSearch,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSearch>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSearch },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-search v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
58
src/components/basic/ab-search.vue
Normal file
58
src/components/basic/ab-search.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import { Search } from '@icon-park/vue-next';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
value: '',
|
||||
placeholder: '',
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:value', 'click-search']);
|
||||
|
||||
function onInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
emit('update:value', input.value);
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
emit('click-search', props.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
bg="#7752B4"
|
||||
text-white
|
||||
fx-cer
|
||||
rounded-12px
|
||||
h-36px
|
||||
px-12px
|
||||
space-x-12px
|
||||
w-276px
|
||||
focus-within:w-396px
|
||||
transition-width
|
||||
>
|
||||
<Search
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
@click="onSearch"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
input-reset
|
||||
@input="onInput"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
22
src/components/basic/ab-select.stories.ts
Normal file
22
src/components/basic/ab-select.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSelect from './ab-select.vue';
|
||||
|
||||
const meta: Meta<typeof AbSelect> = {
|
||||
title: 'basic/ab-select',
|
||||
component: AbSelect,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSelect>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSelect },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-select v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
111
src/components/basic/ab-select.vue
Normal file
111
src/components/basic/ab-select.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from '@headlessui/vue';
|
||||
import { Down, Up } from '@icon-park/vue-next';
|
||||
import { isObject, isString } from 'lodash';
|
||||
import type { SelectItem } from '#/components';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: SelectItem | string;
|
||||
items: Array<SelectItem | string>;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const selected = ref<SelectItem | string>(
|
||||
props.modelValue || (props.items?.[0] ?? '')
|
||||
);
|
||||
|
||||
const otherItems = computed(() => {
|
||||
return (
|
||||
props.items.filter((e) => {
|
||||
if (isString(e) && isString(selected.value)) {
|
||||
return e !== selected.value;
|
||||
} else if (isObject(e) && isObject(selected.value)) {
|
||||
return e.id !== selected.value.id;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
const label = computed(() => {
|
||||
if (isString(selected.value)) {
|
||||
return selected.value;
|
||||
} else {
|
||||
return selected.value.label ?? selected.value.value;
|
||||
}
|
||||
});
|
||||
|
||||
function getLabel(item: SelectItem | string) {
|
||||
if (isString(item)) {
|
||||
return item;
|
||||
} else {
|
||||
return item.label ?? item.value;
|
||||
}
|
||||
}
|
||||
|
||||
function getDisabled(item: SelectItem | string) {
|
||||
return isString(item) ? false : item.disabled;
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
emit('update:modelValue', selected.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Listbox v-slot="{ open }" v-model="selected">
|
||||
<div
|
||||
rel
|
||||
flex="inline col"
|
||||
rounded-6px
|
||||
border="1px black"
|
||||
text-main
|
||||
py-4px
|
||||
px-12px
|
||||
>
|
||||
<ListboxButton bg-transparent fx-cer justify-between space-x-24px>
|
||||
<div>
|
||||
{{ label }}
|
||||
</div>
|
||||
<div :class="[{ hidden: open }]">
|
||||
<Down />
|
||||
</div>
|
||||
</ListboxButton>
|
||||
|
||||
<ListboxOptions mt-8px>
|
||||
<div flex="~ items-end" justify-between space-x-24px>
|
||||
<div flex="~ col" space-y-8px>
|
||||
<ListboxOption
|
||||
v-for="item in otherItems"
|
||||
v-slot="{ active }"
|
||||
:key="isString(item) ? item : item.id"
|
||||
:value="item"
|
||||
:disabled="getDisabled(item)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
{ 'text-primary': active },
|
||||
getDisabled(item) ? 'is-disabled' : 'is-btn',
|
||||
]"
|
||||
>
|
||||
{{ getLabel(item) }}
|
||||
</div>
|
||||
</ListboxOption>
|
||||
</div>
|
||||
|
||||
<div :class="[{ hidden: !open }]"><Up /></div>
|
||||
</div>
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
22
src/components/basic/ab-status.stories.ts
Normal file
22
src/components/basic/ab-status.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbStatus from './ab-status.vue';
|
||||
|
||||
const meta: Meta<typeof AbStatus> = {
|
||||
title: 'basic/ab-status',
|
||||
component: AbStatus,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbStatus>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbStatus },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-status v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
23
src/components/basic/ab-status.vue
Normal file
23
src/components/basic/ab-status.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
running: boolean;
|
||||
}>(),
|
||||
{
|
||||
running: false,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div wh-24px f-cer>
|
||||
<div rounded="1/2" f-cer border="2px solid white" wh-22px>
|
||||
<div
|
||||
:class="[running ? 'bg-running' : 'bg-stopped']"
|
||||
rounded="1/2"
|
||||
wh-10px
|
||||
transition-colors
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
src/components/basic/ab-switch.stories.ts
Normal file
22
src/components/basic/ab-switch.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
|
||||
import AbSwitch from './ab-switch.vue';
|
||||
|
||||
const meta: Meta<typeof AbSwitch> = {
|
||||
title: 'basic/ab-switch',
|
||||
component: AbSwitch,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbSwitch>;
|
||||
|
||||
export const Template: Story = {
|
||||
render: (args) => ({
|
||||
components: { AbSwitch },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ab-switch v-bind="args" />',
|
||||
}),
|
||||
};
|
||||
74
src/components/basic/ab-switch.vue
Normal file
74
src/components/basic/ab-switch.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { Switch } from '@headlessui/vue';
|
||||
|
||||
const checked = defineModel<boolean>('checked', {
|
||||
default: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div
|
||||
is-btn
|
||||
w-48px
|
||||
h-28px
|
||||
rounded-full
|
||||
rel
|
||||
flex="inline items-center"
|
||||
transition-colors
|
||||
duration-300
|
||||
p-3px
|
||||
shadow="~ inset"
|
||||
class="box"
|
||||
:class="{ checked }"
|
||||
>
|
||||
<div
|
||||
wh-22px
|
||||
rounded="1/2"
|
||||
transition-all
|
||||
duration-300
|
||||
class="slider"
|
||||
:class="{ checked, 'translate-x-20px': checked }"
|
||||
></div>
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
$bg-unchecked: #929292;
|
||||
$bg-checked: #e7e7e7;
|
||||
|
||||
$slider-unchecked: #ececef;
|
||||
$slider-unchecked-hover: #dbd8ec;
|
||||
|
||||
$slider-checked: #1c1259;
|
||||
$slider-checked-hover: #62589e;
|
||||
|
||||
.box {
|
||||
background: $bg-unchecked;
|
||||
|
||||
&.checked {
|
||||
background: $bg-checked;
|
||||
}
|
||||
|
||||
&:hover .slider {
|
||||
&:not(.checked) {
|
||||
background: $slider-unchecked-hover;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: $slider-checked-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
&:not(.checked) {
|
||||
background: $slider-unchecked;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: $slider-checked;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
src/components/layout/ab-sidebar.vue
Normal file
145
src/components/layout/ab-sidebar.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Calendar,
|
||||
Download,
|
||||
Home,
|
||||
Log,
|
||||
Logout,
|
||||
MenuUnfold,
|
||||
Play,
|
||||
SettingTwo,
|
||||
} from '@icon-park/vue-next';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open?: boolean;
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
}
|
||||
);
|
||||
|
||||
const show = ref(props.open);
|
||||
const toggle = () => (show.value = !show.value);
|
||||
const route = useRoute();
|
||||
const { logout } = useAuth();
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 1,
|
||||
icon: Home,
|
||||
label: 'HomePage',
|
||||
path: '/bangumi',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Calendar,
|
||||
label: 'Calendar',
|
||||
path: '/calendar',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Play,
|
||||
label: 'Player',
|
||||
path: '/player',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Download,
|
||||
label: 'Downloader',
|
||||
path: '/downloader',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: Log,
|
||||
label: 'Log',
|
||||
path: '/log',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: SettingTwo,
|
||||
label: 'Config',
|
||||
path: '/config',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[show ? 'w-240px' : 'w-72px']"
|
||||
bg-theme-col
|
||||
text-white
|
||||
transition-width
|
||||
pb-12px
|
||||
rounded-12px
|
||||
>
|
||||
<div overflow-hidden wh-full flex="~ col">
|
||||
<div
|
||||
w-full
|
||||
h-60px
|
||||
is-btn
|
||||
f-cer
|
||||
rounded-t-10px
|
||||
bg="#E7E7E7"
|
||||
text="#2A1C52"
|
||||
rel
|
||||
@click="toggle"
|
||||
>
|
||||
<div :class="[!show && 'abs opacity-0']" transition-opacity>
|
||||
<div text-h1>Menu</div>
|
||||
</div>
|
||||
|
||||
<MenuUnfold
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#2A1C52"
|
||||
abs
|
||||
left="24px"
|
||||
:class="[show && 'rotate-y-180']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RouterLink
|
||||
v-for="i in items"
|
||||
:key="i.id"
|
||||
:to="i.path"
|
||||
replace
|
||||
:title="i.label"
|
||||
fx-cer
|
||||
px-24px
|
||||
space-x-42px
|
||||
h-48px
|
||||
is-btn
|
||||
transition-colors
|
||||
hover:bg="#F1F5FA"
|
||||
hover:text="#2A1C52"
|
||||
:class="[
|
||||
route.path === i.path && 'bg-[#F1F5FA] text-[#2A1C52]',
|
||||
i.hidden && 'hidden',
|
||||
]"
|
||||
>
|
||||
<Component :is="i.icon" :size="24" />
|
||||
<div text-h2>{{ i.label }}</div>
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
title="logout"
|
||||
mt-auto
|
||||
fx-cer
|
||||
px-24px
|
||||
space-x-42px
|
||||
h-48px
|
||||
is-btn
|
||||
transition-colors
|
||||
hover:bg="#F1F5FA"
|
||||
hover:text="#2A1C52"
|
||||
@click="logout"
|
||||
>
|
||||
<Logout :size="24" />
|
||||
<div text-h2>Logout</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
74
src/components/layout/ab-topbar.vue
Normal file
74
src/components/layout/ab-topbar.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { Me, Pause, PlayOne, Power, Refresh } from '@icon-park/vue-next';
|
||||
|
||||
const search = ref('');
|
||||
const show = ref(false);
|
||||
const showAdd = ref(false);
|
||||
|
||||
const { onUpdate, offUpdate, start, pause, shutdown, restart } =
|
||||
useProgramStore();
|
||||
const { running } = storeToRefs(useProgramStore());
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'Start',
|
||||
icon: PlayOne,
|
||||
handle: start,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Pause',
|
||||
icon: Pause,
|
||||
handle: pause,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Restart',
|
||||
icon: Refresh,
|
||||
handle: restart,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Shutdown',
|
||||
icon: Power,
|
||||
handle: shutdown,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'Profile',
|
||||
icon: Me,
|
||||
handle: () => {
|
||||
show.value = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onBeforeMount(() => {
|
||||
onUpdate();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
offUpdate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-60px bg-theme-row text-white rounded-12px fx-cer px-24px>
|
||||
<div text-h1 mr-12px>AutoBangumi</div>
|
||||
|
||||
<ab-search v-model:value="search" hidden />
|
||||
|
||||
<div ml-auto>
|
||||
<ab-status-bar
|
||||
:items="items"
|
||||
:running="running"
|
||||
@click-add="() => (showAdd = true)"
|
||||
></ab-status-bar>
|
||||
</div>
|
||||
|
||||
<ab-change-account v-model:show="show"></ab-change-account>
|
||||
|
||||
<ab-add-bangumi v-model:show="showAdd"></ab-add-bangumi>
|
||||
</div>
|
||||
</template>
|
||||
76
src/components/setting/config-download.vue
Normal file
76
src/components/setting/config-download.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Downloader, DownloaderType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const downloader = getSettingGroup('downloader');
|
||||
const downloaderType: DownloaderType = ['qbittorrent'];
|
||||
|
||||
const items: SettingItem<Downloader>[] = [
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Downloader Type',
|
||||
type: 'select',
|
||||
css: 'w-115px',
|
||||
prop: {
|
||||
items: downloaderType,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'host',
|
||||
label: 'Host',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '127.0.0.1:8989',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'username',
|
||||
label: 'Username',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'admin',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'password',
|
||||
label: 'Password',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'admindmin',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'path',
|
||||
label: 'Download Path',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '/downloads/Bangumi',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'ssl',
|
||||
label: 'SSL',
|
||||
type: 'switch',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Downloader Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="downloader[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
54
src/components/setting/config-manage.vue
Normal file
54
src/components/setting/config-manage.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiManage, RenameMethod } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const manage = getSettingGroup('bangumi_manage');
|
||||
const renameMethod: RenameMethod = ['normal', 'pn', 'advance', 'none'];
|
||||
|
||||
const items: SettingItem<BangumiManage>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'rename_method',
|
||||
label: 'Rename Method',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: renameMethod,
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'eps_complete',
|
||||
label: 'Eps complete',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'group_tag',
|
||||
label: 'Add Group Tag',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'remove_bad_torrent',
|
||||
label: 'Delete Bad Torrent',
|
||||
type: 'switch',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Manage Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="manage[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
67
src/components/setting/config-normal.vue
Normal file
67
src/components/setting/config-normal.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Log, Program } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const program = getSettingGroup('program');
|
||||
const log = getSettingGroup('log');
|
||||
|
||||
const programItems: SettingItem<Program>[] = [
|
||||
{
|
||||
configKey: 'rss_time',
|
||||
label: 'Interval Time of Rss',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'rename_time',
|
||||
label: 'Interval Time of Rename',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'webui_port',
|
||||
label: 'WebUI Port',
|
||||
type: 'input',
|
||||
css: 'w-72px',
|
||||
prop: {
|
||||
type: 'number',
|
||||
placeholder: 'port',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
];
|
||||
|
||||
const logItems: SettingItem<Log> = {
|
||||
configKey: 'debug_enable',
|
||||
label: 'Debug',
|
||||
type: 'switch',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Normal Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in programItems"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="program[i.configKey]"
|
||||
></ab-setting>
|
||||
|
||||
<ab-setting
|
||||
v-bind="logItems"
|
||||
v-model:data="log[logItems.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
58
src/components/setting/config-notification.vue
Normal file
58
src/components/setting/config-notification.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Notification, NotificationType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const notification = getSettingGroup('notification');
|
||||
const notificationType: NotificationType = ['telegram', 'server-chan', 'bark'];
|
||||
|
||||
const items: SettingItem<Notification>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Type',
|
||||
type: 'select',
|
||||
css: 'w-140px',
|
||||
prop: {
|
||||
items: notificationType,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'token',
|
||||
label: 'Token',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'token',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'chat_id',
|
||||
label: 'Chat ID',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'chat id',
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Notification Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="notification[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
88
src/components/setting/config-parser.vue
Normal file
88
src/components/setting/config-parser.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
RssParser,
|
||||
RssParserLang,
|
||||
RssParserMethodType,
|
||||
RssParserType,
|
||||
} from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const parser = getSettingGroup('rss_parser');
|
||||
|
||||
const sourceItems: RssParserType = ['mikan'];
|
||||
const langs: RssParserLang = ['zh', 'en', 'jp'];
|
||||
/** @ts-expect-error Incorrect order */
|
||||
const parserMethods: RssParserMethodType = ['tmdb', 'mikan', 'parser'];
|
||||
|
||||
const items: SettingItem<RssParser>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Source',
|
||||
type: 'select',
|
||||
css: 'w-115px',
|
||||
prop: {
|
||||
items: sourceItems,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'token',
|
||||
label: 'Token',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'token',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'custom_url',
|
||||
label: 'Custom Url',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'mikanime.tv',
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'language',
|
||||
label: 'Language',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: langs,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'parser_type',
|
||||
label: 'Parser Type',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: parserMethods,
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'filter',
|
||||
label: 'Exclude',
|
||||
type: 'dynamic-tags',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Parser Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="parser[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
23
src/components/setting/config-player.vue
Normal file
23
src/components/setting/config-player.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
const { types, type, url } = storeToRefs(usePlayerStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Media Player Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-model:data="type"
|
||||
type="select"
|
||||
label="type"
|
||||
:prop="{ items: types }"
|
||||
></ab-setting>
|
||||
|
||||
<ab-setting
|
||||
v-model:data="url"
|
||||
type="input"
|
||||
label="url"
|
||||
:prop="{ placeholder: 'media player url' }"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
75
src/components/setting/config-proxy.vue
Normal file
75
src/components/setting/config-proxy.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Proxy, ProxyType } from '#/config';
|
||||
import type { SettingItem } from '#/components';
|
||||
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const proxy = getSettingGroup('proxy');
|
||||
const proxyType: ProxyType = ['http', 'https', 'socks5'];
|
||||
|
||||
const items: SettingItem<Proxy>[] = [
|
||||
{
|
||||
configKey: 'enable',
|
||||
label: 'Enable',
|
||||
type: 'switch',
|
||||
},
|
||||
{
|
||||
configKey: 'type',
|
||||
label: 'Proxy Type',
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: proxyType,
|
||||
},
|
||||
bottomLine: true,
|
||||
},
|
||||
{
|
||||
configKey: 'host',
|
||||
label: 'Host',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '127.0.0.1',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'port',
|
||||
label: 'Port',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: '7890',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'username',
|
||||
label: 'Username',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'username',
|
||||
},
|
||||
},
|
||||
{
|
||||
configKey: 'password',
|
||||
label: 'Password',
|
||||
type: 'input',
|
||||
prop: {
|
||||
type: 'text',
|
||||
placeholder: 'password',
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ab-fold-panel title="Proxy Setting">
|
||||
<div space-y-12px>
|
||||
<ab-setting
|
||||
v-for="i in items"
|
||||
:key="i.configKey"
|
||||
v-bind="i"
|
||||
v-model:data="proxy[i.configKey]"
|
||||
></ab-setting>
|
||||
</div>
|
||||
</ab-fold-panel>
|
||||
</template>
|
||||
58
src/hooks/useApi.ts
Normal file
58
src/hooks/useApi.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type AnyAsyncFuntion<TData = any> = (...args: any[]) => Promise<TData>;
|
||||
type UnPromisify<T> = T extends AnyAsyncFuntion<infer U> ? U : never;
|
||||
|
||||
export function useApi<
|
||||
TError = any,
|
||||
TApi extends AnyAsyncFuntion = AnyAsyncFuntion,
|
||||
TData = UnPromisify<TApi>
|
||||
>(
|
||||
api: TApi,
|
||||
options?: {
|
||||
failRule?: (data: TData) => boolean;
|
||||
message?: {
|
||||
success?: string;
|
||||
fail?: string;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const data = ref<TData>();
|
||||
const isLoading = ref(false);
|
||||
|
||||
const fetchResult = createEventHook<TData>();
|
||||
const fetchError = createEventHook<TError>();
|
||||
const message = useMessage();
|
||||
|
||||
function execute(...params: Parameters<TApi>) {
|
||||
isLoading.value = true;
|
||||
|
||||
api(...params)
|
||||
.then((res: TData) => {
|
||||
data.value = res;
|
||||
fetchResult.trigger(res);
|
||||
|
||||
if (options?.failRule && options.failRule(res)) {
|
||||
options.message?.fail && message.error(options.message.fail);
|
||||
} else {
|
||||
options?.message?.success && message.success(options.message.success);
|
||||
}
|
||||
})
|
||||
.catch((err: TError) => {
|
||||
fetchError.trigger(err);
|
||||
|
||||
options?.message?.error && message.error(options.message.error);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
|
||||
execute,
|
||||
onResult: fetchResult.on,
|
||||
onError: fetchError.on,
|
||||
};
|
||||
}
|
||||
119
src/hooks/useAuth.ts
Normal file
119
src/hooks/useAuth.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { User } from '#/auth';
|
||||
import type { ApiError } from '#/error';
|
||||
|
||||
export const useAuth = createSharedComposable(() => {
|
||||
const auth = useLocalStorage('auth', '');
|
||||
const message = useMessage();
|
||||
|
||||
const user = reactive<User>({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const isLogin = computed(() => auth.value !== '');
|
||||
|
||||
function clearUser() {
|
||||
user.username = '';
|
||||
user.password = '';
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (user.username === '') {
|
||||
message.warning('Please Enter Username!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.password === '') {
|
||||
message.warning('Please Enter Password!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function login() {
|
||||
const { execute, onResult, onError } = useApi(apiAuth.login, {
|
||||
message: {
|
||||
success: 'Login Success!',
|
||||
},
|
||||
});
|
||||
|
||||
onResult((res) => {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
clearUser();
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
const error = err as ApiError;
|
||||
message.error(error.detail);
|
||||
});
|
||||
|
||||
if (check()) {
|
||||
execute(user.username, user.password);
|
||||
}
|
||||
}
|
||||
|
||||
const { execute: logout, onResult: onLogoutResult } = useApi(apiAuth.logout, {
|
||||
failRule: (res) => !res,
|
||||
message: {
|
||||
success: 'Logout Success!',
|
||||
fail: 'Logout Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
onLogoutResult(() => {
|
||||
clearUser();
|
||||
auth.value = '';
|
||||
});
|
||||
|
||||
const { execute: refresh, onResult: onRefreshResult } = useApi(
|
||||
apiAuth.refresh,
|
||||
{
|
||||
message: {
|
||||
success: 'Refresh Success!',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
onRefreshResult((res) => {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
});
|
||||
|
||||
function update() {
|
||||
const { execute, onResult } = useApi(apiAuth.update, {
|
||||
failRule: (res) => res.message !== 'update success',
|
||||
message: {
|
||||
success: 'Update Success!',
|
||||
fail: 'Update Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
onResult((res) => {
|
||||
if (res.message === 'update success') {
|
||||
auth.value = `${res.token_type} ${res.access_token}`;
|
||||
clearUser();
|
||||
} else {
|
||||
user.password = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (check()) {
|
||||
if (user.password.length < 8) {
|
||||
message.error('Password must be at least 8 characters long!');
|
||||
} else {
|
||||
execute(user.username, user.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
auth,
|
||||
user,
|
||||
isLogin,
|
||||
|
||||
login,
|
||||
logout,
|
||||
refresh,
|
||||
update,
|
||||
};
|
||||
});
|
||||
6
src/hooks/useMessage.ts
Normal file
6
src/hooks/useMessage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
|
||||
export const useMessage = createSharedComposable(() => {
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
return message;
|
||||
});
|
||||
@@ -1,14 +1,11 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import router from './router';
|
||||
import { router } from './router';
|
||||
import App from './App.vue';
|
||||
|
||||
import '@unocss/reset/tailwind-compat.css';
|
||||
import 'virtual:uno.css';
|
||||
|
||||
import 'element-plus/es/components/message/style/css';
|
||||
import 'element-plus/es/components/message-box/style/css';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import YMenu from './YMenu.vue';
|
||||
|
||||
const { status } = storeToRefs(programStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout" w-full h-screen overflow-hidden flex>
|
||||
<el-container>
|
||||
<el-header
|
||||
class="header"
|
||||
flex="~ items-center justify-center"
|
||||
h-65px
|
||||
relative
|
||||
>
|
||||
<img src="@/assets/logo.png" alt="logo" class="h-7/10" />
|
||||
|
||||
<div absolute right-5 flex="~ items-center" text-3>
|
||||
运行状态:
|
||||
<div
|
||||
class="i-carbon:dot-mark"
|
||||
:class="[status ? 'text-green' : 'text-red']"
|
||||
></div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-container overflow-hidden>
|
||||
<el-aside width="auto">
|
||||
<YMenu />
|
||||
</el-aside>
|
||||
|
||||
<el-main>
|
||||
<el-scrollbar>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
.app-layout {
|
||||
@media screen and (max-width: 980px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
const { fullPath } = useRoute();
|
||||
const { width } = useWindowSize();
|
||||
const isCollapse = ref(false);
|
||||
const WIDTH = 980;
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value < WIDTH) {
|
||||
isCollapse.value = true;
|
||||
} else {
|
||||
isCollapse.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: 'i-carbon-home',
|
||||
title: '番剧管理',
|
||||
url: '/bangumi',
|
||||
},
|
||||
{
|
||||
icon: 'i-carbon-debug',
|
||||
title: '调试',
|
||||
url: '/debug',
|
||||
},
|
||||
{
|
||||
icon: 'i-carbon:align-box-middle-right',
|
||||
title: '日志',
|
||||
url: '/log',
|
||||
},
|
||||
{
|
||||
icon: 'i-carbon:settings',
|
||||
title: '配置',
|
||||
url: '/config',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-menu :default-active="fullPath" :collapse="isCollapse" router h-full>
|
||||
<template v-for="(i, index) in items" :key="index">
|
||||
<el-menu-item :index="i.url">
|
||||
<div :class="[i.icon]" mr-0.5em></div>
|
||||
<template #title>{{ i.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</template>
|
||||
@@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { addBangumi } from '@/api/bangumi';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const rssLink = ref('');
|
||||
const loading = ref(false);
|
||||
const dialog = ref();
|
||||
const dialogData = ref(null);
|
||||
|
||||
async function add() {
|
||||
loading.value = true;
|
||||
const res = await addBangumi(props.type, rssLink.value);
|
||||
if (res) {
|
||||
loading.value = false;
|
||||
dialogData.value = res.data;
|
||||
dialog.value.open();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ShowResults ref="dialog" title="执行结果" :results="dialogData" />
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span v-if="type === 'new'">订阅新番</span>
|
||||
<span v-else-if="type === 'old'">订阅旧番</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-con">
|
||||
<el-input v-model="rssLink" placeholder="请输入番剧的rss链接">
|
||||
<template #append>
|
||||
<el-button type="primary" :loading="loading" @click="add"
|
||||
>订阅</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const store = bangumiStore();
|
||||
|
||||
const activeName = ref('1');
|
||||
const dialogData = ref(null);
|
||||
const dialog = ref();
|
||||
|
||||
onActivated(() => {
|
||||
store.get();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ShowResults ref="dialog" title="执行结果" :results="dialogData" />
|
||||
|
||||
<div class="bangumi-data">
|
||||
<el-collapse v-model="activeName" accordion>
|
||||
<el-collapse-item title="已订阅番剧" name="1">
|
||||
<span class="tips"
|
||||
>注: 目前只能管理 mikan 源, 如通过 api 添加其他来源的新番将
|
||||
<b>不会</b> 出现在此处</span
|
||||
>
|
||||
<el-table
|
||||
:data="store.data"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
max-height="40vh"
|
||||
>
|
||||
<el-table-column prop="official_title" label="番名" min-width="250" />
|
||||
<el-table-column prop="season" label="季度" width="60" />
|
||||
<el-table-column prop="dpi" label="分辨率" />
|
||||
<el-table-column prop="subtitle" label="字幕" />
|
||||
<el-table-column prop="group_name" label="字幕组" />
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
.bangumi-data {
|
||||
.tips {
|
||||
line-height: 2;
|
||||
color: #f56c6c;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import AddBangumi from './components/AddBangumi.vue';
|
||||
import BangumiData from './components/BangumiData.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bangumi">
|
||||
<el-row>
|
||||
<!-- S 番剧列表 -->
|
||||
<el-col>
|
||||
<BangumiData />
|
||||
</el-col>
|
||||
<!-- E 番剧列表 -->
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="display: none">
|
||||
<!-- S 添加新番 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="8" mb-20px>
|
||||
<AddBangumi type="new" />
|
||||
</el-col>
|
||||
<!-- E 添加新番 -->
|
||||
|
||||
<!-- S 添加旧番 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="8" mb-20px>
|
||||
<AddBangumi type="old" />
|
||||
</el-col>
|
||||
<!-- E 添加旧番 -->
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
.el-row {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { form, renameMethod, tfOptions } from '../form-data';
|
||||
|
||||
const bgmManage = computed(() => form.bangumi_manage);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="番剧管理">
|
||||
<ConfigFormCol label="启用">
|
||||
<el-select v-model="bgmManage.enable" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="历史番剧下载">
|
||||
<el-select v-model="bgmManage.eps_complete" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="重命名方法">
|
||||
<el-select v-model="bgmManage.rename_method" flex-1>
|
||||
<el-option
|
||||
v-for="opt in renameMethod"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="下载规则中添加组名">
|
||||
<el-select v-model="bgmManage.group_tag" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="删除坏种">
|
||||
<el-select v-model="bgmManage.remove_bad_torrent" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { downloaderType, form, tfOptions } from '../form-data';
|
||||
|
||||
const downloader = computed(() => form.downloader);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="下载器设置">
|
||||
<ConfigFormCol label="下载器">
|
||||
<el-select v-model="downloader.type" flex-1>
|
||||
<el-option
|
||||
v-for="opt in downloaderType"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="下载器地址" prop="downloader.host">
|
||||
<el-input v-model="downloader.host"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="下载路径(映射后)">
|
||||
<el-input v-model="downloader.path"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="用户名">
|
||||
<el-input v-model="downloader.username"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="密码">
|
||||
<el-input v-model="downloader.password"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="ssl">
|
||||
<el-select v-model="downloader.ssl" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { form, tfOptions } from '../form-data';
|
||||
|
||||
const log = computed(() => form.log);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="日志">
|
||||
<ConfigFormCol label="启用debug">
|
||||
<el-select v-model="log.debug_enable" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { form, notificationType, tfOptions } from '../form-data';
|
||||
|
||||
const notification = computed(() => form.notification);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="通知">
|
||||
<ConfigFormCol label="启用">
|
||||
<el-select v-model="notification.enable" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="通知类型">
|
||||
<el-select v-model="notification.type" flex-1>
|
||||
<el-option
|
||||
v-for="opt in notificationType"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="token">
|
||||
<el-input v-model="notification.token"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="chat_id">
|
||||
<el-input v-model="notification.chat_id" type="number"></el-input>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { form } from '../form-data';
|
||||
|
||||
const program = computed(() => form.program);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="主程序">
|
||||
<ConfigFormCol label="RSS 检查间隔">
|
||||
<el-input v-model.number="program.rss_time" type="number" />
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="重命名频率">
|
||||
<el-input v-model.number="program.rename_time" type="number" />
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="WebUI 端口" prop="program.webui_port">
|
||||
<el-input v-model.number="program.webui_port" type="number" />
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { form, proxyType, tfOptions } from '../form-data';
|
||||
|
||||
const proxy = computed(() => form.proxy);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="代理">
|
||||
<ConfigFormCol label="启用">
|
||||
<el-select v-model="proxy.enable" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="代理类型">
|
||||
<el-select v-model="proxy.type" flex-1>
|
||||
<el-option
|
||||
v-for="opt in proxyType"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="host" prop="proxy.host">
|
||||
<el-input v-model="proxy.host"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="port" prop="proxy.port">
|
||||
<el-input v-model.number="proxy.port" type="number"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="username">
|
||||
<el-input v-model="proxy.username"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="password">
|
||||
<el-input
|
||||
v-model="proxy.password"
|
||||
type="password"
|
||||
show-password
|
||||
></el-input>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,74 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
form,
|
||||
rssParserLang,
|
||||
rssParserMethodType,
|
||||
rssParserType,
|
||||
tfOptions,
|
||||
} from '../form-data';
|
||||
|
||||
const rssParser = computed(() => form.rss_parser);
|
||||
|
||||
const filter = ref('');
|
||||
watch(filter, (nv) => {
|
||||
const value = nv.split(',').filter((e) => e !== '');
|
||||
rssParser.value.filter = value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigFormRow title="RSS 解析">
|
||||
<ConfigFormCol label="启用">
|
||||
<el-select v-model="rssParser.enable" flex-1>
|
||||
<el-option
|
||||
v-for="(opt, index) in tfOptions"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="源">
|
||||
<el-select v-model="rssParser.type" flex-1>
|
||||
<el-option
|
||||
v-for="opt in rssParserType"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="token">
|
||||
<el-input v-model="rssParser.token"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="语言">
|
||||
<el-select v-model="rssParser.language" flex-1>
|
||||
<el-option
|
||||
v-for="opt in rssParserLang"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="源站链接">
|
||||
<el-input v-model="rssParser.custom_url"></el-input>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="解析类型">
|
||||
<el-select v-model="rssParser.parser_type" flex-1>
|
||||
<el-option
|
||||
v-for="opt in rssParserMethodType"
|
||||
:key="opt"
|
||||
:value="opt"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</ConfigFormCol>
|
||||
|
||||
<ConfigFormCol label="筛选">
|
||||
<el-input v-model="filter" :value="rssParser.filter.join(',')"></el-input>
|
||||
</ConfigFormCol>
|
||||
</ConfigFormRow>
|
||||
</template>
|
||||
@@ -1,79 +0,0 @@
|
||||
import type {
|
||||
Config,
|
||||
DownloaderType,
|
||||
NotificationType,
|
||||
ProxyType,
|
||||
RenameMethod,
|
||||
RssParserLang,
|
||||
RssParserMethodType,
|
||||
RssParserType,
|
||||
} from '#/config';
|
||||
|
||||
export const form = reactive<Config>({
|
||||
program: {
|
||||
rss_time: 0,
|
||||
rename_time: 0,
|
||||
webui_port: 0,
|
||||
},
|
||||
downloader: {
|
||||
type: 'qbittorrent',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
path: '',
|
||||
ssl: false,
|
||||
},
|
||||
rss_parser: {
|
||||
enable: true,
|
||||
type: 'mikan',
|
||||
token: '',
|
||||
custom_url: '',
|
||||
filter: [],
|
||||
language: 'zh',
|
||||
parser_type: 'parser',
|
||||
},
|
||||
bangumi_manage: {
|
||||
enable: true,
|
||||
eps_complete: true,
|
||||
rename_method: 'normal',
|
||||
group_tag: true,
|
||||
remove_bad_torrent: true,
|
||||
},
|
||||
log: {
|
||||
debug_enable: false,
|
||||
},
|
||||
proxy: {
|
||||
enable: false,
|
||||
type: 'http',
|
||||
host: '',
|
||||
port: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
notification: {
|
||||
enable: false,
|
||||
type: 'telegram',
|
||||
token: '',
|
||||
chat_id: '',
|
||||
},
|
||||
});
|
||||
|
||||
export const downloaderType: DownloaderType = ['qbittorrent'];
|
||||
export const rssParserType: RssParserType = ['mikan'];
|
||||
export const rssParserMethodType: RssParserMethodType = [
|
||||
'tmdb',
|
||||
'mikan',
|
||||
'parser',
|
||||
];
|
||||
export const rssParserLang: RssParserLang = ['zh', 'en', 'jp'];
|
||||
export const renameMethod: RenameMethod = ['normal', 'pn', 'advance', 'none'];
|
||||
export const proxyType: ProxyType = ['http', 'https', 'socks5'];
|
||||
export const notificationType: NotificationType = [
|
||||
'telegram',
|
||||
'server-chan',
|
||||
'bark',
|
||||
];
|
||||
export const tfOptions = [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
];
|
||||
@@ -1,65 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import ProgramItem from './components/ProgramItem.vue';
|
||||
import DownloaderItem from './components/DownloaderItem.vue';
|
||||
import RssParserItem from './components/RssParserItem.vue';
|
||||
import BangumiManageItem from './components/BangumiManageItem.vue';
|
||||
import LogItem from './components/LogItem.vue';
|
||||
import ProxyItem from './components/ProxyItem.vue';
|
||||
import NotificationItem from './components/NotificationItem.vue';
|
||||
|
||||
import { form } from './form-data';
|
||||
|
||||
const store = configStore();
|
||||
|
||||
function submit() {
|
||||
store.set(form);
|
||||
}
|
||||
|
||||
function formSync() {
|
||||
if (store.config) {
|
||||
Object.keys(store.config).forEach((key) => {
|
||||
if (store.config) {
|
||||
form[key] = JSON.parse(JSON.stringify(store.config[key]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onActivated(async () => {
|
||||
await store.get();
|
||||
formSync();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="settings" pb30>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="24">
|
||||
<el-form :model="form" label-position="right">
|
||||
<el-collapse>
|
||||
<ProgramItem />
|
||||
<DownloaderItem />
|
||||
<RssParserItem />
|
||||
<BangumiManageItem />
|
||||
<LogItem />
|
||||
<ProxyItem />
|
||||
<NotificationItem />
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div flex="~ items-center justify-center" mt20>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
<el-button @click="formSync">还原</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
.el-row {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { resetRule } from '@/api/debug';
|
||||
import { appRestart } from '@/api/program';
|
||||
|
||||
const loading = ref(false);
|
||||
async function reset() {
|
||||
loading.value = true;
|
||||
const res = await resetRule();
|
||||
loading.value = false;
|
||||
if (res.data === 'Success') {
|
||||
ElMessage({
|
||||
message: '数据已重置, 建议重启程序或容器',
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
ElMessage({
|
||||
message: `错误: ${res.data}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restart() {
|
||||
ElMessageBox.confirm('该操作将重启程序!', {
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
appRestart()
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
ElMessage({
|
||||
message: '重启中...',
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('🚀 ~ file: index.vue:41 ~ .then ~ error:', error);
|
||||
ElMessage({
|
||||
message: '操作失败, 请手动重启容器!',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="debug">
|
||||
<el-row :gutter="20">
|
||||
<!-- S 重置数据 -->
|
||||
<el-col :xs="24" :sm="12" :lg="8" mb-20px>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>重置AB数据</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-con">
|
||||
<el-button type="danger" :loading="loading" @click="reset"
|
||||
>重置</el-button
|
||||
>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- E 重置数据 -->
|
||||
|
||||
<!-- S 重启程序 -->
|
||||
<el-col :xs="24" :sm="12" :lg="8" mb-20px>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>重启程序</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-con">
|
||||
<el-button type="danger" :loading="loading" @click="restart"
|
||||
>重启</el-button
|
||||
>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- E 重启程序 -->
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
28
src/pages/index.vue
Normal file
28
src/pages/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Index',
|
||||
redirect: '/bangumi',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div layout-container>
|
||||
<ab-topbar />
|
||||
|
||||
<main layout-main>
|
||||
<ab-sidebar />
|
||||
|
||||
<div layout-content>
|
||||
<ab-page-title :title="$route.name"></ab-page-title>
|
||||
|
||||
<div overflow-auto mt-12px flex-grow>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
92
src/pages/index/bangumi.vue
Normal file
92
src/pages/index/bangumi.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const { data } = storeToRefs(useBangumiStore());
|
||||
const { getAll, useUpdateRule, useDisableRule, useEnableRule } =
|
||||
useBangumiStore();
|
||||
|
||||
const editRule = reactive<{
|
||||
show: boolean;
|
||||
item: BangumiRule;
|
||||
}>({
|
||||
show: false,
|
||||
item: {
|
||||
added: false,
|
||||
deleted: false,
|
||||
dpi: '',
|
||||
eps_collect: false,
|
||||
filter: [],
|
||||
group_name: '',
|
||||
id: 0,
|
||||
official_title: '',
|
||||
offset: 0,
|
||||
poster_link: '',
|
||||
rss_link: [],
|
||||
rule_name: '',
|
||||
save_path: '',
|
||||
season: 1,
|
||||
season_raw: '',
|
||||
source: null,
|
||||
subtitle: '',
|
||||
title_raw: '',
|
||||
year: null,
|
||||
},
|
||||
});
|
||||
|
||||
function open(data: BangumiRule) {
|
||||
editRule.show = true;
|
||||
editRule.item = data;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
editRule.show = false;
|
||||
getAll();
|
||||
}
|
||||
|
||||
const { execute: updateRule, onResult: onUpdateRuleResult } = useUpdateRule();
|
||||
const { execute: enableRule, onResult: onEnableRuleResult } = useEnableRule();
|
||||
const { execute: disableRule, onResult: onDisableRuleResult } =
|
||||
useDisableRule();
|
||||
|
||||
onUpdateRuleResult(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
onDisableRuleResult(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
onEnableRuleResult(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
getAll();
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: 'Bangumi List',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ wrap" gap-y-12px gap-x-50px>
|
||||
<ab-bangumi-card
|
||||
v-for="i in data"
|
||||
:key="i.id"
|
||||
:class="[i.deleted && 'grayscale']"
|
||||
:poster="i.poster_link ?? ''"
|
||||
:name="i.official_title"
|
||||
:season="i.season"
|
||||
@click="() => open(i)"
|
||||
></ab-bangumi-card>
|
||||
|
||||
<ab-edit-rule
|
||||
v-model:show="editRule.show"
|
||||
v-model:rule="editRule.item"
|
||||
@enable="(id) => enableRule(id)"
|
||||
@disable="({ id, deleteFile }) => disableRule(id, deleteFile)"
|
||||
@apply="(rule) => updateRule(rule)"
|
||||
></ab-edit-rule>
|
||||
</div>
|
||||
</template>
|
||||
9
src/pages/index/calendar.vue
Normal file
9
src/pages/index/calendar.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Calendar',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>null</div>
|
||||
</template>
|
||||
38
src/pages/index/config.vue
Normal file
38
src/pages/index/config.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
const { getConfig, setConfig } = useConfigStore();
|
||||
|
||||
getConfig();
|
||||
|
||||
definePage({
|
||||
name: 'Config',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full flex="~ col">
|
||||
<div grid="~ cols-2" gap-20px mb-auto>
|
||||
<div space-y-20px>
|
||||
<config-normal></config-normal>
|
||||
|
||||
<config-parser></config-parser>
|
||||
|
||||
<config-download></config-download>
|
||||
|
||||
<config-manage></config-manage>
|
||||
</div>
|
||||
|
||||
<div space-y-20px>
|
||||
<config-notification></config-notification>
|
||||
|
||||
<config-proxy></config-proxy>
|
||||
|
||||
<config-player></config-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div fx-cer justify-end gap-8px mt-20px>
|
||||
<ab-button type="warn" @click="getConfig">Cancel</ab-button>
|
||||
<ab-button @click="setConfig">Apply</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
9
src/pages/index/downloader.vue
Normal file
9
src/pages/index/downloader.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Downloader',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>null</div>
|
||||
</template>
|
||||
111
src/pages/index/log.vue
Normal file
111
src/pages/index/log.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
const { onUpdate, offUpdate, reset, copy } = useLogStore();
|
||||
const { log } = storeToRefs(useLogStore());
|
||||
|
||||
onActivated(() => {
|
||||
onUpdate();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
offUpdate();
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: 'Log',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ wrap" gap-12px>
|
||||
<ab-container title="Log" w-660px grow>
|
||||
<div
|
||||
rounded-10px
|
||||
border="1px solid black"
|
||||
overflow-auto
|
||||
p-10px
|
||||
max-h-60vh
|
||||
>
|
||||
<pre text-main>{{ log }}</pre>
|
||||
</div>
|
||||
|
||||
<div flex="~ justify-end" space-x-10px mt-12px>
|
||||
<ab-button type="warn" size="small" @click="reset">Reset</ab-button>
|
||||
<ab-button size="small" @click="copy">Copy</ab-button>
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<div grow w-500px space-y-20px>
|
||||
<ab-container title="Contact Infomation">
|
||||
<div space-y-12px>
|
||||
<ab-label label="Github">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://github.com/EstrellaXD/Auto_Bangumi"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="WebUI Repo">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://github.com/Rewrite0/Auto_Bangumi_WebUI"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<ab-label label="Twitter">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://twitter.com/Estrella_Pan"
|
||||
target="_blank"
|
||||
>
|
||||
Go
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Telegram Group">
|
||||
<ab-button
|
||||
size="small"
|
||||
link="https://t.me/autobangumi"
|
||||
target="_blank"
|
||||
>
|
||||
Join
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<ab-container title="Bug Report">
|
||||
<div space-y-12px>
|
||||
<ab-button
|
||||
mx-auto
|
||||
text-16px
|
||||
w-300px
|
||||
h-46px
|
||||
rounded-10px
|
||||
link="https://github.com/EstrellaXD/Auto_Bangumi/issues"
|
||||
>Github Issue</ab-button
|
||||
>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<ab-button
|
||||
mx-auto
|
||||
text-16px
|
||||
w-300px
|
||||
h-46px
|
||||
rounded-10px
|
||||
link="mailto:estrellaxd05@gmail.com"
|
||||
>Email Contact</ab-button
|
||||
>
|
||||
</div>
|
||||
</ab-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
src/pages/index/player.vue
Normal file
28
src/pages/index/player.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
definePage({
|
||||
name: 'Player',
|
||||
});
|
||||
|
||||
const { url } = storeToRefs(usePlayerStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="url === ''">
|
||||
<div wh-full f-cer text-h1 text-primary>
|
||||
<RouterLink to="/config" hover:underline
|
||||
>Please set up the media player</RouterLink
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<iframe
|
||||
v-else
|
||||
:src="url"
|
||||
frameborder="0"
|
||||
allowfullscreen="true"
|
||||
w-full
|
||||
h-full
|
||||
flex-1
|
||||
rounded-12px
|
||||
></iframe>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const store = logStore();
|
||||
|
||||
onActivated(() => {
|
||||
store.get();
|
||||
store.onUpdate();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
store.removeUpdate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="log-box" wh-full overflow-hidden px-2em leading-2em text-12px>
|
||||
<pre whitespace-break-spaces>{{ store.log }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
40
src/pages/login.vue
Normal file
40
src/pages/login.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
const { user, login } = useAuth();
|
||||
|
||||
definePage({
|
||||
name: 'Login',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div f-cer layout-container>
|
||||
<ab-container title="Login" w-365px>
|
||||
<div space-y-16px>
|
||||
<ab-label label="Username">
|
||||
<input
|
||||
v-model="user.username"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
ab-input
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<ab-label label="Password">
|
||||
<input
|
||||
v-model="user.password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
ab-input
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<ab-button size="small" @click="login">Login</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</ab-container>
|
||||
</div>
|
||||
</template>
|
||||
148
src/router-type.d.ts
vendored
Normal file
148
src/router-type.d.ts
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
/// <reference types="unplugin-vue-router/client" />
|
||||
|
||||
import type {
|
||||
// type safe route locations
|
||||
RouteLocationTypedList,
|
||||
RouteLocationResolvedTypedList,
|
||||
RouteLocationNormalizedTypedList,
|
||||
RouteLocationNormalizedLoadedTypedList,
|
||||
RouteLocationAsString,
|
||||
RouteLocationAsRelativeTypedList,
|
||||
RouteLocationAsPathTypedList,
|
||||
|
||||
// helper types
|
||||
// route definitions
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
|
||||
// vue-router extensions
|
||||
_RouterTyped,
|
||||
RouterLinkTyped,
|
||||
RouterLinkPropsTyped,
|
||||
NavigationGuard,
|
||||
UseLinkFnTyped,
|
||||
|
||||
// data fetching
|
||||
_DataLoader,
|
||||
_DefineLoaderOptions,
|
||||
} from 'unplugin-vue-router/types'
|
||||
|
||||
declare module 'vue-router/auto/routes' {
|
||||
export interface RouteNamedMap {
|
||||
'Index': RouteRecordInfo<'Index', '/', Record<never, never>, Record<never, never>>,
|
||||
'Bangumi List': RouteRecordInfo<'Bangumi List', '/bangumi', Record<never, never>, Record<never, never>>,
|
||||
'Calendar': RouteRecordInfo<'Calendar', '/calendar', Record<never, never>, Record<never, never>>,
|
||||
'Config': RouteRecordInfo<'Config', '/config', Record<never, never>, Record<never, never>>,
|
||||
'Downloader': RouteRecordInfo<'Downloader', '/downloader', Record<never, never>, Record<never, never>>,
|
||||
'Log': RouteRecordInfo<'Log', '/log', Record<never, never>, Record<never, never>>,
|
||||
'Player': RouteRecordInfo<'Player', '/player', Record<never, never>, Record<never, never>>,
|
||||
'Login': RouteRecordInfo<'Login', '/login', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router/auto' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export type RouterTyped = _RouterTyped<RouteNamedMap>
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
|
||||
| RouteLocationAsString<RouteNamedMap>
|
||||
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
|
||||
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
|
||||
/**
|
||||
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
|
||||
|
||||
export function useRouter(): RouterTyped
|
||||
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
export const useLink: UseLinkFnTyped<RouteNamedMap>
|
||||
|
||||
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
|
||||
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
|
||||
|
||||
export const RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
|
||||
|
||||
// Experimental Data Fetching
|
||||
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
name: Name,
|
||||
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
loader: (route: RouteLocationNormalizedLoaded) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
|
||||
export {
|
||||
_definePage as definePage,
|
||||
_HasDataLoaderMeta as HasDataLoaderMeta,
|
||||
_setupDataFetchingGuard as setupDataFetchingGuard,
|
||||
_stopDataFetchingScope as stopDataFetchingScope,
|
||||
} from 'unplugin-vue-router/runtime'
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export interface TypesConfig {
|
||||
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
|
||||
beforeRouteLeave: NavigationGuard<RouteNamedMap>
|
||||
|
||||
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
|
||||
$router: _RouterTyped<RouteNamedMap>
|
||||
|
||||
RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,34 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
const YLayout = () => import('../pages/YLayout.vue');
|
||||
const YBangumi = () => import('../pages/bangumi/index.vue');
|
||||
const YDebug = () => import('../pages/debug/index.vue');
|
||||
const YLog = () => import('../pages/journal/index.vue');
|
||||
const YConfig = () => import('../pages/config/index.vue');
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: YLayout,
|
||||
redirect: '/bangumi',
|
||||
children: [
|
||||
{
|
||||
path: 'bangumi',
|
||||
component: YBangumi,
|
||||
},
|
||||
{
|
||||
path: 'debug',
|
||||
component: YDebug,
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
component: YLog,
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
component: YConfig,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
import { createRouter, createWebHashHistory } from 'vue-router/auto';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
router.beforeEach((to) => {
|
||||
const { isLogin } = useAuth();
|
||||
const { type, url } = storeToRefs(usePlayerStore());
|
||||
|
||||
if (!isLogin.value && to.path !== '/login') {
|
||||
return { name: 'Login' };
|
||||
}
|
||||
|
||||
if (isLogin.value && to.path === '/login') {
|
||||
return { name: 'Index' };
|
||||
}
|
||||
|
||||
if (type.value === 'jump' && url.value !== '' && to.path === '/player') {
|
||||
open(url.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
watch(isLogin, (val) => {
|
||||
if (to.path === '/login' && val) {
|
||||
router.replace({ name: 'Index' });
|
||||
}
|
||||
if (to.path !== '/login' && !val) {
|
||||
router.replace({ name: 'Login' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export { router };
|
||||
|
||||
@@ -1,13 +1,78 @@
|
||||
import { getABData } from '../api/bangumi';
|
||||
import type { BangumiItem } from '#/bangumi';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
export const bangumiStore = defineStore('bangumi', () => {
|
||||
const data = ref<BangumiItem[]>();
|
||||
export const useBangumiStore = defineStore('bangumi', () => {
|
||||
const data = ref<BangumiRule[]>();
|
||||
|
||||
const get = async () => {
|
||||
const res = await getABData();
|
||||
data.value = res.data;
|
||||
};
|
||||
function getAll() {
|
||||
const { execute, onResult } = useApi(apiBangumi.getAll);
|
||||
|
||||
return { data, get };
|
||||
function sort(arr: BangumiRule[]) {
|
||||
return arr.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
|
||||
onResult((res) => {
|
||||
const enabled = sort(res.filter((e) => !e.deleted));
|
||||
const disabled = sort(res.filter((e) => e.deleted));
|
||||
|
||||
data.value = [...enabled, ...disabled];
|
||||
});
|
||||
|
||||
execute();
|
||||
}
|
||||
|
||||
function useUpdateRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.updateRule, {
|
||||
failRule: (data) => {
|
||||
return data.status !== 'success';
|
||||
},
|
||||
message: {
|
||||
success: 'Update Success!',
|
||||
fail: 'Update Failed!',
|
||||
error: 'Operation Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
function useDisableRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.disableRule, {
|
||||
failRule: (data) => {
|
||||
return data.status !== 'success';
|
||||
},
|
||||
message: {
|
||||
success: 'Disabled Success!',
|
||||
fail: 'Disabled Failed!',
|
||||
error: 'Operation Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
function useEnableRule() {
|
||||
const { execute, onResult } = useApi(apiBangumi.enableRule, {
|
||||
failRule: (data) => {
|
||||
return data.status !== 'success';
|
||||
},
|
||||
message: {
|
||||
success: 'Enabled Success!',
|
||||
fail: 'Enabled Failed!',
|
||||
error: 'Operation Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
execute,
|
||||
onResult,
|
||||
};
|
||||
}
|
||||
|
||||
return { data, getAll, useUpdateRule, useDisableRule, useEnableRule };
|
||||
});
|
||||
|
||||
@@ -1,70 +1,34 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getConfig, setConfig } from '@/api/config';
|
||||
import { appRestart } from '@/api/program';
|
||||
import type { Config } from '#/config';
|
||||
import { type Config, initConfig } from '#/config';
|
||||
|
||||
const { status } = storeToRefs(programStore());
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const config = ref<Config>(initConfig);
|
||||
|
||||
export const configStore = defineStore('config', () => {
|
||||
const config = ref<Config>();
|
||||
const { execute: getConfig, onResult: onGetConfigRusult } = useApi(
|
||||
apiConfig.getConfig
|
||||
);
|
||||
|
||||
const get = async () => {
|
||||
config.value = await getConfig();
|
||||
};
|
||||
onGetConfigRusult((res) => {
|
||||
config.value = res;
|
||||
});
|
||||
|
||||
const set = async (newConfig: Omit<Config, 'data_version'>) => {
|
||||
let finalConfig: Config;
|
||||
if (config.value !== undefined) {
|
||||
finalConfig = Object.assign(config.value, newConfig);
|
||||
const res = await setConfig(finalConfig);
|
||||
const { execute: set } = useApi(apiConfig.updateConfig, {
|
||||
failRule: (res) => !res,
|
||||
message: {
|
||||
success: 'Apply Success!',
|
||||
fail: 'Apply Failed!',
|
||||
},
|
||||
});
|
||||
|
||||
if (res) {
|
||||
ElMessage({
|
||||
message: '保存成功!',
|
||||
type: 'success',
|
||||
});
|
||||
const setConfig = () => set(config.value);
|
||||
|
||||
if (!status.value) {
|
||||
ElMessageBox.confirm('当前程序没有运行,是否重启?', {
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
appRestart()
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
ElMessage({
|
||||
message: '重启中...',
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
'🚀 ~ file: index.vue:41 ~ .then ~ error:',
|
||||
error
|
||||
);
|
||||
ElMessage({
|
||||
message: '操作失败, 请手动重启容器!',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
} else {
|
||||
ElMessage({
|
||||
message: '保存失败, 请重试!',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
function getSettingGroup<Tkey extends keyof Config>(key: Tkey) {
|
||||
return computed<Config[Tkey]>(() => config.value[key]);
|
||||
}
|
||||
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
config,
|
||||
getConfig,
|
||||
setConfig,
|
||||
getSettingGroup,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,25 +1,51 @@
|
||||
import { getABLog } from '../api/debug';
|
||||
|
||||
export const logStore = defineStore('log', () => {
|
||||
export const useLogStore = defineStore('log', () => {
|
||||
const log = ref('');
|
||||
const timer = ref<NodeJS.Timer | null>(null);
|
||||
const { auth } = useAuth();
|
||||
const message = useMessage();
|
||||
|
||||
const get = async () => {
|
||||
log.value = await getABLog();
|
||||
};
|
||||
function get() {
|
||||
const { execute, onResult } = useApi(apiLog.getLog);
|
||||
|
||||
const onUpdate = () => {
|
||||
timer.value = setInterval(() => get(), 3000);
|
||||
};
|
||||
onResult((value) => {
|
||||
log.value = value;
|
||||
});
|
||||
|
||||
const removeUpdate = () => {
|
||||
clearInterval(Number(timer.value));
|
||||
};
|
||||
if (auth.value !== '') {
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
||||
const { execute: reset, onResult: onClearLogResult } = useApi(
|
||||
apiLog.clearLog
|
||||
);
|
||||
|
||||
onClearLogResult((res) => {
|
||||
if (res) {
|
||||
log.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(get, 3000, {
|
||||
immediate: false,
|
||||
immediateCallback: true,
|
||||
});
|
||||
|
||||
function copy() {
|
||||
const { copy: copyLog, isSupported } = useClipboard({ source: log });
|
||||
if (isSupported) {
|
||||
copyLog();
|
||||
message.success('Copy Success!');
|
||||
} else {
|
||||
message.error('Your browser does not support Clipboard API!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
log,
|
||||
get,
|
||||
reset,
|
||||
onUpdate,
|
||||
removeUpdate,
|
||||
offUpdate,
|
||||
copy,
|
||||
};
|
||||
});
|
||||
|
||||
13
src/store/player.ts
Normal file
13
src/store/player.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
type MediaPlayerType = 'jump' | 'iframe';
|
||||
|
||||
export const usePlayerStore = defineStore('player', () => {
|
||||
const types = ref<MediaPlayerType[]>(['jump', 'iframe']);
|
||||
const type = useLocalStorage<MediaPlayerType>('media-player-type', 'jump');
|
||||
const url = useLocalStorage<string>('media-player-url', '');
|
||||
|
||||
return {
|
||||
types,
|
||||
type,
|
||||
url,
|
||||
};
|
||||
});
|
||||
@@ -1,20 +1,52 @@
|
||||
import { appStatus } from '../api/program';
|
||||
export const useProgramStore = defineStore('program', () => {
|
||||
const { auth } = useAuth();
|
||||
const running = ref(false);
|
||||
|
||||
export const programStore = defineStore('program', () => {
|
||||
const status = ref(false);
|
||||
const timer = ref<NodeJS.Timer | null>(null);
|
||||
function getStatus() {
|
||||
const { execute, onResult } = useApi(apiProgram.status);
|
||||
|
||||
const getStatus = async () => {
|
||||
status.value = await appStatus();
|
||||
};
|
||||
onResult((res) => {
|
||||
running.value = res;
|
||||
});
|
||||
|
||||
const onUpdate = () => {
|
||||
timer.value = setInterval(() => getStatus(), 3000);
|
||||
};
|
||||
if (auth.value !== '') {
|
||||
execute();
|
||||
}
|
||||
}
|
||||
|
||||
const { pause: offUpdate, resume: onUpdate } = useIntervalFn(
|
||||
getStatus,
|
||||
3000,
|
||||
{
|
||||
immediate: false,
|
||||
immediateCallback: true,
|
||||
}
|
||||
);
|
||||
|
||||
function opts(handle: string) {
|
||||
return {
|
||||
failRule: (res: boolean) => !res,
|
||||
message: {
|
||||
success: `${handle} Success!`,
|
||||
fail: `${handle} Failed!`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { execute: start } = useApi(apiProgram.start, opts('Start'));
|
||||
const { execute: pause } = useApi(apiProgram.stop, opts('Pause'));
|
||||
const { execute: shutdown } = useApi(apiProgram.shutdown, opts('Shutdown'));
|
||||
const { execute: restart } = useApi(apiProgram.restart, opts('Restart'));
|
||||
|
||||
return {
|
||||
status,
|
||||
running,
|
||||
getStatus,
|
||||
onUpdate,
|
||||
offUpdate,
|
||||
|
||||
start,
|
||||
pause,
|
||||
shutdown,
|
||||
restart,
|
||||
};
|
||||
});
|
||||
|
||||
50
src/style/global.scss
Normal file
50
src/style/global.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
$scrollbar-color: #372a87;
|
||||
|
||||
:root {
|
||||
--scrollbar-size: 6px;
|
||||
--scrollbar-color: transparent;
|
||||
--scrollbar-thumb-color: #{rgba($scrollbar-color, 0.5)};
|
||||
--scrollbar-thumb-hover-color: #{rgba($scrollbar-color, 1)};
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
/* 滚动槽--外层轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-color);
|
||||
}
|
||||
|
||||
/* 内层轨道(不包含滚动块部分) */
|
||||
/* 透明度设置为全透明,使得滚动条背景色为网页颜色 */
|
||||
::-webkit-scrollbar-track-piece {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: calc(var(--scrollbar-size) / 2);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条按钮 */
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 横向滚动条和纵向滚动条相交处尖角的颜色 */
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
13
src/style/mixin.scss
Normal file
13
src/style/mixin.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@mixin bg-mouse-event($normal, $hover, $active) {
|
||||
background: $normal;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition: none;
|
||||
background: $active;
|
||||
}
|
||||
}
|
||||
16
src/style/transition.scss
Normal file
16
src/style/transition.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
// transition
|
||||
|
||||
.fade {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// transition-group
|
||||
43
src/utils/axios.ts
Normal file
43
src/utils/axios.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import Axios from 'axios';
|
||||
import type { ApiError } from '#/error';
|
||||
|
||||
export const axios = Axios.create();
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
const { auth } = useAuth();
|
||||
|
||||
if (auth.value !== '' && config.headers) {
|
||||
config.headers.Authorization = auth.value;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(res) => {
|
||||
return res;
|
||||
},
|
||||
(err) => {
|
||||
const status = err.response.status as ApiError['status'];
|
||||
const detail = err.response.data.detail as ApiError['detail'];
|
||||
const error = {
|
||||
status,
|
||||
detail,
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
/** token 过期 */
|
||||
if (error.status === 401) {
|
||||
const { auth } = useAuth();
|
||||
auth.value = '';
|
||||
}
|
||||
|
||||
if (error.status === 500) {
|
||||
const msg = error.detail ? error.detail : 'Request Error!';
|
||||
message.error(msg);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -17,15 +17,14 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"#/*": ["types/*"]
|
||||
},
|
||||
"types": ["element-plus/global"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"types/config.ts"
|
||||
"types/**/*.ts"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
18
types/auth.ts
Normal file
18
types/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface LoginSuccess {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expire: number;
|
||||
}
|
||||
|
||||
export interface Logout {
|
||||
message: 'logout success';
|
||||
}
|
||||
|
||||
export interface Update extends LoginSuccess {
|
||||
message: 'update success';
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
export interface BangumiItem {
|
||||
export interface BangumiRule {
|
||||
added: boolean;
|
||||
deleted: boolean;
|
||||
dpi: string;
|
||||
eps_collect: boolean;
|
||||
filter: string[];
|
||||
group_name: string;
|
||||
id: number;
|
||||
official_title: string;
|
||||
year: string | null;
|
||||
title_raw: string;
|
||||
offset: number;
|
||||
poster_link: string | null;
|
||||
rss_link: string[];
|
||||
rule_name: string;
|
||||
save_path: string;
|
||||
season: number;
|
||||
season_raw: string;
|
||||
group_name: string;
|
||||
dpi: string;
|
||||
source: string;
|
||||
source: string | null;
|
||||
subtitle: string;
|
||||
eps_collect: boolean;
|
||||
offset: number;
|
||||
filter: string[];
|
||||
rss_link: string[];
|
||||
poster_link: string;
|
||||
added: boolean;
|
||||
title_raw: string;
|
||||
year: string | null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user