mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 20:42:40 +08:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
058b32a263 | ||
|
|
e7b960838e | ||
|
|
14e776a287 | ||
|
|
73f11b920f | ||
|
|
5c93040a8e | ||
|
|
a517769e8a | ||
|
|
4bb59a9f05 | ||
|
|
b37879d2d4 | ||
|
|
05defc39d7 | ||
|
|
18bfad07d2 | ||
|
|
b83591255d | ||
|
|
804350bc81 | ||
|
|
46e1cae0bb | ||
|
|
81062d4580 | ||
|
|
55481db2ee | ||
|
|
ecdd12f5a9 | ||
|
|
ef92cdc183 | ||
|
|
08f4a6cf2c | ||
|
|
38889acb4e | ||
|
|
c0517cd29a | ||
|
|
084449ccf3 | ||
|
|
0e8203ae03 | ||
|
|
236440be52 | ||
|
|
6f7e4bb272 | ||
|
|
38dcd3635a | ||
|
|
a3f3330dad | ||
|
|
bbc6c57c08 | ||
|
|
2f36a8edef | ||
|
|
df637fb887 | ||
|
|
be74c92a35 | ||
|
|
a219a64e20 | ||
|
|
25c22a276a | ||
|
|
6e6be057ca | ||
|
|
af69efa48b | ||
|
|
c551083fa4 | ||
|
|
9767feed29 | ||
|
|
4392818e92 | ||
|
|
8d22bafeb6 | ||
|
|
89ddd1fb78 | ||
|
|
24513fa22b | ||
|
|
cddde0c2a0 | ||
|
|
9c674e0018 | ||
|
|
0c6476d283 | ||
|
|
bf0c529a59 | ||
|
|
877bb4d4a2 | ||
|
|
dc4db0b2b3 | ||
|
|
a738d4a3b9 | ||
|
|
e9866a04df | ||
|
|
4f5193d602 | ||
|
|
37b92c55ba | ||
|
|
9299f1bcb6 | ||
|
|
7fe12192df | ||
|
|
1169644ab3 | ||
|
|
6f7770ed43 | ||
|
|
8059fd6f90 | ||
|
|
556dbd8d78 | ||
|
|
6695fd8c14 | ||
|
|
3ab0229275 | ||
|
|
99467127a0 | ||
|
|
90d73b7bd5 | ||
|
|
2e326e1798 | ||
|
|
251eac93c7 | ||
|
|
c74d70808c | ||
|
|
e63b2d7152 | ||
|
|
16b29b56a5 | ||
|
|
6d79c4fe2f | ||
|
|
4b1fb60ee3 | ||
|
|
1d2be54f9e | ||
|
|
83547e32db | ||
|
|
70ddb929f2 | ||
|
|
8b22961394 | ||
|
|
c15d42c179 | ||
|
|
098e473cab | ||
|
|
f6f3d9368a | ||
|
|
9558a420e9 | ||
|
|
4d3b69ca34 | ||
|
|
fdcc4a44c8 | ||
|
|
5de0494538 | ||
|
|
2045f833e4 | ||
|
|
cc4f89aac1 | ||
|
|
1c2f2c17d4 | ||
|
|
ace7a6621f | ||
|
|
d02fe55a1e | ||
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 | ||
|
|
d0aada1d3d | ||
|
|
8a4848387c | ||
|
|
6904fc7da3 | ||
|
|
28c55a05e6 | ||
|
|
562c829267 | ||
|
|
b200ed242d | ||
|
|
815cfe55df | ||
|
|
40a1094d74 | ||
|
|
346650c091 | ||
|
|
7f74715f51 | ||
|
|
b6fcee517d | ||
|
|
4f62551f6b | ||
|
|
3980249271 | ||
|
|
e3b11b1130 | ||
|
|
f866f23af1 | ||
|
|
c793bc24f0 | ||
|
|
591a46d559 | ||
|
|
2852f26702 | ||
|
|
fc818fdfd6 | ||
|
|
5566ef87f8 | ||
|
|
366fe34d6f | ||
|
|
37a0e83124 | ||
|
|
061a3f393a | ||
|
|
1dff22aeab | ||
|
|
78e6fd4809 | ||
|
|
96bbf3d0f2 | ||
|
|
a842eaba4e | ||
|
|
37565bf8e4 | ||
|
|
beb158b387 | ||
|
|
408eb06f8d | ||
|
|
abe0e44635 | ||
|
|
cfaf414f1c | ||
|
|
f9c4dc616b | ||
|
|
bf845bab6b | ||
|
|
bae9c85990 | ||
|
|
56bbb8d0ff | ||
|
|
60d3565231 | ||
|
|
81340fd287 | ||
|
|
c10c348c73 | ||
|
|
65cb7d9674 | ||
|
|
24f1a10ff7 | ||
|
|
767d11182a | ||
|
|
cf363f667e |
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=/api/v1/
|
||||
VITE_API_BASE_URL=api/v1/
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -31,8 +31,8 @@
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
|
||||
32
README.md
32
README.md
@@ -1,35 +1,39 @@
|
||||
# MoviePilot-Frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目。
|
||||
|
||||
## Recommended IDE Setup
|
||||
## 推荐的IDE设置
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur).
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
## 配置Vite
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates.
|
||||
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
|
||||
|
||||
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can run `Volar: Switch TS Plugin on/off` from VSCode command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
## 依赖安装
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
### 开发运行
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
### 编译打包
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 静态运行
|
||||
|
||||
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
|
||||
|
||||
2. 使用 `node` 命令直接运行`service.js`,默认监听 `3000` 端口,设置环境变量 `NGINX_PORT` 来调整运行端口。
|
||||
|
||||
```shell
|
||||
node dist/service.js
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.4.4",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -24,10 +24,12 @@
|
||||
"@floating-ui/dom": "1.2.8",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"ace-builds": "^1.32.6",
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.4.0",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
@@ -47,6 +49,7 @@
|
||||
"vue-prism-component": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^1.6.0",
|
||||
"vuetify": "3.3.5",
|
||||
|
||||
@@ -28,7 +28,12 @@ app.use(
|
||||
|
||||
// 处理根路径的请求
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
// 处理所有其他请求,重定向到前端入口文件
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
|
||||
@@ -20,26 +20,30 @@ const {
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
|
||||
globalTheme.name.value = nextTheme
|
||||
savedTheme.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
function updateTheme() {
|
||||
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||
globalTheme.name.value = theme
|
||||
savedTheme.value = theme
|
||||
// 修改载入时背景色
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
|
||||
themeTransition()
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
// 监听系统主题变化
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
(val) => {
|
||||
currentThemeName.value = val
|
||||
},
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
currentThemeName.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
}
|
||||
|
||||
// Apply saved theme on page load
|
||||
// onMounted(() => {
|
||||
// globalTheme.name.value = savedTheme.value
|
||||
|
||||
21
src/@core/utils/dom.ts
Normal file
21
src/@core/utils/dom.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefer(maxFrameCount = 1) {
|
||||
const frameCount = ref(0)
|
||||
const refreshFrameCount = () => {
|
||||
requestAnimationFrame(() => {
|
||||
frameCount.value++
|
||||
if (frameCount.value < maxFrameCount)
|
||||
refreshFrameCount()
|
||||
})
|
||||
}
|
||||
refreshFrameCount()
|
||||
return function (showInFrameCount: number) {
|
||||
return frameCount.value >= showInFrameCount
|
||||
}
|
||||
}
|
||||
@@ -109,3 +109,41 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// 格式化剧集列表
|
||||
export function formatEp(nums: number[]): string {
|
||||
if (!nums.length)
|
||||
return ''
|
||||
|
||||
if (nums.length === 1)
|
||||
return nums[0].toString()
|
||||
|
||||
// 将数组升序排序
|
||||
nums.sort((a, b) => a - b)
|
||||
const formattedRanges: string[] = []
|
||||
let start = nums[0]
|
||||
let end = nums[0]
|
||||
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
if (nums[i] === end + 1) {
|
||||
end = nums[i]
|
||||
}
|
||||
else {
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
start = end = nums[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
return formattedRanges.join('、')
|
||||
}
|
||||
|
||||
23
src/@core/utils/image.ts
Normal file
23
src/@core/utils/image.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import ColorThief from 'colorthief'
|
||||
|
||||
// 将 RGB 转换为十六进制
|
||||
function rgbStringToHex(rgbArray: number[]): string {
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
|
||||
throw new Error('Invalid RGB string format')
|
||||
|
||||
const [r, g, b] = rgbArray
|
||||
|
||||
const toHex = (c: number): string => {
|
||||
const hex = c.toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
}
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(image: HTMLImageElement): Promise<string> {
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export function calculateTimeDiff(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
// 使用当前时区
|
||||
const inputDate = new Date(inputTime)
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||
const secones = Math.floor(secondsDifference % 60)
|
||||
|
||||
if (days > 0)
|
||||
return `${days}天${hours}小时${minutes}分钟`
|
||||
|
||||
else if (hours > 0)
|
||||
return `${hours}小时${minutes}分钟`
|
||||
|
||||
else if (minutes > 0)
|
||||
return `${minutes}分钟`
|
||||
|
||||
else if (secones > 0)
|
||||
return `${secones}秒`
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
|
||||
30
src/@core/utils/navigator.ts
Normal file
30
src/@core/utils/navigator.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 请求和获取剪贴板内容
|
||||
export async function getClipboardContent() {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return await navigator.clipboard.readText()
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('paste')
|
||||
const content = input.value
|
||||
document.body.removeChild(input)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// 将内容复制到剪贴板,兼容非安全域场景
|
||||
export async function copyToClipboard(content: string) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(content)
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
input.value = content
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
15
src/App.vue
15
src/App.vue
@@ -1,15 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useTheme } from 'vuetify'
|
||||
import store from './store'
|
||||
|
||||
// 第一时间应用主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
||||
import store from './store'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 设置主题
|
||||
function setTheme() {
|
||||
const { global: globalTheme } = useTheme()
|
||||
let theme = localStorage.getItem('theme') || 'light'
|
||||
if (theme === 'auto')
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
globalTheme.name.value = theme
|
||||
}
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
@@ -32,6 +38,7 @@ function startSSEMessager() {
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
setTheme()
|
||||
startSSEMessager()
|
||||
})
|
||||
</script>
|
||||
|
||||
54
src/ace-config.ts
Normal file
54
src/ace-config.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import ace from 'ace-builds'
|
||||
|
||||
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||
|
||||
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
|
||||
|
||||
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||
|
||||
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
|
||||
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
|
||||
|
||||
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
|
||||
|
||||
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
|
||||
|
||||
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||
|
||||
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||
|
||||
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||
|
||||
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
|
||||
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
|
||||
ace.require('ace/ext/language_tools')
|
||||
@@ -80,8 +80,14 @@ export interface Subscribe {
|
||||
// 是否洗版,数字或者boolean
|
||||
best_version: any
|
||||
|
||||
// 使用 imdbid 搜索
|
||||
search_imdbid?: boolean
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
|
||||
// 保存目录
|
||||
save_path: string
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -540,9 +546,6 @@ export interface Plugin {
|
||||
// 插件图标
|
||||
plugin_icon?: string
|
||||
|
||||
// 主题色
|
||||
plugin_color?: string
|
||||
|
||||
// 插件版本
|
||||
plugin_version?: string
|
||||
|
||||
@@ -651,6 +654,16 @@ export interface TorrentInfo {
|
||||
|
||||
// 促销描述
|
||||
volume_factor: string
|
||||
|
||||
// 免费时间
|
||||
freedate: string
|
||||
|
||||
// 剩余免费时间
|
||||
freedate_diff: string
|
||||
|
||||
// 种子类型
|
||||
category: string
|
||||
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
@@ -870,6 +883,9 @@ export interface ScheduleInfo {
|
||||
// 名称
|
||||
name: string
|
||||
|
||||
// 提供者
|
||||
provider: string
|
||||
|
||||
// 状态
|
||||
status: string
|
||||
|
||||
@@ -917,3 +933,26 @@ export interface FileItem {
|
||||
children: FileItem[]
|
||||
modify_time: number
|
||||
}
|
||||
|
||||
// 媒体服务器播放条目
|
||||
export interface MediaServerPlayItem {
|
||||
id?: string | number
|
||||
title: string
|
||||
subtitle?: string
|
||||
type?: string
|
||||
image?: string
|
||||
link?: string
|
||||
percent?: number
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
export interface MediaServerLibrary {
|
||||
server: string
|
||||
id?: string | number
|
||||
name: string
|
||||
path?: string
|
||||
type?: string
|
||||
image?: string
|
||||
image_list?: string[]
|
||||
link?: string
|
||||
}
|
||||
|
||||
BIN
src/assets/images/logos/fanart.webp
Normal file
BIN
src/assets/images/logos/fanart.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/logos/plugin.png
Normal file
BIN
src/assets/images/logos/plugin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/images/misc/emby.png
Normal file
BIN
src/assets/images/misc/emby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/jellyfin.png
Normal file
BIN
src/assets/images/misc/jellyfin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/plex.png
Normal file
BIN
src/assets/images/misc/plex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
import List from './filebrowser/List.vue'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import List from './filebrowser/List.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
@@ -70,10 +70,12 @@ const storagesArray = computed(() => {
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading)
|
||||
if (loading) {
|
||||
loading++
|
||||
else if (loading > 0)
|
||||
}
|
||||
else if (loading > 0) {
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
@@ -92,56 +94,58 @@ function sortChanged(s: string) {
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<Toolbar
|
||||
:path="props.path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="props.endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||
<div v-if="path">
|
||||
<Toolbar
|
||||
:path="path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
89
src/components/cards/BackdropCard.vue
Normal file
89
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': imageLoaded,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="getImgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="props.media?.percent"
|
||||
:model-value="props.media?.percent"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-shadow{
|
||||
text-shadow:1px 1px #777;
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,11 @@ function getSpeedText() {
|
||||
// 下载状态
|
||||
const isDownloading = ref(props.info?.state === 'downloading')
|
||||
|
||||
// 监听props.info?.state的变化
|
||||
watch(() => props.info?.state, (newValue) => {
|
||||
isDownloading.value = newValue === 'downloading';
|
||||
});
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
|
||||
202
src/components/cards/LibraryCard.vue
Normal file
202
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerLibrary } from '@/api/types'
|
||||
import plex from '@images/misc/plex.png'
|
||||
import emby from '@images/misc/emby.png'
|
||||
import jellyfin from '@images/misc/jellyfin.png'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerLibrary>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// canvas
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
|
||||
// 图片地址
|
||||
const imgUrl = ref('')
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载错误
|
||||
const imageError = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
function imageErrorHandler() {
|
||||
imageError.value = true
|
||||
}
|
||||
|
||||
// 默认图片
|
||||
function getDefaultImage() {
|
||||
if (props.media?.server === 'plex')
|
||||
return plex
|
||||
else if (props.media?.server === 'emby')
|
||||
return emby
|
||||
else if (props.media?.server === 'jellyfin')
|
||||
return jellyfin
|
||||
else
|
||||
return plex
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string) {
|
||||
if (!url)
|
||||
return getDefaultImage()
|
||||
else
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}/0`
|
||||
}
|
||||
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[]) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
if (IMAGES.length === 0)
|
||||
return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
for (let i = 0; i < IMAGES.length; i++)
|
||||
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}/0`
|
||||
|
||||
// canvas
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||
const MARGIN_WIDTH = 4
|
||||
const MARGIN_HEIGHT = 4
|
||||
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return getDefaultImage()
|
||||
|
||||
// 设置背景色为黑色
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 绘制图片
|
||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
const img = new Image()
|
||||
img.setAttribute('crossorigin', 'anonymous')
|
||||
img.src = imgSrc
|
||||
await new Promise(resolve => img.onload = resolve)
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = MARGIN_HEIGHT
|
||||
|
||||
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0, canvas.height)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
x,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
POSTER_WIDTH,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
0,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 绘制多张图片
|
||||
const loopCount = Math.min(4, IMAGES.length)
|
||||
for (let i = 0; i < loopCount; i++)
|
||||
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||
|
||||
// 转换为图片地址
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||
else
|
||||
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||
<VImg
|
||||
:src="imgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
@error="imageErrorHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.name }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -146,7 +151,7 @@ async function addSubscribe(season = 0) {
|
||||
)
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1) {
|
||||
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
@@ -223,7 +228,7 @@ async function handleCheckSubscribe() {
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
title: props.media?.title,
|
||||
@@ -269,7 +274,7 @@ async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
@@ -302,6 +307,20 @@ async function getMediaSeasons() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function querySubscribeRules() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
subscribeRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
function handleSubscribe() {
|
||||
if (isSubscribed.value)
|
||||
@@ -373,6 +392,7 @@ function handleSearch() {
|
||||
onBeforeMount(() => {
|
||||
handleCheckSubscribe()
|
||||
handleCheckExists()
|
||||
querySubscribeRules()
|
||||
})
|
||||
|
||||
// 计算图片地址
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -13,6 +15,12 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['install'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -25,6 +33,17 @@ const progressText = ref('正在安装插件...')
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
async function installPlugin() {
|
||||
try {
|
||||
@@ -52,7 +71,7 @@ async function installPlugin() {
|
||||
emit('install')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -61,11 +80,54 @@ async function installPlugin() {
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath = computed(() => {
|
||||
return props.plugin?.plugin_icon?.startsWith('http')
|
||||
? props.plugin?.plugin_icon
|
||||
: `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
|
||||
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 访问插件页面
|
||||
function visitPluginPage() {
|
||||
// 将raw.githubusercontent.com转换为项目地址
|
||||
let repoUrl = props.plugin?.repo_url
|
||||
if (repoUrl) {
|
||||
if (repoUrl.includes('raw.githubusercontent.com')) {
|
||||
if (!repoUrl.endsWith('/'))
|
||||
repoUrl += '/'
|
||||
|
||||
if (repoUrl.split('/').length < 6)
|
||||
repoUrl = `${repoUrl}main/`
|
||||
|
||||
try {
|
||||
const [user, repo] = repoUrl.split('/').slice(-4, -2)
|
||||
repoUrl = `https://github.com/${user}/${repo}`
|
||||
}
|
||||
catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
repoUrl = props.plugin?.author_url
|
||||
}
|
||||
window.open(repoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: visitPluginPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -76,11 +138,34 @@ const iconPath = computed(() => {
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 right-5"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
@@ -89,13 +174,15 @@ const iconPath = computed(() => {
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import FormRender from '@/components/render/FormRender.vue'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import store from '@/store'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,9 +20,18 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 本身是否可见
|
||||
const isVisible = ref(true)
|
||||
|
||||
@@ -31,17 +44,44 @@ const pluginConfigForm = ref({})
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件详情页面
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 插件详情页面配置项
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 调用API卸载插件
|
||||
async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认卸载插件 ${props.plugin?.plugin_name} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
@@ -74,7 +114,7 @@ async function loadPluginForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API读取详情页面
|
||||
// 调用API读取数据页面
|
||||
async function loadPluginPage() {
|
||||
try {
|
||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||
@@ -117,9 +157,9 @@ async function savePluginConf() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示插件详情
|
||||
// 显示插件数据
|
||||
async function showPluginInfo() {
|
||||
// 加载详情
|
||||
// 加载数据
|
||||
await loadPluginPage()
|
||||
pluginConfigDialog.value = false
|
||||
pluginInfoDialog.value = true
|
||||
@@ -137,16 +177,66 @@ async function showPluginConfig() {
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath = computed(() => {
|
||||
return props.plugin?.plugin_icon?.startsWith('http')
|
||||
? props.plugin?.plugin_icon
|
||||
: `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
|
||||
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 重置插件
|
||||
async function resetPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`plugin/reset/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 数据已重置`)
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 重置失败:${result.message}}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
}
|
||||
|
||||
// 查看日志URL
|
||||
function openLoggerWindow() {
|
||||
const token = store.state.auth.token
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
title: '查看数据',
|
||||
value: 1,
|
||||
show: props.plugin?.has_page,
|
||||
props: {
|
||||
@@ -155,7 +245,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '配置',
|
||||
title: '设置',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
@@ -164,15 +254,45 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
title: '重置',
|
||||
value: 3,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cancel',
|
||||
color: 'warning',
|
||||
click: resetPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '查看日志',
|
||||
value: 5,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-file-document-outline',
|
||||
click: () => {
|
||||
openLoggerWindow()
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 5,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
click: visitAuthorPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -191,7 +311,7 @@ const dropdownItems = ref([
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
@@ -220,13 +340,15 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
@@ -261,7 +383,7 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||
查看详情
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@@ -274,7 +396,7 @@ const dropdownItems = ref([
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件详情页面 -->
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
scrollable
|
||||
|
||||
102
src/components/cards/PosterCard.vue
Normal file
102
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影')
|
||||
return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧')
|
||||
return ' bg-indigo-500 border-indigo-600'
|
||||
else
|
||||
return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="props">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
@click.stop="goPlay"
|
||||
>
|
||||
<VImg
|
||||
aspect-ratio="2/3"
|
||||
:src="getImgUrl"
|
||||
class="object-cover aspect-w-2 aspect-h-3"
|
||||
:class="hover.isHovering ? 'on-hover' : ''"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
@error="imageLoadError = true"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.on-hover img {
|
||||
@apply brightness-50;
|
||||
}
|
||||
</style>
|
||||
@@ -80,6 +80,7 @@ const resourceItemsPerPage = ref(25)
|
||||
const userPwForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 打开种子详情页面
|
||||
@@ -152,6 +153,7 @@ async function updateSiteCookie() {
|
||||
params: {
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
code: userPwForm.value.code,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -335,7 +337,7 @@ onMounted(() => {
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.username"
|
||||
@@ -345,7 +347,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
@@ -359,6 +361,15 @@ onMounted(() => {
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.code"
|
||||
label="两步验证"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -412,6 +423,23 @@ onMounted(() => {
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.raw?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang='ts' setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import router from '@/router'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -55,7 +56,7 @@ function getPercentage() {
|
||||
return Math.round(
|
||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
||||
/ (props.media?.total_episode ?? 1))
|
||||
* 100,
|
||||
* 100,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,8 +127,28 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
title: '查看详情',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-open-in-new',
|
||||
click: () => {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${
|
||||
props.media?.tmdbid
|
||||
? `tmdb:${props.media?.tmdbid}`
|
||||
: `douban:${props.media?.doubanid}`
|
||||
}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -162,7 +183,7 @@ const dropdownItems = ref([
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
@@ -252,7 +273,8 @@ const dropdownItems = ref([
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/> {{ lastUpdateText }}
|
||||
/>
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
|
||||
@@ -25,7 +25,6 @@ const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
console.log(item)
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -195,6 +195,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
@@ -150,6 +150,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pt-2"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
@@ -267,6 +267,28 @@ async function recognize(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API刮削
|
||||
async function scrape(path: string) {
|
||||
try {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在刮削 ${path} ...`
|
||||
const result: { [key: string]: any } = await api.get('media/scrape', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!result.success)
|
||||
$toast.error(result.message)
|
||||
else
|
||||
$toast.success(`${path}削刮完成!`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -279,8 +301,17 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
title: '刮削',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-auto-fix',
|
||||
click: (_item: FileItem) => {
|
||||
scrape(_item.path || '')
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
@@ -288,7 +319,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 3,
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
@@ -296,7 +327,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 4,
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
@@ -345,111 +376,173 @@ onMounted(() => {
|
||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||
<VList v-if="dirs.length" subheader>
|
||||
<VListSubheader>目录</VListSubheader>
|
||||
<VListItem
|
||||
<VHover
|
||||
v-for="(item, index) in dirs"
|
||||
:key="index"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-bind="hover.props"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<span v-show="hover.isHovering" class="flex">
|
||||
<VTooltip text="识别">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</VTooltip>
|
||||
<VTooltip text="刮削">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||
<VIcon icon="mdi-auto-fix" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="重命名">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="整理">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="删除">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" color="error" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VHover>
|
||||
</VList>
|
||||
<VDivider v-if="dirs.length && files.length" />
|
||||
<VList v-if="files.length" subheader>
|
||||
<VListSubheader>文件</VListSubheader>
|
||||
<VListItem
|
||||
<VHover
|
||||
v-for="(item, index) in files"
|
||||
:key="index"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-bind="hover.props"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<span v-show="hover.isHovering" class="flex">
|
||||
<VTooltip text="识别">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</VTooltip>
|
||||
<VTooltip text="刮削">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||
<VIcon icon="mdi-auto-fix" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="重命名">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="整理">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="删除">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" color="error" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VHover>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
|
||||
@@ -144,19 +144,31 @@ const sortIcon = computed(() => {
|
||||
</template>
|
||||
</VToolbarItems>
|
||||
<div class="flex-grow-1" />
|
||||
<IconBtn @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<VTooltip text="调整排序">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="返回上一级" v-if="pathSegments.length > 0">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VDialog
|
||||
v-model="newFolderPopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="新建文件夹" v-bind="props">
|
||||
<VIcon icon="mdi-folder-plus-outline" />
|
||||
<IconBtn v-bind="props">
|
||||
<VTooltip text="新建文件夹">
|
||||
<template #activator="{ props: _props }">
|
||||
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard title="新建文件夹">
|
||||
|
||||
39
src/components/form/ImportCodeForm.vue
Normal file
39
src/components/form/ImportCodeForm.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -121,6 +121,9 @@ async function updateSiteInfo() {
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
:close-on-back="false"
|
||||
persistent
|
||||
eager
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
@@ -199,7 +202,7 @@ async function updateSiteInfo() {
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
v-model="siteForm.limit_count"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
total_episode: 0,
|
||||
start_episode: 0,
|
||||
best_version: 0,
|
||||
search_imdbid: false,
|
||||
sites: [],
|
||||
type: '',
|
||||
name: '',
|
||||
@@ -39,6 +40,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
save_path: '',
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -322,6 +324,16 @@ watchEffect(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.save_path"
|
||||
label="保存路径"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
@@ -332,6 +344,15 @@ watchEffect(() => {
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="subscribeForm.search_imdbid"
|
||||
label="使用 ImdbID 搜索"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -32,23 +32,58 @@ const formData = ref<any>(elementProps.form || {})
|
||||
<template>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-if="!formItem.html"
|
||||
v-if="!formItem.html && !!formItem.props?.modelvalue"
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model || '']"
|
||||
v-model:value="formData[formItem.props?.modelvalue]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<FormRender
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
v-model="formData[innerItem.props?.model || '']"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-if="formItem.html"
|
||||
v-else-if="formItem.html"
|
||||
v-bind="formItem.props"
|
||||
v-html="formItem.html"
|
||||
/>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-else
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
@@ -14,6 +14,10 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
name: 'purple',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'auto',
|
||||
icon: 'mdi-brightness-auto',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function openSearchDialog() {
|
||||
<VTextField
|
||||
key="search_navbar"
|
||||
v-model="searchWord"
|
||||
class="d-none d-lg-block text-disabled"
|
||||
class="d-none d-lg-block text-disabled search-box"
|
||||
density="compact"
|
||||
variant="solo"
|
||||
label="搜索电影、电视剧"
|
||||
@@ -98,3 +98,9 @@ function openSearchDialog() {
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.search-box div.v-input__control div[role="textbox"] {
|
||||
border: 1px solid rgb(var(--v-theme-background));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import NameTestView from '@/views/system/NameTestView.vue'
|
||||
import NetTestView from '@/views/system/NetTestView.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false)
|
||||
@@ -18,6 +19,12 @@ const loggingDialog = ref(false)
|
||||
|
||||
// 过滤规则弹窗
|
||||
const ruleTestDialog = ref(false)
|
||||
|
||||
// 拼接全部日志url
|
||||
function allLoggingUrl() {
|
||||
const token = store.state.auth.token
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -171,8 +178,19 @@ const ruleTestDialog = ref(false)
|
||||
class="w-full lg:w-4/5"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="实时日志">
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="inline-flex">
|
||||
实时日志
|
||||
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<VIcon icon="mdi-open-in-new" />
|
||||
<span class="ms-1">在新窗口中打开</span>
|
||||
</div>
|
||||
</a>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<LoggingView />
|
||||
</VCardText>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
api.get('user/current')
|
||||
.catch(() => {
|
||||
router.replace('/login')
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayoutWithVerticalNav>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
<component :is="Component" v-if="route.meta.keepAlive" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||
</router-view>
|
||||
</DefaultLayoutWithVerticalNav>
|
||||
</template>
|
||||
|
||||
13
src/main.ts
13
src/main.ts
@@ -1,7 +1,11 @@
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { createApp } from 'vue'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import './ace-config'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { removeEl } from './@core/utils/dom'
|
||||
import App from '@/App.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import { loadFonts } from '@/plugins/webfontloader'
|
||||
@@ -11,14 +15,17 @@ import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { removeEl } from '@/util'
|
||||
|
||||
loadFonts()
|
||||
|
||||
// Create vue app
|
||||
// 创建Vue实例
|
||||
const app = createApp(App)
|
||||
|
||||
// Use plugins Mount vue app
|
||||
// 注册全局组件
|
||||
app.component('VAceEditor', VAceEditor)
|
||||
.component('VApexChart', VueApexCharts)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
|
||||
@@ -6,11 +6,57 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
|
||||
// 仪表盘配置
|
||||
const dashboard_names = {
|
||||
storage: '存储空间',
|
||||
mediaStatistic: '媒体统计',
|
||||
weeklyOverview: '最近入库',
|
||||
speed: '实时速率',
|
||||
scheduler: '后台任务',
|
||||
cpu: 'CPU',
|
||||
memory: '内存',
|
||||
library: '我的媒体库',
|
||||
playing: '继续观看',
|
||||
latest: '最近添加',
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 从localStorage中获取数据
|
||||
const default_config = {
|
||||
mediaStatistic: true,
|
||||
scheduler: false,
|
||||
speed: false,
|
||||
storage: true,
|
||||
weeklyOverview: false,
|
||||
cpu: false,
|
||||
memory: false,
|
||||
library: true,
|
||||
playing: true,
|
||||
latest: true,
|
||||
}
|
||||
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||
if (Object.keys(config.value).length === 0) {
|
||||
config.value = default_config
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function setDashboardConfig() {
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
v-if="config.storage"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -18,6 +64,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.mediaStatistic"
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
@@ -25,6 +72,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.weeklyOverview"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -32,6 +80,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.speed"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -39,6 +88,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.scheduler"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -46,6 +96,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.cpu"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
@@ -53,10 +104,75 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.memory"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AnalyticsMemory />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.library"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLibrary />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.playing"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerPlaying />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.latest"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLatest />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 底部操作按钮 -->
|
||||
<span class="fixed right-5 bottom-5">
|
||||
<VBtn icon="mdi-view-dashboard-edit" class="me-2" color="primary" size="x-large" @click="dialog = true" />
|
||||
</span>
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog
|
||||
v-model="dialog"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="设置仪表板">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="(item, key) in dashboard_names"
|
||||
:key="key"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VCheckbox
|
||||
v-model="config[key]"
|
||||
:label="dashboard_names[key]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="setDashboardConfig"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</vdialog>
|
||||
</template>
|
||||
|
||||
@@ -77,8 +77,8 @@ function login() {
|
||||
store.dispatch('auth/updateUserName', username)
|
||||
store.dispatch('auth/updateAvatar', avatar)
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(store.state.auth.originalPath)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 登录失败,显示错误提示
|
||||
|
||||
@@ -8,6 +8,7 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -20,6 +21,11 @@ const tabs = [
|
||||
icon: 'mdi-account',
|
||||
tab: 'account',
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
icon: 'mdi-cog',
|
||||
tab: 'system',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
@@ -83,6 +89,13 @@ const tabs = [
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 系统 -->
|
||||
<VWindowItem value="system">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSystem />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
elevation: 0,
|
||||
},
|
||||
VList: {
|
||||
activeColor: 'primary',
|
||||
color: 'primary',
|
||||
},
|
||||
VPagination: {
|
||||
activeColor: 'primary',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import store from '@/store'
|
||||
|
||||
@@ -7,7 +7,7 @@ configureNProgress()
|
||||
|
||||
// Router
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
||||
if (to.meta.keepAlive && savedPosition)
|
||||
@@ -162,6 +162,7 @@ router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = store.state.auth.token !== null
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
store.state.auth.originalPath = to.fullPath
|
||||
next('/login')
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -7,6 +7,7 @@ interface AuthState {
|
||||
superUser: boolean
|
||||
userName: string
|
||||
avatar: string
|
||||
originalPath: string | null
|
||||
}
|
||||
|
||||
// 定义根状态类型
|
||||
@@ -23,6 +24,7 @@ const authModule: Module<AuthState, RootState> = {
|
||||
superUser: false,
|
||||
userName: '',
|
||||
avatar: '',
|
||||
originalPath: null,
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token: string) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dom'
|
||||
48
src/views/dashboard/MediaServerLatest.vue
Normal file
48
src/views/dashboard/MediaServerLatest.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||
|
||||
// 最近入库列表
|
||||
const latestList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLatest() {
|
||||
try {
|
||||
latestList.value = await api.get('mediaserver/latest')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLatest()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>最近添加</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="latestList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PosterCard
|
||||
v-for="data in latestList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||
|
||||
// 媒体库列表
|
||||
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLibrary() {
|
||||
try {
|
||||
libraryList.value = await api.get('mediaserver/library')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLibrary()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>我的媒体库</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="libraryList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<LibraryCard
|
||||
v-for="data in libraryList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||
|
||||
// 继续播放列表
|
||||
const playingList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadPlayingList() {
|
||||
try {
|
||||
playingList.value = await api.get('mediaserver/playing')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPlayingList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>继续观看</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="playingList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<BackdropCard
|
||||
v-for="data in playingList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -25,8 +25,8 @@ const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 本地是否存在
|
||||
const isExists = ref(false)
|
||||
// 本地是否存在,存在则包括Item信息
|
||||
const existsItemId = ref('')
|
||||
|
||||
// 是否已订阅
|
||||
const isSubscribed = ref(false)
|
||||
@@ -46,6 +46,11 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||
// 订阅编号
|
||||
const subscribeId = ref<number>()
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 调用API查询详情
|
||||
async function getMediaDetail() {
|
||||
if (mediaProps.mediaid && mediaProps.type) {
|
||||
@@ -59,9 +64,8 @@ async function getMediaDetail() {
|
||||
return
|
||||
|
||||
// 检查存在状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
checkMovieExists()
|
||||
else
|
||||
checkExists()
|
||||
if (mediaDetail.value.type === '电视剧')
|
||||
checkSeasonsNotExists()
|
||||
// 检查订阅状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
@@ -86,9 +90,9 @@ async function loadSeasonEpisodes(season: number) {
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已入库
|
||||
async function checkMovieExists() {
|
||||
async function checkExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: mediaDetail.value.tmdb_id,
|
||||
title: mediaDetail.value.title,
|
||||
@@ -99,7 +103,7 @@ async function checkMovieExists() {
|
||||
})
|
||||
|
||||
if (result.success)
|
||||
isExists.value = true
|
||||
existsItemId.value = result.data.item.id
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -133,11 +137,8 @@ async function checkSeasonsNotExists() {
|
||||
if (mediaDetail.value.type !== '电视剧')
|
||||
return
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', mediaDetail.value)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||
if (result) {
|
||||
if (result.length === 0)
|
||||
isExists.value = true
|
||||
|
||||
result.forEach((item) => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
@@ -145,8 +146,6 @@ async function checkSeasonsNotExists() {
|
||||
state = 2
|
||||
else if (item.episodes.length < item.total_episode)
|
||||
state = 1
|
||||
if (state !== 2)
|
||||
isExists.value = true
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
@@ -188,7 +187,7 @@ async function addSubscribe(season = 0) {
|
||||
startNProgress()
|
||||
try {
|
||||
// 是否洗版
|
||||
let best_version = isExists.value ? 1 : 0
|
||||
let best_version = existsItemId.value ? 1 : 0
|
||||
if (season)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
@@ -221,7 +220,7 @@ async function addSubscribe(season = 0) {
|
||||
)
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
if (result.success && subscribeRules.value.show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
@@ -283,6 +282,20 @@ async function removeSubscribe(season: number) {
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function querySubscribeRules() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
subscribeRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅按钮响应
|
||||
function handleSubscribe(season = 0) {
|
||||
if (isSubscribed.value)
|
||||
@@ -403,8 +416,29 @@ function handleSearch(area: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转播放页面
|
||||
async function handlePlay() {
|
||||
// 获取播放链接地址
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`mediaserver/play/${existsItemId.value}`,
|
||||
)
|
||||
if (result?.success) {
|
||||
// 打开链接地址
|
||||
setTimeout(() => {
|
||||
window.open(result.data.url, '_blank')
|
||||
}, 100)
|
||||
}
|
||||
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
getMediaDetail()
|
||||
querySubscribeRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -438,7 +472,7 @@ onBeforeMount(() => {
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="media-title">
|
||||
<div v-if="isExists" class="media-status">
|
||||
<div v-if="existsItemId" class="media-status">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||
</span>
|
||||
@@ -458,7 +492,7 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
@@ -484,12 +518,18 @@ onBeforeMount(() => {
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
{{ isSubscribed ? '已订阅' : '订阅' }}
|
||||
</VBtn>
|
||||
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-play" />
|
||||
</template>
|
||||
在线播放
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-overview">
|
||||
@@ -504,7 +544,9 @@ onBeforeMount(() => {
|
||||
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
|
||||
<li v-for="director in mediaDetail.directors" :key="director.id">
|
||||
<span>{{ director.job }}</span>
|
||||
<a class="crew-name" :href="`person?personid=${director.id}`" target="_blank">{{ director.name }}</a>
|
||||
<RouterLink :to="`/person?personid=${director.id}`" class="crew-name" target="_blank">
|
||||
{{ director.name }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="!mediaDetail.tmdb_id && mediaDetail.douban_id" class="media-crew">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
interface SearchTorrent extends Context {
|
||||
more?: Array<Context>
|
||||
@@ -48,7 +48,7 @@ const editionFilterOptions = ref<Array<string>>([])
|
||||
const resolutionFilterOptions = ref<Array<string>>([])
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<SearchTorrent>>([])
|
||||
const dataList = ref<Array<SearchTorrent>>([])
|
||||
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = ref<Map<string, Context[]>>()
|
||||
@@ -69,7 +69,7 @@ function initOptions(data: Context) {
|
||||
}
|
||||
|
||||
// 计算分组后的列表
|
||||
watchEffect(() => {
|
||||
onMounted(() => {
|
||||
// 数据分组
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
// 遍历数据
|
||||
@@ -92,10 +92,12 @@ watchEffect(() => {
|
||||
groupedDataList.value = groupMap
|
||||
})
|
||||
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -126,10 +128,12 @@ watchEffect(() => {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
|
||||
dataList.value.push(firstData)
|
||||
}
|
||||
}
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -216,12 +220,9 @@ watchEffect(() => {
|
||||
</VRow>
|
||||
</VCard>
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
:more="item.more"
|
||||
/>
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentCard v-if="defer(index)" :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
@@ -27,7 +28,7 @@ const filterForm = reactive({
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<Context>>([])
|
||||
const dataList = ref<Array<Context>>([])
|
||||
|
||||
// 获取站点过滤选项
|
||||
const siteFilterOptions = ref<Array<string>>([])
|
||||
@@ -59,10 +60,12 @@ function initOptions(data: Context) {
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -72,21 +75,22 @@ watchEffect(() => {
|
||||
if (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
dataList.value.push(data)
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
|
||||
// 初始化过滤选项
|
||||
@@ -100,25 +104,18 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VList
|
||||
lines="three"
|
||||
class="rounded"
|
||||
>
|
||||
<TorrentItem
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
/>
|
||||
<VListItem v-if="dataList.length === 0">
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded">
|
||||
<VListItem>
|
||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<div>
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentItem v-if="defer(index)" :torrent="item" />
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol
|
||||
xl="2"
|
||||
md="3"
|
||||
class="d-none d-md-block"
|
||||
>
|
||||
<VCol xl="2" md="3" class="d-none d-md-block">
|
||||
<VList lines="one" class="rounded">
|
||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||
站点
|
||||
|
||||
@@ -5,25 +5,21 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
|
||||
import PluginCard from '@/components/cards/PluginCard.vue'
|
||||
|
||||
// 数据列表
|
||||
// 已安装插件列表
|
||||
const dataList = ref<Plugin[]>([])
|
||||
|
||||
// 未安装插件列表
|
||||
const uninstalledList = ref<Plugin[]>([])
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// APP市场是否加载完成
|
||||
const isAppMarketLoaded = ref(false)
|
||||
|
||||
// APP市场窗口
|
||||
const PluginAppDialog = ref(false)
|
||||
|
||||
// 获取已安装的插件列表
|
||||
const getInstalledPluginList = computed(() => {
|
||||
return dataList.value.filter(item => item.installed)
|
||||
})
|
||||
|
||||
// 获取未安装或者有更新的插件列表
|
||||
const getUninstalledPluginList = computed(() => {
|
||||
return dataList.value.filter(item => !item.installed || item.has_update)
|
||||
})
|
||||
|
||||
// 关闭插件市场窗口
|
||||
function pluginDialogClose() {
|
||||
PluginAppDialog.value = false
|
||||
@@ -31,14 +27,19 @@ function pluginDialogClose() {
|
||||
|
||||
// 新安装了插件
|
||||
function pluginInstalled() {
|
||||
fetchData()
|
||||
fetchInstalledPlugins()
|
||||
pluginDialogClose()
|
||||
fetchUninstalledPlugins()
|
||||
}
|
||||
|
||||
// 获取插件列表数据
|
||||
async function fetchData() {
|
||||
async function fetchInstalledPlugins() {
|
||||
try {
|
||||
dataList.value = await api.get('plugin/')
|
||||
dataList.value = await api.get('plugin/', {
|
||||
params: {
|
||||
state: 'installed',
|
||||
},
|
||||
})
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
@@ -46,8 +47,26 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未安装插件列表数据
|
||||
async function fetchUninstalledPlugins() {
|
||||
try {
|
||||
uninstalledList.value = await api.get('plugin/', {
|
||||
params: {
|
||||
state: 'market',
|
||||
},
|
||||
})
|
||||
isAppMarketLoaded.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
onBeforeMount(() => {
|
||||
fetchInstalledPlugins()
|
||||
fetchUninstalledPlugins()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,19 +82,19 @@ onBeforeMount(fetchData)
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="getInstalledPluginList.length > 0"
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-4 grid-plugin-card"
|
||||
>
|
||||
<PluginCard
|
||||
v-for="data in getInstalledPluginList"
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:plugin="data"
|
||||
@remove="fetchData"
|
||||
@save="fetchData"
|
||||
@remove="fetchInstalledPlugins"
|
||||
@save="fetchInstalledPlugins"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="getInstalledPluginList.length === 0 && isRefreshed"
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有安装插件"
|
||||
error-description="点击右下角按钮,前往插件市场安装插件。"
|
||||
@@ -86,6 +105,7 @@ onBeforeMount(fetchData)
|
||||
fullscreen
|
||||
scrollable
|
||||
:scrim="false"
|
||||
:z-index="1010"
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
@@ -121,16 +141,27 @@ onBeforeMount(fetchData)
|
||||
</VToolbar>
|
||||
</div>
|
||||
<VCardText>
|
||||
<div class="grid gap-4 grid-plugin-card">
|
||||
<div
|
||||
v-if="!isAppMarketLoaded"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
v-if="!isAppMarketLoaded"
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||
<PluginAppCard
|
||||
v-for="data in getUninstalledPluginList"
|
||||
v-for="data in uninstalledList"
|
||||
:key="data.id"
|
||||
:plugin="data"
|
||||
@install="pluginInstalled"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="getUninstalledPluginList.length === 0 && isRefreshed"
|
||||
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
|
||||
error-code="404"
|
||||
error-title="没有未安装插件"
|
||||
error-description="所有可用插件均已安装。"
|
||||
|
||||
@@ -6,10 +6,6 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
@@ -42,6 +38,9 @@ function onRefresh() {
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser)
|
||||
return dataList.value
|
||||
else
|
||||
|
||||
@@ -3,42 +3,79 @@ import api from '@/api'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
|
||||
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
|
||||
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
|
||||
download: { url: '/filebrowser/download?path={path}', method: 'get' },
|
||||
image: { url: '/filebrowser/image?path={path}', method: 'get' },
|
||||
rename: { url: '/filebrowser/rename?path={path}&new_name={newname}', method: 'get' },
|
||||
list: {
|
||||
url: '/filebrowser/list?path={path}&sort={sort}',
|
||||
method: 'get',
|
||||
},
|
||||
mkdir: {
|
||||
url: '/filebrowser/mkdir?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
delete: {
|
||||
url: '/filebrowser/delete?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
download: {
|
||||
url: '/filebrowser/download?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
image: {
|
||||
url: '/filebrowser/image?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
rename: {
|
||||
url: '/filebrowser/rename?path={path}&new_name={newname}',
|
||||
method: 'get',
|
||||
},
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
const path = ref('/')
|
||||
const path: Ref<string | undefined> = ref()
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
async function loadSystemSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success)
|
||||
path.value = result.data?.DOWNLOAD_PATH || '/'
|
||||
if (path.value && !path.value.endsWith('/'))
|
||||
path.value += '/'
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
function loadSystemSettings(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api
|
||||
.get('system/env')
|
||||
.then((result: any) => {
|
||||
let path = '/'
|
||||
if (result.success)
|
||||
path = result.data?.DOWNLOAD_PATH || '/'
|
||||
|
||||
if (!path.endsWith('/'))
|
||||
path += '/'
|
||||
|
||||
resolve(path)
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadSystemSettings()
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
.then((res) => {
|
||||
path.value = res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
path.value = '/'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FileBrowser storages="local" :tree="false" :path="path" :endpoints="endpoints" :axios="api" @pathchanged="pathChanged" />
|
||||
<FileBrowser
|
||||
storages="local"
|
||||
:tree="false"
|
||||
:path="path"
|
||||
:endpoints="endpoints"
|
||||
:axios="api"
|
||||
@pathchanged="pathChanged"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,20 +25,46 @@ const selected = ref<TransferHistory[]>([])
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
{ title: '目录', key: 'src', sortable: false },
|
||||
{ title: '转移方式', key: 'mode', sortable: false },
|
||||
{ title: '时间', key: 'date', sortable: false },
|
||||
{ title: '状态', key: 'status', sortable: false },
|
||||
{ title: '失败原因', key: 'errmsg', sortable: false },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '目录',
|
||||
key: 'src',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '转移方式',
|
||||
key: 'mode',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'date',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TransferHistory[]>([])
|
||||
|
||||
// 搜索
|
||||
const search = ref('')
|
||||
const search = ref()
|
||||
|
||||
// 搜索提示词列表
|
||||
const searchHintList = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
@@ -68,13 +94,7 @@ const deleteConfirmDialog = ref(false)
|
||||
const confirmTitle = ref('')
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData({
|
||||
page,
|
||||
itemsPerPage,
|
||||
}: {
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
}) {
|
||||
async function fetchData({ page, itemsPerPage }: { page: number; itemsPerPage: number }) {
|
||||
loading.value = true
|
||||
try {
|
||||
currentPage.value = page
|
||||
@@ -89,6 +109,9 @@ async function fetchData({
|
||||
|
||||
dataList.value = result.data.list
|
||||
totalItems.value = result.data.total
|
||||
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
|
||||
title => title !== '',
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -106,11 +129,6 @@ function getIcon(type: string) {
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算颜色
|
||||
function getStatusColor(status: boolean) {
|
||||
return status ? 'success' : 'error'
|
||||
}
|
||||
|
||||
// 转移方式字典
|
||||
const TransferDict: { [key: string]: string } = {
|
||||
copy: '复制',
|
||||
@@ -132,7 +150,9 @@ async function removeHistory(item: TransferHistory) {
|
||||
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
|
||||
try {
|
||||
// 调用删除API
|
||||
const result: { [key: string]: any } = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
||||
const result: {
|
||||
[key: string]: any
|
||||
} = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
||||
data: item,
|
||||
})
|
||||
|
||||
@@ -150,6 +170,7 @@ async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
|
||||
deleteConfirmDialog.value = false
|
||||
if (!currentHistory.value)
|
||||
return
|
||||
|
||||
// 删除
|
||||
await remove(currentHistory.value, deleteSrc, deleteDest)
|
||||
// 刷新
|
||||
@@ -167,6 +188,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
||||
const total = selected.value.length
|
||||
if (total === 0)
|
||||
return
|
||||
|
||||
// 已处理条数
|
||||
let handled = 0
|
||||
// 显示进度条
|
||||
@@ -178,7 +200,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
||||
await remove(item, deleteSrc, deleteDest)
|
||||
// 删除完成
|
||||
handled++
|
||||
progressValue.value = handled / total * 100
|
||||
progressValue.value = (handled / total) * 100
|
||||
}
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
@@ -203,6 +225,7 @@ async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
|
||||
async function removeHistoryBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
|
||||
@@ -210,19 +233,47 @@ async function removeHistoryBatch() {
|
||||
deleteConfirmDialog.value = true
|
||||
}
|
||||
|
||||
// 计算根路径
|
||||
function getRootPath(path: string, type: string, category: string) {
|
||||
if (!path)
|
||||
return ''
|
||||
|
||||
let index = -2
|
||||
if (type !== '电影')
|
||||
index = -3
|
||||
|
||||
if (category)
|
||||
index -= 1
|
||||
|
||||
if (path.includes('/'))
|
||||
return path.split('/').slice(0, index).join('/')
|
||||
else
|
||||
return path.split('\\').slice(0, index).join('\\')
|
||||
}
|
||||
|
||||
// 批量重新整理
|
||||
async function retransferBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1)
|
||||
redoTarget.value = selected.value[0].dest ?? ''
|
||||
else
|
||||
if (selected.value.length === 1) {
|
||||
// 目的目录
|
||||
const dest = selected.value[0].dest ?? ''
|
||||
// 类型
|
||||
const mediaType = selected.value[0].type ?? ''
|
||||
// 分类
|
||||
const category = selected.value[0].category ?? ''
|
||||
// 计算根路径
|
||||
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||
}
|
||||
else {
|
||||
redoTarget.value = ''
|
||||
}
|
||||
// 打开识别弹窗
|
||||
redoDialog.value = true
|
||||
}
|
||||
@@ -236,7 +287,7 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = item.dest ?? ''
|
||||
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -260,20 +311,24 @@ const dropdownItems = ref([
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VRow>
|
||||
<VCol> 历史记录 </VCol>
|
||||
<VCol>
|
||||
<VTextField
|
||||
<VCol cols="4" md="6">
|
||||
历史记录
|
||||
</VCol>
|
||||
<VCol cols="8" md="6">
|
||||
<VCombobox
|
||||
key="search_navbar"
|
||||
v-model="search"
|
||||
:items="searchHintList"
|
||||
class="text-disabled"
|
||||
density="compact"
|
||||
label="搜索"
|
||||
label="搜索标题、状态"
|
||||
append-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
single-line
|
||||
hide-details
|
||||
flat
|
||||
rounded
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -297,58 +352,52 @@ const dropdownItems = ref([
|
||||
@update:options="fetchData"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex">
|
||||
<VAvatar><VIcon :icon="getIcon(item.raw.type || '')" /></VAvatar>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar>
|
||||
<VIcon :icon="getIcon(item.value.type || '')" />
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column ms-1">
|
||||
<span class="d-block whitespace-nowrap text-high-emphasis">
|
||||
{{ item.raw.title }} {{ item.raw.seasons }}{{ item.raw.episodes }}
|
||||
<span class="d-block text-high-emphasis">
|
||||
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }}
|
||||
</span>
|
||||
<small>{{ item.raw.category }}</small>
|
||||
<small>{{ item.value.category }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.src="{ item }">
|
||||
<small>{{ item.raw.src }} <br>=> {{ item.raw.dest }}</small>
|
||||
<small>{{ item.value.src }} <br>=> {{ item.value.dest }}</small>
|
||||
</template>
|
||||
<template #item.mode="{ item }">
|
||||
<VChip
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
TransferDict[item.raw.mode]
|
||||
}}
|
||||
<VChip variant="outlined" color="primary" size="small">
|
||||
{{ TransferDict[item.value.mode] }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="getStatusColor(item.raw.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.raw.status ? "成功" : "失败" }}
|
||||
<VChip v-if="item.value.status" color="success" size="small">
|
||||
成功
|
||||
</VChip>
|
||||
<v-tooltip v-else :text="item.value.errmsg">
|
||||
<template #activator="{ props }">
|
||||
<VChip v-bind="props" color="error" size="small">
|
||||
失败
|
||||
</VChip>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #item.date="{ item }">
|
||||
<small>{{ item.raw.date }}</small>
|
||||
</template>
|
||||
<template #item.errmsg="{ item }">
|
||||
<small class="text-error">{{ item.raw.errmsg }}</small>
|
||||
<small>{{ item.value.date }}</small>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item.raw)"
|
||||
@click="menu.props.click(item.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
@@ -365,23 +414,17 @@ const dropdownItems = ref([
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
<!-- 底部操作按钮 -->
|
||||
<span
|
||||
v-if="selected.length > 0"
|
||||
class="fixed right-5 bottom-5"
|
||||
>
|
||||
<VBtn
|
||||
icon="mdi-redo-variant"
|
||||
class="me-2"
|
||||
color="primary"
|
||||
size="x-large"
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
size="x-large"
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
||||
<VTooltip text="批量重新整理">
|
||||
<template #activator="{ props }">
|
||||
<VBtn v-bind="props" icon="mdi-redo-variant" class="me-2" color="primary" size="x-large" @click="retransferBatch" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="批量删除">
|
||||
<template #activator="{ props }">
|
||||
<VBtn v-bind="props" icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</span>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
@@ -390,33 +433,17 @@ const dropdownItems = ref([
|
||||
<VCardTitle class="pe-10">
|
||||
{{ confirmTitle }}
|
||||
</VCardTitle>
|
||||
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
||||
<VBtn
|
||||
color="primary"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(false, false)"
|
||||
>
|
||||
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
||||
<VBtn color="primary" class="mb-2 mx-2" @click="deleteConfirmHandler(false, false)">
|
||||
仅删除历史记录
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="warning"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(true, false)"
|
||||
>
|
||||
<VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)">
|
||||
删除历史记录和源文件
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="info"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(false, true)"
|
||||
>
|
||||
<VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)">
|
||||
删除历史记录和媒体库文件
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(true, true)"
|
||||
>
|
||||
<VBtn color="error" class="mb-2 mx-2" @click="deleteConfirmHandler(true, true)">
|
||||
删除历史记录、源文件和媒体库文件
|
||||
</VBtn>
|
||||
</div>
|
||||
@@ -427,17 +454,19 @@ const dropdownItems = ref([
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage,
|
||||
itemsPerPage,
|
||||
})
|
||||
}"
|
||||
@done="
|
||||
() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage,
|
||||
itemsPerPage,
|
||||
})
|
||||
}
|
||||
"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,52 @@ import type { NotificationSwitch } from '@/api/types'
|
||||
|
||||
const messagemTypes = ref<NotificationSwitch[]>([])
|
||||
|
||||
// 选中的消息渠道
|
||||
const selectedChannels = ref([])
|
||||
|
||||
// 消息渠道标签页
|
||||
const messagerTab = ref('wechat')
|
||||
|
||||
// 消息设置
|
||||
const notificationSettings = ref({
|
||||
WECHAT_CORPID: '',
|
||||
WECHAT_APP_SECRET: '',
|
||||
WECHAT_APP_ID: '',
|
||||
WECHAT_PROXY: '',
|
||||
WECHAT_TOKEN: '',
|
||||
WECHAT_ENCODING_AESKEY: '',
|
||||
WECHAT_ADMINS: '',
|
||||
TELEGRAM_TOKEN: '',
|
||||
TELEGRAM_CHAT_ID: '',
|
||||
TELEGRAM_USERS: '',
|
||||
TELEGRAM_ADMINS: '',
|
||||
SLACK_OAUTH_TOKEN: '',
|
||||
SLACK_APP_TOKEN: '',
|
||||
SLACK_CHANNEL: '',
|
||||
SYNOLOGYCHAT_WEBHOOK: '',
|
||||
SYNOLOGYCHAT_TOKEN: '',
|
||||
})
|
||||
|
||||
// 消息渠道
|
||||
const NotificationChannels = [
|
||||
{
|
||||
title: '微信',
|
||||
value: 'wechat',
|
||||
},
|
||||
{
|
||||
title: 'Telegram',
|
||||
value: 'telegram',
|
||||
},
|
||||
{
|
||||
title: 'Slack',
|
||||
value: 'slack',
|
||||
},
|
||||
{
|
||||
title: 'SynologyChat',
|
||||
value: 'synologychat',
|
||||
},
|
||||
]
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -32,87 +78,365 @@ async function saveNotificationSwitchs() {
|
||||
$toast.success('保存通知消息设置成功')
|
||||
else
|
||||
$toast.error('保存通知消息设置失败!')
|
||||
|
||||
// messagemTypes.value = messagemTypes.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询消息渠道设置
|
||||
async function loadNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MESSAGER')
|
||||
if (result1.success)
|
||||
selectedChannels.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
const {
|
||||
WECHAT_CORPID,
|
||||
WECHAT_APP_SECRET,
|
||||
WECHAT_APP_ID,
|
||||
WECHAT_PROXY,
|
||||
WECHAT_TOKEN,
|
||||
WECHAT_ENCODING_AESKEY,
|
||||
WECHAT_ADMINS,
|
||||
TELEGRAM_TOKEN,
|
||||
TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_USERS,
|
||||
TELEGRAM_ADMINS,
|
||||
SLACK_OAUTH_TOKEN,
|
||||
SLACK_APP_TOKEN,
|
||||
SLACK_CHANNEL,
|
||||
SYNOLOGYCHAT_WEBHOOK,
|
||||
SYNOLOGYCHAT_TOKEN,
|
||||
} = result2.data
|
||||
notificationSettings.value = {
|
||||
WECHAT_CORPID,
|
||||
WECHAT_APP_SECRET,
|
||||
WECHAT_APP_ID,
|
||||
WECHAT_PROXY,
|
||||
WECHAT_TOKEN,
|
||||
WECHAT_ENCODING_AESKEY,
|
||||
WECHAT_ADMINS,
|
||||
TELEGRAM_TOKEN,
|
||||
TELEGRAM_CHAT_ID,
|
||||
TELEGRAM_USERS,
|
||||
TELEGRAM_ADMINS,
|
||||
SLACK_OAUTH_TOKEN,
|
||||
SLACK_APP_TOKEN,
|
||||
SLACK_CHANNEL,
|
||||
SYNOLOGYCHAT_WEBHOOK,
|
||||
SYNOLOGYCHAT_TOKEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存消息渠道设置
|
||||
async function saveNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/MESSAGER',
|
||||
selectedChannels.value.join(','),
|
||||
)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
notificationSettings.value,
|
||||
)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存通知渠道设置成功')
|
||||
reloadModule()
|
||||
}
|
||||
else { $toast.error('保存通知渠道设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API接口重新加载模块
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadNotificationSwitchs()
|
||||
loadNotificationSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="消息通知">
|
||||
<VCardText> 对应消息类型只会发送给选中的消息渠道。 </VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="通知渠道">
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedChannels"
|
||||
multiple
|
||||
chips
|
||||
:items="NotificationChannels"
|
||||
label="当前使用通知渠道"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="messagerTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="wechat">
|
||||
微信
|
||||
</VTab>
|
||||
<VTab value="telegram">
|
||||
Telegram
|
||||
</VTab>
|
||||
<VTab value="slack">
|
||||
Slack
|
||||
</VTab>
|
||||
<VTab value="synologychat">
|
||||
SynologyChat
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="messagerTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem value="wechat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_CORPID"
|
||||
label="企业ID"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_SECRET"
|
||||
label="应用密钥"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_ID"
|
||||
label="应用ID"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_PROXY"
|
||||
label="代理地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_TOKEN"
|
||||
label="Token"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
|
||||
label="EncodingAESKey"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="telegram">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_TOKEN"
|
||||
label="Bot Token"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_CHAT_ID"
|
||||
label="Chat ID"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_USERS"
|
||||
label="用户白名单"
|
||||
placeholder="多个用,分隔"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="slack">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="5">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_OAUTH_TOKEN"
|
||||
label="Slack Bot User OAuth Token"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="5">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_APP_TOKEN"
|
||||
label="Slack App-Level Token"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="2">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SLACK_CHANNEL"
|
||||
label="频道名称"
|
||||
placeholder="全体"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="synologychat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
|
||||
label="Webhook"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
|
||||
label="Token"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSettings"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="消息类型">
|
||||
<VCardSubtitle> 对应消息类型只会发送给选中的消息渠道。 </VCardSubtitle>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
消息类型
|
||||
</th>
|
||||
<th scope="col">
|
||||
微信
|
||||
</th>
|
||||
<th scope="col">
|
||||
Telegram
|
||||
</th>
|
||||
<th scope="col">
|
||||
Slack
|
||||
</th>
|
||||
<th scope="col">
|
||||
SynologyChat
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="message in messagemTypes"
|
||||
:key="message.mtype"
|
||||
>
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.wechat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.telegram" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.slack" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.synologychat" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td
|
||||
colspan="5"
|
||||
class="text-center"
|
||||
>
|
||||
没有设置任何通知渠道
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<VDivider />
|
||||
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
消息类型
|
||||
</th>
|
||||
<th scope="col">
|
||||
微信
|
||||
</th>
|
||||
<th scope="col">
|
||||
Telegram
|
||||
</th>
|
||||
<th scope="col">
|
||||
Slack
|
||||
</th>
|
||||
<th scope="col">
|
||||
SynologyChat
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="message in messagemTypes"
|
||||
:key="message.mtype"
|
||||
>
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.wechat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.telegram" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.slack" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.synologychat" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td
|
||||
colspan="4"
|
||||
class="text-center"
|
||||
>
|
||||
没有设置任何通知渠道
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSwitchs"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSwitchs"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -30,6 +32,12 @@ const defaultFilterRules = ref({
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
@@ -227,6 +235,49 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules() {
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
@@ -264,6 +315,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importCodeDialog = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -332,6 +413,17 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -78,11 +78,14 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<VCard title="定时作业">
|
||||
<VCardText> 手动执行不会影响作业正常的时间表。 </VCardText>
|
||||
<VCardSubtitle> 包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。 </VCardSubtitle>
|
||||
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
提供者
|
||||
</th>
|
||||
<th scope="col">
|
||||
任务名称
|
||||
</th>
|
||||
@@ -100,6 +103,9 @@ onUnmounted(() => {
|
||||
v-for="scheduler in schedulerList"
|
||||
:key="scheduler.id"
|
||||
>
|
||||
<td>
|
||||
{{ scheduler.provider }}
|
||||
</td>
|
||||
<td>
|
||||
{{ scheduler.name }}
|
||||
</td>
|
||||
|
||||
@@ -17,12 +17,32 @@ const resetSitesDisabled = ref(false)
|
||||
// 种子优先规则
|
||||
const selectedTorrentPriority = ref<string>('seeder')
|
||||
|
||||
// CookieCloud设置项
|
||||
const cookieCloudSetting = ref({
|
||||
COOKIECLOUD_HOST: '',
|
||||
COOKIECLOUD_KEY: '',
|
||||
COOKIECLOUD_PASSWORD: '',
|
||||
COOKIECLOUD_INTERVAL: 0,
|
||||
USER_AGENT: '',
|
||||
})
|
||||
|
||||
// 种子优先规则下拉框
|
||||
const TorrentPriorityItems = [
|
||||
{ title: '站点优先', value: 'site' },
|
||||
{ title: '做种数优先', value: 'seeder' },
|
||||
]
|
||||
|
||||
// 同步间隔下拉框
|
||||
const CookieCloudIntervalItems = [
|
||||
{ title: '每小时', value: 60 },
|
||||
{ title: '每6小时', value: 360 },
|
||||
{ title: '每12小时', value: 720 },
|
||||
{ title: '每天', value: 1440 },
|
||||
{ title: '每周', value: 10080 },
|
||||
{ title: '每月', value: 43200 },
|
||||
{ title: '永不', value: 0 },
|
||||
]
|
||||
|
||||
// 重置站点
|
||||
async function resetSites() {
|
||||
try {
|
||||
@@ -77,13 +97,111 @@ async function saveTorrentPriority() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载CookieCloud设置
|
||||
async function loadCookieCloudSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const {
|
||||
COOKIECLOUD_HOST,
|
||||
COOKIECLOUD_KEY,
|
||||
COOKIECLOUD_PASSWORD,
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
} = result.data
|
||||
cookieCloudSetting.value = {
|
||||
COOKIECLOUD_HOST,
|
||||
COOKIECLOUD_KEY,
|
||||
COOKIECLOUD_PASSWORD,
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存CookieCloud设置
|
||||
async function saveCookieCloudetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
cookieCloudSetting.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存站点同步设置成功')
|
||||
else
|
||||
$toast.error('保存站点同步设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
loadCookieCloudSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="站点同步">
|
||||
<VCardSubtitle> 从CookieCloud快速同步站点数据。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_HOST"
|
||||
label="CookieCloud服务器地址"
|
||||
placeholder="https://movie-pilot.org/cookiecloud"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_KEY"
|
||||
label="用户KEY"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD"
|
||||
type="password"
|
||||
label="端对端加密密码"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL"
|
||||
label="自动同步间隔"
|
||||
:items="CookieCloudIntervalItems"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.USER_AGENT"
|
||||
label="浏览器User-Agent"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCookieCloudetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载优先规则">
|
||||
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||
@@ -94,8 +212,7 @@ onMounted(() => {
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
label="当前使用下载优先规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang='ts' setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -27,12 +29,50 @@ const allSites = ref<Site[]>([])
|
||||
// 选中订阅站点
|
||||
const selectedRssSites = ref<number[]>([])
|
||||
|
||||
// 当前规则类型
|
||||
const currentRuleType = ref('SubscribeFilterRules')
|
||||
|
||||
// 是否开启订阅定时搜索
|
||||
const enableIntervalSearch = ref(false)
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
movie_size: '',
|
||||
tv_size: '',
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 订阅模式选择项
|
||||
const subscribeModeItems = [
|
||||
{ title: '自动', value: 'spider' },
|
||||
{ title: '站点RSS', value: 'rss' },
|
||||
]
|
||||
|
||||
// 选择的订阅模式
|
||||
const selectedSubscribeMode = ref('spider')
|
||||
|
||||
// RSS运行周期选择项
|
||||
const rssIntervalItems = [
|
||||
{ title: '5分钟', value: 5 },
|
||||
{ title: '10分钟', value: 10 },
|
||||
{ title: '20分钟', value: 20 },
|
||||
{ title: '半小时', value: 30 },
|
||||
{ title: '1小时', value: 60 },
|
||||
{ title: '12小时', value: 720 },
|
||||
{ title: '1天', value: 1440 },
|
||||
]
|
||||
|
||||
// 选择的RSS运行周期
|
||||
const selectedRssInterval = ref<number>(5)
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
async function querySelectedRssSites() {
|
||||
try {
|
||||
@@ -48,9 +88,26 @@ async function querySelectedRssSites() {
|
||||
// 保存用户选中的订阅站点
|
||||
async function saveSelectedRssSites() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/RssSites', selectedRssSites.value)
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/RssSites',
|
||||
selectedRssSites.value)
|
||||
|
||||
if (result.success)
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_SEARCH',
|
||||
enableIntervalSearch.value ? 'True' : 'False',
|
||||
)
|
||||
|
||||
const result3: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_MODE',
|
||||
selectedSubscribeMode.value,
|
||||
)
|
||||
|
||||
const result4: { [key: string]: any } = await api.post(
|
||||
'system/setting/SUBSCRIBE_RSS_INTERVAL',
|
||||
selectedRssInterval.value,
|
||||
)
|
||||
|
||||
if (result1.success && result2.success && result3.success && result4.success)
|
||||
$toast.success('订阅站点保存成功')
|
||||
else
|
||||
$toast.error('订阅站点保存失败!')
|
||||
@@ -68,6 +125,19 @@ async function querySites() {
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
allSites.value = data.filter(item => item.is_active)
|
||||
querySelectedRssSites()
|
||||
|
||||
// 查询订阅搜索开关
|
||||
const result: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_SEARCH')
|
||||
if (result.success)
|
||||
enableIntervalSearch.value = result.data?.value
|
||||
// 查询订阅模式
|
||||
const result2: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_MODE')
|
||||
if (result2.success)
|
||||
selectedSubscribeMode.value = result2.data?.value
|
||||
// 查询站点RSS周期
|
||||
const result3: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_RSS_INTERVAL')
|
||||
if (result3.success)
|
||||
selectedRssInterval.value = result3.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
@@ -244,6 +314,66 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules(ruleType: string) {
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (ruleType === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
async function importRules(ruleType: string) {
|
||||
currentRuleType.value = ruleType
|
||||
importCodeString.value = ''
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
if (!currentRuleType.value)
|
||||
return
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (currentRuleType.value === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
@@ -272,7 +402,34 @@ onMounted(() => {
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedSubscribeMode"
|
||||
:items="subscribeModeItems"
|
||||
label="订阅模式"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedRssInterval"
|
||||
:items="rssIntervalItems"
|
||||
label="站点RSS周期"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="enableIntervalSearch"
|
||||
label="开启订阅定时搜索"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites">
|
||||
保存
|
||||
@@ -282,7 +439,37 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅优先级">
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。 </VCardSubtitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
<FilterRuleCard
|
||||
@@ -318,7 +505,37 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="洗版优先级">
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。 </VCardSubtitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。</VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
<FilterRuleCard
|
||||
@@ -354,7 +571,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -372,6 +589,28 @@ onMounted(() => {
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.movie_size"
|
||||
type="text"
|
||||
label="电影文件大小(GB)"
|
||||
placeholder="0-30"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.tv_size"
|
||||
type="text"
|
||||
label="剧集单集文件大小(GB)"
|
||||
placeholder="0-10"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="defaultFilterRules.show_edit_dialog"
|
||||
label="订阅时编辑更多规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -386,9 +625,20 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang='scss'>
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
|
||||
737
src/views/setting/AccountSettingSystem.vue
Normal file
737
src/views/setting/AccountSettingSystem.vue
Normal file
@@ -0,0 +1,737 @@
|
||||
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
|
||||
// 选中的媒体服务器
|
||||
const selectedMediaServers = ref([])
|
||||
|
||||
// 下载器选中标签页
|
||||
const downloaderTab = ref('qbittorrent')
|
||||
|
||||
// 媒体服务器选中标签页
|
||||
const mediaserverTab = ref('emby')
|
||||
|
||||
// 媒体库设置项
|
||||
const mediaSettings = ref({
|
||||
SCRAP_METADATA: true,
|
||||
DOWNLOAD_PATH: '',
|
||||
DOWNLOAD_MOVIE_PATH: '',
|
||||
DOWNLOAD_TV_PATH: '',
|
||||
DOWNLOAD_ANIME_PATH: '',
|
||||
DOWNLOAD_CATEGORY: false,
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
LIBRARY_PATH: '',
|
||||
LIBRARY_MOVIE_NAME: '',
|
||||
LIBRARY_TV_NAME: '',
|
||||
LIBRARY_ANIME_NAME: '',
|
||||
LIBRARY_CATEGORY: false,
|
||||
})
|
||||
|
||||
// 下载器设置项
|
||||
const downloaderSettings = ref({
|
||||
DOWNLOADER: '',
|
||||
DOWNLOADER_MONITOR: true,
|
||||
TORRENT_TAG: '',
|
||||
QB_HOST: '',
|
||||
QB_USER: '',
|
||||
QB_PASSWORD: '',
|
||||
QB_CATEGORY: false,
|
||||
QB_SEQUENTIAL: false,
|
||||
QB_FORCE_RESUME: false,
|
||||
TR_HOST: '',
|
||||
TR_USER: '',
|
||||
TR_PASSWORD: '',
|
||||
})
|
||||
|
||||
// 媒体服务器设置项
|
||||
const mediaServerSettings = ref({
|
||||
MEDIASERVER_SYNC_INTERVAL: 6,
|
||||
MEDIASERVER_SYNC_BLACKLIST: '',
|
||||
EMBY_HOST: '',
|
||||
EMBY_PLAY_HOST: '',
|
||||
EMBY_API_KEY: '',
|
||||
JELLYFIN_HOST: '',
|
||||
JELLYFIN_PLAY_HOST: '',
|
||||
JELLYFIN_API_KEY: '',
|
||||
PLEX_HOST: '',
|
||||
PLEX_PLAY_HOST: '',
|
||||
PLEX_TOKEN: '',
|
||||
})
|
||||
|
||||
// 下载器字典项
|
||||
const Downloaders = [
|
||||
{
|
||||
title: 'Qbittorrent',
|
||||
value: 'qbittorrent',
|
||||
},
|
||||
{
|
||||
title: 'Transmission',
|
||||
value: 'transmission',
|
||||
},
|
||||
]
|
||||
|
||||
// 媒体服务器字典项
|
||||
const MediaServers = [
|
||||
{
|
||||
title: 'Emby',
|
||||
value: 'emby',
|
||||
},
|
||||
{
|
||||
title: 'Jellyfin',
|
||||
value: 'jellyfin',
|
||||
},
|
||||
{
|
||||
title: 'Plex',
|
||||
value: 'plex',
|
||||
},
|
||||
]
|
||||
|
||||
// 转移方式字典
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'rclone移动', value: 'rclone_move' },
|
||||
]
|
||||
|
||||
// 覆盖模式字典
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '按大小覆盖', value: 'size' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '仅保留最新版本', value: 'latest' },
|
||||
]
|
||||
|
||||
// 媒体库同步周期字典
|
||||
const syncIntervalItems = [
|
||||
{ title: '从不', value: 0 },
|
||||
{ title: '每小时', value: 1 },
|
||||
{ title: '每6小时', value: 6 },
|
||||
{ title: '每12小时', value: 12 },
|
||||
{ title: '每天', value: 24 },
|
||||
{ title: '每周', value: 168 },
|
||||
]
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 加载媒体库设置
|
||||
async function loadMediaSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
} = result.data
|
||||
mediaSettings.value = {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体设置
|
||||
async function saveMediaSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaSettings.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存媒体库设置成功')
|
||||
else
|
||||
$toast.error('保存媒体库设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownladerSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const {
|
||||
DOWNLOADER,
|
||||
DOWNLOADER_MONITOR,
|
||||
TORRENT_TAG,
|
||||
QB_HOST,
|
||||
QB_USER,
|
||||
QB_PASSWORD,
|
||||
QB_CATEGORY,
|
||||
QB_SEQUENTIAL,
|
||||
QB_FORCE_RESUME,
|
||||
TR_HOST,
|
||||
TR_USER,
|
||||
TR_PASSWORD,
|
||||
} = result.data
|
||||
downloaderSettings.value = {
|
||||
DOWNLOADER,
|
||||
DOWNLOADER_MONITOR,
|
||||
TORRENT_TAG,
|
||||
QB_HOST,
|
||||
QB_USER,
|
||||
QB_PASSWORD,
|
||||
QB_CATEGORY,
|
||||
QB_SEQUENTIAL,
|
||||
QB_FORCE_RESUME,
|
||||
TR_HOST,
|
||||
TR_USER,
|
||||
TR_PASSWORD,
|
||||
}
|
||||
downloaderTab.value = DOWNLOADER === 'qbittorrent' ? 'qbittorrent' : 'transmission'
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存下载器设置
|
||||
async function saveDownloaderSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
downloaderSettings.value,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
$toast.success('保存下载器设置成功')
|
||||
reloadModule()
|
||||
}
|
||||
else { $toast.error('保存下载器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询媒体服务器设置
|
||||
async function loadMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
|
||||
if (result1.success)
|
||||
selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
const {
|
||||
MEDIASERVER_SYNC_INTERVAL,
|
||||
MEDIASERVER_SYNC_BLACKLIST,
|
||||
EMBY_HOST,
|
||||
EMBY_PLAY_HOST,
|
||||
EMBY_API_KEY,
|
||||
JELLYFIN_HOST,
|
||||
JELLYFIN_PLAY_HOST,
|
||||
JELLYFIN_API_KEY,
|
||||
PLEX_HOST,
|
||||
PLEX_PLAY_HOST,
|
||||
PLEX_TOKEN,
|
||||
} = result2.data
|
||||
mediaServerSettings.value = {
|
||||
MEDIASERVER_SYNC_INTERVAL,
|
||||
MEDIASERVER_SYNC_BLACKLIST,
|
||||
EMBY_HOST,
|
||||
EMBY_PLAY_HOST,
|
||||
EMBY_API_KEY,
|
||||
JELLYFIN_HOST,
|
||||
JELLYFIN_PLAY_HOST,
|
||||
JELLYFIN_API_KEY,
|
||||
PLEX_HOST,
|
||||
PLEX_PLAY_HOST,
|
||||
PLEX_TOKEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体服务器设置
|
||||
async function saveMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/MEDIASERVER',
|
||||
selectedMediaServers.value.join(','),
|
||||
)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaServerSettings.value,
|
||||
)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存媒体服务器设置成功')
|
||||
reloadModule()
|
||||
}
|
||||
else { $toast.error('保存媒体服务器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API接口重新加载模块
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDownladerSetting()
|
||||
loadMediaServerSetting()
|
||||
loadMediaSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载器">
|
||||
<VCardSubtitle>只有选中的下载器才会被默认使用。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="downloaderSettings.DOWNLOADER"
|
||||
:items="Downloaders"
|
||||
label="当前使用下载器"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TORRENT_TAG"
|
||||
label="下载器种子标签"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.DOWNLOADER_MONITOR"
|
||||
label="监控下载器"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="downloaderTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="qbittorrent">
|
||||
Qbittorrent
|
||||
</VTab>
|
||||
<VTab value="transmission">
|
||||
Transmission
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="downloaderTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem value="qbittorrent">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_CATEGORY"
|
||||
label="自动分类管理"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_SEQUENTIAL"
|
||||
label="顺序下载"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_FORCE_RESUME"
|
||||
label="强制继续"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="transmission">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveDownloaderSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体服务器">
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="selectedMediaServers"
|
||||
multiple
|
||||
chips
|
||||
:items="MediaServers"
|
||||
label="当前使用媒体服务器"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
|
||||
:items="syncIntervalItems"
|
||||
label="同步周期"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
|
||||
label="媒体库同步黑名单"
|
||||
placeholder="使用,分隔"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="mediaserverTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="emby">
|
||||
Emby
|
||||
</VTab>
|
||||
<VTab value="jellyfin">
|
||||
Jellyfin
|
||||
</VTab>
|
||||
<VTab value="plex">
|
||||
Plex
|
||||
</vtab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="mediaserverTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindowItem value="emby">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_API_KEY"
|
||||
label="API密钥"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="jellyfin">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_API_KEY"
|
||||
label="API密钥"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="plex">
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_TOKEN"
|
||||
label="API密钥"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveMediaServerSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体库">
|
||||
<VCardSubtitle>设置下载目录、媒体库目录以及整理方式。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_PATH"
|
||||
label="下载目录"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_MOVIE_PATH"
|
||||
label="电影下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_TV_PATH"
|
||||
label="电视剧下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_ANIME_PATH"
|
||||
label="动漫下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.DOWNLOAD_CATEGORY"
|
||||
label="下载目录自动分类"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.TRANSFER_TYPE"
|
||||
:items="transferTypeItems"
|
||||
label="整理方式"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.SCRAP_METADATA"
|
||||
label="自动刮削媒体信息"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_PATH"
|
||||
label="媒体库目录"
|
||||
placeholder="多个目录使用,分隔"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_MOVIE_NAME"
|
||||
label="电影目录名称"
|
||||
placeholder="电影"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_TV_NAME"
|
||||
label="电视剧目录名称"
|
||||
placeholder="电视剧"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_ANIME_NAME"
|
||||
label="动漫目录名称"
|
||||
placeholder="动漫"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.LIBRARY_CATEGORY"
|
||||
label="媒体库目录自动分类"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveMediaSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -173,7 +173,7 @@ onMounted(() => {
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
title="支持的配置格式:"
|
||||
title="支持的配置格式(注意空格):"
|
||||
>
|
||||
<span
|
||||
v-html="`
|
||||
|
||||
@@ -68,6 +68,7 @@ onBeforeMount(fetchData)
|
||||
@click="siteAddDialog = true"
|
||||
/>
|
||||
<SiteAddEditForm
|
||||
v-if="siteAddDialog"
|
||||
v-model="siteAddDialog"
|
||||
oper="add"
|
||||
@save="siteAddDialog = false; fetchData()"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang='ts' setup>
|
||||
import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import type { Ref } from 'vue'
|
||||
import type { MediaInfo, Rss, Subscribe, TmdbEpisode } from '@/api/types'
|
||||
import type { MediaInfo, Subscribe, TmdbEpisode } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { parseDate } from '@/@core/utils/formatters'
|
||||
import { formatEp, parseDate } from '@/@core/utils/formatters'
|
||||
|
||||
// 日历属性
|
||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||
@@ -33,7 +33,7 @@ const calendarOptions: Ref<CalendarOptions> = ref({
|
||||
events: [],
|
||||
})
|
||||
|
||||
async function eventsHander(subscribe: Subscribe | Rss) {
|
||||
async function eventsHander(subscribe: Subscribe) {
|
||||
// 如果是电影直接返回
|
||||
if (subscribe.type === '电影') {
|
||||
// 调用API查询TMDB详情
|
||||
@@ -48,24 +48,52 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
||||
allDay: false,
|
||||
posterPath: subscribe.poster,
|
||||
mediaType: subscribe.type,
|
||||
len: 1,
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 调用API查询集信息
|
||||
const episodes: TmdbEpisode[] = await api.get(
|
||||
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
|
||||
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
|
||||
)
|
||||
|
||||
return episodes.map((episode) => {
|
||||
return {
|
||||
title: subscribe.name,
|
||||
subtitle: `第 ${episode.episode_number} 集`,
|
||||
start: parseDate(episode.air_date || ''),
|
||||
allDay: false,
|
||||
posterPath: subscribe.poster,
|
||||
mediaType: subscribe.type,
|
||||
interface EpisodeInfo {
|
||||
title: string
|
||||
subtitle: string
|
||||
start: Date | null
|
||||
allDay: boolean
|
||||
posterPath: string | undefined
|
||||
mediaType: string
|
||||
len: number
|
||||
}
|
||||
|
||||
interface EpisodesDictionary {
|
||||
[key: string]: EpisodeInfo
|
||||
}
|
||||
|
||||
const dictEpisode: EpisodesDictionary = {}
|
||||
episodes.forEach((episode: TmdbEpisode) => {
|
||||
const air_date = episode.air_date ?? ''
|
||||
if (dictEpisode[air_date]) {
|
||||
dictEpisode[air_date].subtitle += `,${episode.episode_number}`
|
||||
dictEpisode[air_date].len++
|
||||
}
|
||||
else {
|
||||
dictEpisode[air_date] = {
|
||||
title: subscribe.name,
|
||||
subtitle: `${episode.episode_number}`,
|
||||
start: parseDate(episode.air_date || ''),
|
||||
allDay: false,
|
||||
posterPath: subscribe.poster,
|
||||
mediaType: subscribe.type,
|
||||
len: 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
for (const key in dictEpisode)
|
||||
dictEpisode[key].subtitle = formatEp(dictEpisode[key].subtitle.split(',').map(Number))
|
||||
|
||||
return Object.values(dictEpisode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,11 +143,11 @@ onMounted(() => {
|
||||
</VImg>
|
||||
</div>
|
||||
<div>
|
||||
<VCardSubtitle class="pa-2 font-bold break-words whitespace-break-spaces">
|
||||
<VCardSubtitle class="pa-1 px-2 font-bold break-words whitespace-break-spaces">
|
||||
{{ arg.event.title }}
|
||||
</VCardSubtitle>
|
||||
<VCardText class="pa-0 px-2">
|
||||
{{ arg.event.extendedProps.subtitle }}
|
||||
<VCardText v-if="arg.event.extendedProps.subtitle" class="pa-0 px-2 break-words">
|
||||
第{{ arg.event.extendedProps.subtitle }}集
|
||||
</VCardText>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,6 +170,15 @@ onMounted(() => {
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
<VChip
|
||||
v-if="arg.event.extendedProps.len > 1"
|
||||
variant="elevated"
|
||||
color="primary"
|
||||
size="x-small"
|
||||
class="absolute right-0 top-0"
|
||||
>
|
||||
{{ arg.event.extendedProps.len }}
|
||||
</VChip>
|
||||
</VImg>
|
||||
</template>
|
||||
</VTooltip>
|
||||
@@ -150,7 +187,7 @@ onMounted(() => {
|
||||
</FullCalendar>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang='scss'>
|
||||
.v-application .fc {
|
||||
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||
--fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
@@ -253,10 +290,10 @@ onMounted(() => {
|
||||
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary,
|
||||
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary:hover,
|
||||
.v-application
|
||||
.fc
|
||||
.fc-toolbar-chunk
|
||||
.fc-button-group
|
||||
.fc-button-primary:not(.disabled):active {
|
||||
.fc
|
||||
.fc-toolbar-chunk
|
||||
.fc-button-group
|
||||
.fc-button-primary:not(.disabled):active {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
@@ -281,19 +318,18 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.v-application
|
||||
.fc
|
||||
.fc-toolbar-chunk:last-child
|
||||
.fc-button-group
|
||||
.fc-button:not(:last-child) {
|
||||
border-inline-end: 0.0625rem solid
|
||||
rgba(var(--v-theme-primary), var(--v-overlay-scrim-opacity));
|
||||
.fc
|
||||
.fc-toolbar-chunk:last-child
|
||||
.fc-button-group
|
||||
.fc-button:not(:last-child) {
|
||||
border-inline-end: 0.0625rem solid rgba(var(--v-theme-primary), var(--v-overlay-scrim-opacity));
|
||||
}
|
||||
|
||||
.v-application
|
||||
.fc
|
||||
.fc-toolbar-chunk:last-child
|
||||
.fc-button-group
|
||||
.fc-button.fc-button-active {
|
||||
.fc
|
||||
.fc-toolbar-chunk:last-child
|
||||
.fc-button-group
|
||||
.fc-button.fc-button-active {
|
||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
@@ -359,8 +395,8 @@ onMounted(() => {
|
||||
.v-application .fc .fc-popover {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity),
|
||||
0 4px 8px -4px var(--v-shadow-key-penumbra-opacity),
|
||||
0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
|
||||
0 4px 8px -4px var(--v-shadow-key-penumbra-opacity),
|
||||
0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
|
||||
}
|
||||
|
||||
.v-application .fc .fc-popover .fc-popover-header,
|
||||
@@ -400,11 +436,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.v-theme--dark
|
||||
.v-application
|
||||
.fc
|
||||
.fc-toolbar-chunk
|
||||
.fc-button-group
|
||||
.fc-drawerToggler-button {
|
||||
.v-application
|
||||
.fc
|
||||
.fc-toolbar-chunk
|
||||
.fc-button-group
|
||||
.fc-drawerToggler-button {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
@@ -47,6 +43,9 @@ function onRefresh() {
|
||||
|
||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||
const filteredDataList = computed(() => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser)
|
||||
return dataList.value.filter(data => data.type === props.type)
|
||||
else
|
||||
|
||||
@@ -6,6 +6,7 @@ import slack from '@images/logos/slack.png'
|
||||
import telegram from '@images/logos/telegram.webp'
|
||||
import tmdb from '@images/logos/tmdb.png'
|
||||
import wechat from '@images/logos/wechat.png'
|
||||
import fanart from '@images/logos/fanart.webp'
|
||||
|
||||
interface Status {
|
||||
OK: string
|
||||
@@ -57,6 +58,16 @@ const targets = ref<Address[]>([
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: fanart,
|
||||
name: 'webservice.fanart.tv',
|
||||
url: 'https://webservice.fanart.tv',
|
||||
proxy: true,
|
||||
status: 'Normal',
|
||||
time: '',
|
||||
message: '未测试',
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: telegram,
|
||||
name: 'api.telegram.org',
|
||||
|
||||
@@ -9,6 +9,7 @@ import vuetify from 'vite-plugin-vuetify'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
@@ -27,7 +28,16 @@ export default defineConfig({
|
||||
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'vuex'],
|
||||
vueTemplate: true,
|
||||
}),
|
||||
VitePWA({ registerType: 'autoUpdate', injectRegister: 'script', manifest: false }),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'script',
|
||||
manifest: false,
|
||||
workbox: {
|
||||
navigateFallbackDenylist: [
|
||||
/.*\/api\/v\d+\/system\/logging.*/,
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
|
||||
376
yarn.lock
376
yarn.lock
@@ -1703,6 +1703,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
|
||||
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
|
||||
|
||||
"@lokesh.dhakar/quantize@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@lokesh.dhakar/quantize/-/quantize-1.3.0.tgz#04476889953aca94614fbc79e9a43adc7979179a"
|
||||
integrity sha512-4KBSyaMj65d8A+2vnzLxtHFu4OmBU4IKO0yLxZ171Itdf9jGV4w+WbG7VsKts2jUdRkFSzsZqpZOz6hTB3qGAw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -2417,6 +2422,11 @@ accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
ace-builds@^1.32.6:
|
||||
version "1.32.6"
|
||||
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.32.6.tgz#454ec8bc9235fbb960b8d8b86e698f941c104de2"
|
||||
integrity sha512-dO5BnyDOhCnznhOpILzXq4jqkbhRXxNkf3BuVTmyxGyRLrhddfdyk6xXgy+7A8LENrcYoFi/sIxMuH3qjNUN4w==
|
||||
|
||||
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -2432,7 +2442,7 @@ acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.2:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||
|
||||
ajv@^6.10.0, ajv@^6.12.4:
|
||||
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
@@ -2560,6 +2570,18 @@ arrify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
|
||||
dependencies:
|
||||
safer-buffer "~2.1.0"
|
||||
|
||||
assert-plus@1.0.0, assert-plus@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
|
||||
|
||||
ast-walker-scope@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/ast-walker-scope/-/ast-walker-scope-0.4.1.tgz#81ae35fc86f357689dae5a4721d743831e3c240e"
|
||||
@@ -2605,6 +2627,16 @@ available-typed-arrays@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
||||
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
|
||||
|
||||
aws4@^1.8.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
||||
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
||||
|
||||
axios-mock-adapter@^1.21.4:
|
||||
version "1.21.4"
|
||||
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.21.4.tgz#ced09b54b245b338422e3af425ae529bfa26e051"
|
||||
@@ -2656,6 +2688,13 @@ balanced-match@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
@@ -2805,6 +2844,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464, can
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz"
|
||||
integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
|
||||
|
||||
chalk@^2.0.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
@@ -2930,7 +2974,15 @@ colord@^2.9.1, colord@^2.9.3:
|
||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
colorthief@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/colorthief/-/colorthief-2.4.0.tgz#74e6edd142695655bd5f52c7f8116b125ea2b2bd"
|
||||
integrity sha512-0U48RGNRo5fVO+yusBwgp+d3augWSorXabnqXUu9SabEhCpCgZJEUjUTTI41OOBBYuMMxawa3177POT6qLfLeQ==
|
||||
dependencies:
|
||||
"@lokesh.dhakar/quantize" "^1.3.0"
|
||||
get-pixels "^3.3.2"
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
@@ -3011,6 +3063,11 @@ core-js-compat@^3.30.1, core-js-compat@^3.30.2:
|
||||
dependencies:
|
||||
browserslist "^4.21.5"
|
||||
|
||||
core-util-is@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
|
||||
|
||||
cosmiconfig@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
|
||||
@@ -3170,6 +3227,25 @@ csstype@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
cwise-compiler@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5"
|
||||
integrity sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==
|
||||
dependencies:
|
||||
uniq "^1.0.0"
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
data-uri-to-buffer@0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a"
|
||||
integrity sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
@@ -3357,6 +3433,14 @@ domutils@^3.0.1:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
|
||||
dependencies:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
@@ -4051,6 +4135,11 @@ express@^4.18.2:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
extract-comments@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/extract-comments/-/extract-comments-1.1.0.tgz#b90bca033a056bd69b8ba1c6b6b120fc2ee95c18"
|
||||
@@ -4070,6 +4159,16 @@ extract-zip@^2.0.1:
|
||||
optionalDependencies:
|
||||
"@types/yauzl" "^2.9.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
|
||||
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@@ -4204,6 +4303,11 @@ for-each@^0.3.3:
|
||||
dependencies:
|
||||
is-callable "^1.1.3"
|
||||
|
||||
forever-agent@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
|
||||
@@ -4222,6 +4326,15 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
@@ -4303,6 +4416,23 @@ get-own-enumerable-property-symbols@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
|
||||
|
||||
get-pixels@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/get-pixels/-/get-pixels-3.3.3.tgz#71e2dfd4befb810b5478a61c6354800976ce01c7"
|
||||
integrity sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg==
|
||||
dependencies:
|
||||
data-uri-to-buffer "0.0.3"
|
||||
jpeg-js "^0.4.1"
|
||||
mime-types "^2.0.1"
|
||||
ndarray "^1.0.13"
|
||||
ndarray-pack "^1.1.1"
|
||||
node-bitmap "0.0.1"
|
||||
omggif "^1.0.5"
|
||||
parse-data-uri "^0.2.0"
|
||||
pngjs "^3.3.3"
|
||||
request "^2.44.0"
|
||||
through "^2.3.4"
|
||||
|
||||
get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||
@@ -4328,6 +4458,13 @@ get-tsconfig@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.5.0.tgz#6d52d1c7b299bd3ee9cd7638561653399ac77b0f"
|
||||
integrity sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
@@ -4467,6 +4604,19 @@ grapheme-splitter@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
|
||||
|
||||
har-validator@~5.1.3:
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
|
||||
integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
|
||||
dependencies:
|
||||
ajv "^6.12.3"
|
||||
har-schema "^2.0.0"
|
||||
|
||||
hard-rejection@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
|
||||
@@ -4561,6 +4711,15 @@ http-errors@2.0.0:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
human-signals@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||
@@ -4638,6 +4797,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
iota-array@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087"
|
||||
integrity sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
@@ -4700,6 +4864,11 @@ is-boolean-object@^1.1.0:
|
||||
call-bind "^1.0.2"
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-buffer@^1.0.2:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
||||
is-buffer@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
||||
@@ -4865,6 +5034,11 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9:
|
||||
gopd "^1.0.1"
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
|
||||
|
||||
is-weakmap@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||
@@ -4902,6 +5076,11 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
|
||||
|
||||
jake@^10.8.5:
|
||||
version "10.8.7"
|
||||
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
|
||||
@@ -4926,6 +5105,11 @@ jiti@^1.18.2:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
|
||||
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
|
||||
|
||||
jpeg-js@^0.4.1:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
|
||||
@@ -4948,6 +5132,11 @@ js-yaml@^4.1.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
|
||||
|
||||
jsesc@^2.5.1:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
||||
@@ -4978,7 +5167,7 @@ json-schema-traverse@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-schema@^0.4.0:
|
||||
json-schema@0.4.0, json-schema@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
@@ -4988,6 +5177,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
|
||||
|
||||
json5@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
@@ -5040,6 +5234,16 @@ jsonpointer@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
|
||||
integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
|
||||
dependencies:
|
||||
assert-plus "1.0.0"
|
||||
extsprintf "1.3.0"
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
jwt-decode@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
|
||||
@@ -5297,7 +5501,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
mime-types@^2.0.1, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
@@ -5445,11 +5649,32 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
ndarray-pack@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ndarray-pack/-/ndarray-pack-1.2.1.tgz#8caebeaaa24d5ecf70ff86020637977da8ee585a"
|
||||
integrity sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==
|
||||
dependencies:
|
||||
cwise-compiler "^1.1.2"
|
||||
ndarray "^1.0.13"
|
||||
|
||||
ndarray@^1.0.13:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/ndarray/-/ndarray-1.0.19.tgz#6785b5f5dfa58b83e31ae5b2a058cfd1ab3f694e"
|
||||
integrity sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==
|
||||
dependencies:
|
||||
iota-array "^1.0.0"
|
||||
is-buffer "^1.0.2"
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
node-bitmap@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091"
|
||||
integrity sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==
|
||||
|
||||
node-fetch@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
||||
@@ -5521,6 +5746,11 @@ nth-check@^2.0.1:
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
@@ -5577,6 +5807,11 @@ object.values@^1.1.6:
|
||||
define-properties "^1.1.4"
|
||||
es-abstract "^1.20.4"
|
||||
|
||||
omggif@^1.0.5:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
|
||||
integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==
|
||||
|
||||
on-finished@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
@@ -5676,6 +5911,13 @@ parse-code-context@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/parse-code-context/-/parse-code-context-1.0.0.tgz#718c295c593d0d19a37f898473268cc75e98de1e"
|
||||
integrity sha512-OZQaqKaQnR21iqhlnPfVisFjBWjhnMl5J9MgbP8xC+EwoVqbXrq78lp+9Zb3ahmLzrIX5Us/qbvBnaS3hkH6OA==
|
||||
|
||||
parse-data-uri@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-data-uri/-/parse-data-uri-0.2.0.tgz#bf04d851dd5c87b0ab238e5d01ace494b604b4c9"
|
||||
integrity sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==
|
||||
dependencies:
|
||||
data-uri-to-buffer "0.0.3"
|
||||
|
||||
parse-entities@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
|
||||
@@ -5763,6 +6005,11 @@ perfect-scrollbar@^1.5.5:
|
||||
resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz#41a211a2fb52a7191eff301432134ea47052b27f"
|
||||
integrity sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
@@ -5804,6 +6051,11 @@ pluralize@^8.0.0:
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||
|
||||
pngjs@^3.3.3:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
|
||||
|
||||
postcss-calc@^8.2.3:
|
||||
version "8.2.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5"
|
||||
@@ -6171,6 +6423,11 @@ proxy-from-env@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
psl@^1.1.28:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
|
||||
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
|
||||
|
||||
pull-refresh-vue3@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pull-refresh-vue3/-/pull-refresh-vue3-0.3.1.tgz#e75ffad5d71e30a85b5338f2beca9fc8a1e01432"
|
||||
@@ -6189,6 +6446,11 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
||||
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
|
||||
|
||||
punycode@^2.1.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
purgecss@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-5.0.0.tgz#08526ba3fef95e42c54503ca59d3f2ee8d6e5189"
|
||||
@@ -6206,6 +6468,11 @@ qs@6.11.0:
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
@@ -6358,11 +6625,42 @@ regjsparser@^0.9.1:
|
||||
dependencies:
|
||||
jsesc "~0.5.0"
|
||||
|
||||
request@^2.44.0:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.8.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.6"
|
||||
extend "~3.0.2"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.2"
|
||||
har-validator "~5.1.3"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.19"
|
||||
oauth-sign "~0.9.0"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.2"
|
||||
safe-buffer "^5.1.2"
|
||||
tough-cookie "~2.5.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
@@ -6430,7 +6728,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@5.2.1, safe-buffer@^5.1.0:
|
||||
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@@ -6451,7 +6749,7 @@ safe-regex@^2.1.1:
|
||||
dependencies:
|
||||
regexp-tree "~0.1.1"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -6642,6 +6940,21 @@ spdx-license-ids@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5"
|
||||
integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
|
||||
integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
bcrypt-pbkdf "^1.0.0"
|
||||
dashdash "^1.12.0"
|
||||
ecc-jsbn "~0.1.1"
|
||||
getpass "^0.1.1"
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
stable@^0.1.8:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
@@ -7109,6 +7422,11 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
through@^2.3.4:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||
|
||||
tiny-glob@^0.2.9:
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2"
|
||||
@@ -7134,6 +7452,14 @@ toidentifier@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
|
||||
dependencies:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
|
||||
@@ -7183,6 +7509,18 @@ tsutils@^3.21.0:
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
@@ -7304,6 +7642,11 @@ unimport@^3.0.6:
|
||||
strip-literal "^1.0.1"
|
||||
unplugin "^1.3.1"
|
||||
|
||||
uniq@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
|
||||
integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==
|
||||
|
||||
unique-string@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
|
||||
@@ -7411,6 +7754,11 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
v8-compile-cache@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
@@ -7429,6 +7777,15 @@ vary@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
verror@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||
integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vite-plugin-pages@^0.29.0:
|
||||
version "0.29.0"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-pages/-/vite-plugin-pages-0.29.0.tgz#8a7352cbbbc463fd2a725d67e221af8de8992c2f"
|
||||
@@ -7565,6 +7922,13 @@ vue-tsc@^1.6.5:
|
||||
"@volar/vue-typescript" "1.6.5"
|
||||
semver "^7.3.8"
|
||||
|
||||
vue3-ace-editor@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vue3-ace-editor/-/vue3-ace-editor-2.2.4.tgz#1f2a787f91cf7979f27fab29e0e0604bb3ee1c17"
|
||||
integrity sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==
|
||||
dependencies:
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
vue3-apexcharts@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
|
||||
|
||||
Reference in New Issue
Block a user