Compare commits

...

123 Commits

Author SHA1 Message Date
jxxghp
225df7b1e6 插件支持最近更新排序 2024-04-30 08:35:29 +08:00
jxxghp
97ede69609 fix cols 2024-04-30 07:17:01 +08:00
jxxghp
c5ded86d8a fix:站点信息维护增加属性 2024-04-29 20:26:56 +08:00
jxxghp
b4f049ecda fix searchbar size 2024-04-29 15:12:15 +08:00
jxxghp
56692eb6cb feat 搜索框记忆 2024-04-29 14:51:00 +08:00
jxxghp
7c22c60190 plugins defer render 2024-04-29 13:45:21 +08:00
jxxghp
2f2c4d4a44 fix 2024-04-29 13:37:20 +08:00
jxxghp
c1c71916db fix dialog 2024-04-29 13:31:27 +08:00
jxxghp
4b15a7454c fix ui 2024-04-29 12:47:52 +08:00
jxxghp
22e723587d 调整插件页面UI 2024-04-29 12:30:21 +08:00
jxxghp
969adaf5bb Merge pull request #122 from hotlcc/develop-20240429-页面优化
升级vuetify-use-dialog至v0.6.11,解决弹出Confirm后点击overlay关闭后无法再次弹出Confirm的问题
2024-04-29 11:30:55 +08:00
Allen
c2214e8300 升级vuetify-use-dialog至v0.6.11,解决弹出Confirm后点击overlay关闭后无法再次弹出Confirm的问题 2024-04-29 11:28:37 +08:00
jxxghp
10af659227 fix hint 2024-04-29 10:51:34 +08:00
jxxghp
5cd3757f4f rollback 2024-04-29 09:55:25 +08:00
jxxghp
81f674ea01 test 2024-04-29 07:16:16 +08:00
jxxghp
1846ee0ffe fix ui 2024-04-28 21:45:16 +08:00
jxxghp
14a825093a 更新 package.json 2024-04-28 21:07:57 +08:00
jxxghp
d70f477bc1 fix theme 2024-04-28 18:36:10 +08:00
jxxghp
c9c897ffb5 Merge pull request #121 from hotlcc/develop-20240428-页面优化
主题配置保存到服务端,使各端主题一致
2024-04-28 18:10:28 +08:00
Allen
462dea3e05 解决媒体卡片右上角评分和入库标记冲突,hover的时候展示评分 2024-04-28 17:40:39 +08:00
Allen
4e7a0084dd 兼容处理不支持黑白配色的老旧设备 2024-04-28 16:40:47 +08:00
Allen
0268df0e24 主题配置保存到服务端,使各端主题一致 2024-04-28 16:18:12 +08:00
jxxghp
f926ca66c0 fix ui 2024-04-28 14:05:51 +08:00
jxxghp
16b5898928 fix ui 2024-04-28 13:48:16 +08:00
jxxghp
c1bb66cc9d fix 插件过滤 & 标签 2024-04-28 13:40:49 +08:00
jxxghp
f7502d0d18 Merge pull request #118 from thsrite/main 2024-04-28 10:26:46 +08:00
thsrite
b4975f649c fix 默认过滤规则支持最少做种人数生效发布时间,防止过滤掉到最新发布的种子 2024-04-28 10:21:46 +08:00
jxxghp
89353c1f7e feat:媒体搜索聚合开关设置 2024-04-28 08:56:00 +08:00
jxxghp
fce10b6dca media card icon 2024-04-27 18:16:05 +08:00
jxxghp
2cf95c6706 feat:发现内容聚合 2024-04-27 15:19:50 +08:00
jxxghp
58ab1599db fix person cards 2024-04-27 08:44:39 +08:00
jxxghp
9745c2ea1a fix hint 2024-04-27 08:18:58 +08:00
jxxghp
9db46e2949 fix ui 2024-04-27 08:16:36 +08:00
jxxghp
7949505104 fix ui 2024-04-26 23:58:06 +08:00
jxxghp
db0d5133e8 feat:人物搜索 2024-04-26 20:35:40 +08:00
jxxghp
54415377ee Merge pull request #117 from thsrite/main
fix 搜索默认规则支持最小做种数
2024-04-25 16:21:52 +08:00
thsrite
d7f55477da fix 搜索默认规则支持最小做种数 2024-04-25 16:19:50 +08:00
jxxghp
faca586fa7 Merge pull request #116 from hotlcc/develop-20240425-页面优化
修改“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示的问题
2024-04-25 16:09:16 +08:00
jxxghp
5f3ba7b9c7 fix bug 2024-04-25 16:06:47 +08:00
Allen
abace4a58d 修改“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示的问题 2024-04-25 15:43:30 +08:00
jxxghp
5895cea587 feat:进度条公共组件 2024-04-25 15:07:35 +08:00
jxxghp
cdbcef5232 release 2024-04-25 13:52:12 +08:00
jxxghp
d5d6bfdc56 feat:插件详情支持动态API调用 2024-04-25 13:30:25 +08:00
jxxghp
75ae7f0c15 Merge pull request #115 from hotlcc/develop-20240425-页面优化 2024-04-25 12:07:22 +08:00
Allen
6931451f18 媒体详情页面搜索资源按钮当下拉选项只有一个时不触发下拉框直接搜索 2024-04-25 11:25:52 +08:00
jxxghp
f5625e1354 fix bug 2024-04-25 10:26:20 +08:00
jxxghp
d1be4a30b6 feat:前端版本号显示 2024-04-25 10:19:09 +08:00
jxxghp
5c13362db2 fix fullscreen 2024-04-25 09:48:12 +08:00
jxxghp
6c71dce80c 历史记录可排序 2024-04-25 08:51:02 +08:00
jxxghp
790c397951 fix ui 2024-04-25 08:44:36 +08:00
jxxghp
e28e74b874 Merge pull request #113 from hotlcc/develop-20240424-1-插件搜索框宽度优化 2024-04-24 21:42:10 +08:00
Allen
b99ea22d89 插件搜索弹出框小屏下全屏 2024-04-24 21:28:15 +08:00
jxxghp
8938195c5d fix 2024-04-24 18:21:08 +08:00
jxxghp
887b4a7862 fix ui 2024-04-24 18:02:30 +08:00
jxxghp
7c9c39fa0e 更新 package.json 2024-04-24 17:41:07 +08:00
jxxghp
3b800753ec Merge pull request #112 from hotlcc/develop-20240424-VDialog优化 2024-04-24 17:40:28 +08:00
jxxghp
647119052c Merge pull request #111 from dh336699/hotfix-history-scofield 2024-04-24 17:38:07 +08:00
Allen
e9ce6bbd4e 消息中心弹窗小屏时全屏 2024-04-24 17:34:37 +08:00
Allen
1fee27f78e 系统健康检查弹窗小屏时全屏 2024-04-24 17:34:16 +08:00
Allen
e7a334861d 规则测试弹窗小屏时全屏 2024-04-24 17:33:07 +08:00
Allen
267ae3436d 实时日志弹窗小屏时全屏 2024-04-24 17:32:48 +08:00
hao.dai
60ff9f1891 fix: 1.登录双重认证增加防抖 2.历史记录搜索框增加防抖 3.开启项目vscode配置文件 2024-04-24 17:29:54 +08:00
Allen
f83efd23df 网络测试弹窗小屏下全屏 2024-04-24 17:29:24 +08:00
Allen
db60f02745 名称测试弹窗小屏下全屏 2024-04-24 17:28:30 +08:00
Allen
3e109bd27c dashborad配置弹窗小屏下全屏 2024-04-24 17:25:50 +08:00
Allen
c4ccf6e3fa 订阅编辑弹窗小屏时全屏 2024-04-24 17:16:27 +08:00
Allen
fb1a246e4a 订阅历史弹窗小屏时全屏 2024-04-24 17:15:20 +08:00
Allen
a418b03c06 文件整理弹窗小屏时全屏 2024-04-24 17:13:32 +08:00
Allen
e9fee000ca 插件数据页面小屏下全屏 2024-04-24 17:09:26 +08:00
Allen
71c13e0653 插件配置弹窗小屏下全屏 2024-04-24 17:08:46 +08:00
Allen
32d7f933f8 站点编辑弹窗小屏下全屏 2024-04-24 17:07:15 +08:00
Allen
f28dd810ce 站点资源弹窗小屏下全屏 2024-04-24 17:05:46 +08:00
Allen
aaedd88ca7 站点更新弹窗小屏下全屏展示 2024-04-24 17:04:04 +08:00
Allen
00dee40917 站点更新弹窗添加关闭按钮 2024-04-24 16:49:50 +08:00
hao.dai
019248b605 Merge remote-tracking branch 'upstream/main'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2024-04-24 15:43:56 +08:00
hao.dai
826f37bcc4 fix: 账号设置warning问题 2024-04-24 15:43:11 +08:00
jxxghp
fa02a23e4c 更新 package.json 2024-04-24 10:19:02 +08:00
jxxghp
7143fb6f67 Merge pull request #110 from hotlcc/develop-20240423-低版safari时间兼容 2024-04-24 10:18:39 +08:00
Allen
e1524c26cd 统一处理低版本safari浏览器Date兼容性问题 2024-04-24 10:06:32 +08:00
jxxghp
72088dff2e release v1.8.3 2024-04-23 17:48:35 +08:00
jxxghp
8e6d3cf30e fix dashboard config 2024-04-23 10:25:43 +08:00
jxxghp
144992ccec Merge pull request #104 from hotlcc/develop-20240417-用户配置
dashboard配置支持保存入库
2024-04-23 10:00:36 +08:00
jxxghp
673e883ae6 Merge branch 'main' into develop-20240417-用户配置 2024-04-23 10:00:29 +08:00
Allen
f197ed7972 优化dashboard配置功能 2024-04-23 09:52:11 +08:00
jxxghp
ce642aceed release v2 2024-04-22 10:04:23 +08:00
jxxghp
d5411489c0 fix ui 2024-04-22 10:02:57 +08:00
jxxghp
26c66627f8 Merge pull request #108 from thsrite/main 2024-04-20 19:52:12 +08:00
thsrite
c654986042 fix 2024-04-20 19:22:55 +08:00
thsrite
c5b5c15f99 fix 2024-04-20 19:18:16 +08:00
thsrite
7727b0f1c3 fix 2024-04-20 19:11:44 +08:00
thsrite
3d551ac45b fix 订阅时编辑规则移到默认订阅规则页面 2024-04-20 18:48:02 +08:00
jxxghp
555a00b731 fix postercard 2024-04-19 23:08:41 +08:00
jxxghp
9f9091b23e 更新 package.json 2024-04-19 22:45:42 +08:00
jxxghp
14c343142f Merge pull request #107 from falling/main 2024-04-19 22:45:00 +08:00
falling
890920775a fix 安卓手机端hover事件被VCard的click事件覆盖问题 2024-04-19 22:00:15 +08:00
jxxghp
7b38d2d74f fix #105 2024-04-19 19:51:14 +08:00
jxxghp
e85c2870e2 更新 SubscribeHistoryDialog.vue 2024-04-19 17:08:41 +08:00
jxxghp
cfbc5802e4 fix VInfiniteScroll 2024-04-19 13:56:57 +08:00
jxxghp
40cdb820fb fix ui 2024-04-19 13:16:13 +08:00
jxxghp
f63beb776e fix 订阅历史记录 2024-04-19 08:24:57 +08:00
jxxghp
20f031b2e2 rename components 2024-04-18 22:59:00 +08:00
jxxghp
b0f28b7e7c fix 2024-04-18 22:33:03 +08:00
jxxghp
62bb6de80d feat:订阅历史 2024-04-18 21:00:35 +08:00
Allen
3db4d883af fixbug 2024-04-18 15:19:24 +08:00
Allen
8cb514d70e dashboard配置支持保存入库 2024-04-18 12:40:14 +08:00
jxxghp
2d7880351b release 2024-04-18 11:14:03 +08:00
jxxghp
e1ee3ef2db fix #1918 2024-04-18 11:13:36 +08:00
jxxghp
aff30c48a0 fix site stat 2024-04-18 08:12:46 +08:00
jxxghp
55eea50a6e test release 2024-04-17 23:02:37 +08:00
jxxghp
9ff212c94d feat: 插件页面支持slot 2024-04-17 22:55:45 +08:00
jxxghp
6350c7e9e6 feat:插件支持渲染弹窗关闭按钮 2024-04-17 21:20:31 +08:00
jxxghp
d097c1c17c fix ui 2024-04-17 19:31:50 +08:00
jxxghp
b9ee6b4039 fix ui 2024-04-17 15:30:40 +08:00
jxxghp
f1238a03b3 fix 2024-04-17 14:51:05 +08:00
jxxghp
e90cf3ee77 test release 2024-04-17 14:41:22 +08:00
jxxghp
468607c8e8 feat:站点状态显示 2024-04-17 14:38:40 +08:00
jxxghp
5bd9283177 Merge pull request #102 from dh336699/feature-issue-94 2024-04-17 12:44:17 +08:00
hao.dai
117b12348c fix: 低版本Safari浏览器不能正确显示订阅的更新日期 2024-04-17 12:38:34 +08:00
jxxghp
0d325b6eb8 fix ui 2024-04-17 08:16:11 +08:00
jxxghp
86d5903f32 更新 TransferHistoryView.vue 2024-04-16 18:31:32 +08:00
jxxghp
3b518d6f33 release 2024-04-16 11:34:37 +08:00
jxxghp
78f57e7d4b Merge pull request #101 from dh336699/feature-optimization-ranking 2024-04-16 11:33:17 +08:00
hao.dai
f710f1bfc0 fix: 修复ranking页面大批量warning问题 2024-04-16 11:24:07 +08:00
74 changed files with 2712 additions and 3549 deletions

View File

@@ -39,6 +39,7 @@ jobs:
run: |
yarn
yarn build
echo "$frontend_version" > dist/version.txt
zip -r dist.zip dist
- name: Generate Release

View File

@@ -6,9 +6,6 @@
"[javascript]": {
"editor.formatOnSave": false
},
"[typescript]": {
"editor.formatOnSave": false
},
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
},
@@ -25,7 +22,7 @@
},
// Vue
"[vue]": {
"editor.formatOnSave": false
"editor.formatOnSave": true
},
// Extension: Volar
"volar.preview.port": 3000,
@@ -34,6 +31,10 @@
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
// Extension: Stylelint

2
components.d.ts vendored
View File

@@ -10,9 +10,11 @@ declare module 'vue' {
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.8.1-2",
"version": "1.8.5-1",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -51,11 +51,12 @@
"vue-prism-component": "^2.0.0",
"vue-router": "^4.2.0",
"vue-toast-notification": "^3",
"vue-virtual-scroll-grid": "^1.11.0",
"vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^2.0.0",
"vuetify": "3.5.14",
"vuetify-use-dialog": "^0.6.0",
"vuetify-use-dialog": "^0.6.11",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0",
"webfontloader": "^1.6.28"
@@ -81,6 +82,7 @@
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.14",
"dayjs": "^1.11.10",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
@@ -112,4 +114,4 @@
"resolutions": {
"postcss": "8"
}
}
}

View File

@@ -1,18 +1,20 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
// 是否显示
innerClass: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['click'])
const emit = defineEmits(['click', 'update:modelValue'])
// 按钮点击
function onClick() {
emit('update:modelValue', false)
emit('click')
}
</script>
<template>
<IconBtn
class="absolute right-3 top-3"
@click.stop="onClick"
>
<IconBtn :class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'" @click.stop="onClick">
<VIcon icon="mdi-close" />
</IconBtn>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String
})
</script>
<template>
<div
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!props.text"
size="48"
indeterminate
color="primary"
/>
<VProgressCircular
v-if="props.progress"
class="mb-3"
color="primary"
:model-value="props.progress"
size="64"
/>
<span>{{ props.text }}</span>
</div>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
interface Props {
color?: string
message?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
<VBadge :color="props.color" bordered>
<template #badge>
<VIcon icon="mdi-pulse"></VIcon>
</template>
</VBadge>
</div>
</template>

View File

@@ -2,6 +2,8 @@
import { ref } from 'vue'
import { useTheme } from 'vuetify'
import type { ThemeSwitcherTheme } from '@layouts/types'
import api from '@/api'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
const props = defineProps<{
themes: ThemeSwitcherTheme[]
@@ -20,54 +22,17 @@ const {
{ initialValue: savedTheme.value },
)
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()
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
watch(
() => 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
// })
function hasScrollbar(el?: Element | null) {
if (!el || el.nodeType !== Node.ELEMENT_NODE)
return false
const style = window.getComputedStyle(el)
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
// 主题切换动画
function themeTransition() {
const x = performance.now()
for (let i = 0; i++ < 1e7; (i << 9) & ((9 % 9) * 9 + 9));
const cost = performance.now() - x
if (cost > 10)
return
if (cost > 10) return
const el: HTMLElement = document.querySelector('[data-v-app]')!
const children = el.querySelectorAll('*') as NodeListOf<HTMLElement>
children.forEach((el) => {
children.forEach(el => {
if (hasScrollbar(el)) {
el.dataset.scrollX = String(el.scrollLeft)
el.dataset.scrollY = String(el.scrollTop)
@@ -99,7 +64,7 @@ function themeTransition() {
})
document.body.append(copy)
; (copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach((el) => {
;(copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach(el => {
el.scrollLeft = +el.dataset.scrollX!
el.scrollTop = +el.dataset.scrollY!
})
@@ -117,6 +82,55 @@ function themeTransition() {
el.addEventListener('transitionend', onTransitionend)
el.addEventListener('transitioncancel', onTransitionend)
}
// 更新主题
function updateTheme() {
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
globalTheme.name.value = theme
savedTheme.value = theme
themeTransition()
}
// 切换主题
function changeTheme() {
const nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
// 保存主题到本地
localStorage.setItem('theme', nextTheme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
// 保存主题到服务端
try {
api.post('/user/config/theme', nextTheme, {
headers: {
'Content-Type': 'text/plain',
},
})
} catch (e) {
console.error('保存主题到服务端失败')
}
}
// 是否有滚动条
function hasScrollbar(el?: Element | null) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
const style = window.getComputedStyle(el)
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}
// 监听系统主题变化
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
} catch (e) {
console.error('当前设备不支持监听系统主题变化')
}
// 监听设置主题变化
watch(
() => currentThemeName.value,
() => updateTheme(),
)
</script>
<template>

View File

@@ -1,5 +1,12 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import ZH_CN from 'dayjs/locale/zh-cn'
import { isToday } from './index'
dayjs.extend(relativeTime)
dayjs.locale(ZH_CN)
export function avatarText(value: string) {
if (!value)
return ''
@@ -19,7 +26,7 @@ export function kFormatter(num: number) {
* Format and return date in Humanize format
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
* Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
* @param {String} value date to format
* @param {string} value date to format
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
*/
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
@@ -32,8 +39,8 @@ export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions
/**
* Return short human friendly month representation of date
* Can also convert date to only time if date is of today (Better UX)
* @param {String} value date to format
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current
* @param {string} value date to format
* @param {boolean} toTimeForCurrentDay Shall convert to time if day is today/current
*/
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
const date = new Date(value)
@@ -107,7 +114,7 @@ export function formatBytes(bytes: number, decimals = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
}
// 格式化剧集列表
@@ -150,20 +157,21 @@ export function formatEp(nums: number[]): string {
// 将yyyy-mm-dd hh:mm:ss转换为时间差1小时前1天前
export function formatDateDifference(dateString: string): string {
const date = new Date(dateString)
const currentDate = new Date()
const timeDifference = currentDate.getTime() - date.getTime()
const secondsDifference = Math.floor(timeDifference / 1000)
const minutesDifference = Math.floor(secondsDifference / 60)
const hoursDifference = Math.floor(minutesDifference / 60)
const daysDifference = Math.floor(hoursDifference / 24)
// const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
// const secondsDifference = Math.floor(timeDifference / 1000)
// const minutesDifference = Math.floor(secondsDifference / 60)
// const hoursDifference = Math.floor(minutesDifference / 60)
// const daysDifference = Math.floor(hoursDifference / 24)
if (daysDifference > 0)
return `${daysDifference}天前`
else if (hoursDifference > 0)
return `${hoursDifference}小时前`
else if (minutesDifference > 0)
return `${minutesDifference}分钟前`
else
return '刚刚'
// if (daysDifference > 0)
// return `${daysDifference}天前`
// else if (hoursDifference > 0)
// return `${hoursDifference}小时前`
// else if (minutesDifference > 0)
// return `${minutesDifference}分钟前`
// else
// return '刚刚'
if (!dateString)
return ''
return dayjs(dateString).fromNow()
}

View File

@@ -33,12 +33,16 @@ export function isToday(date: Date) {
)
}
// 计算时间差返回xx天/xx小时/xx分钟/xx秒
/**
* 计算时间差返回xx天/xx小时/xx分钟/xx秒
*
* @deprecated 建议使用:@core/utils/formatters.ts formatDateDifference
*/
export function calculateTimeDifference(inputTime: string): string {
if (!inputTime)
return ''
const inputDate = new Date(inputTime)
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime()
@@ -70,7 +74,7 @@ export function calculateTimeDiff(inputTime: string): string {
return ''
// 使用当前时区
const inputDate = new Date(inputTime)
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime()
@@ -114,3 +118,12 @@ export function isNullOrEmptyObject(obj: any): boolean {
// 然后判断是否为空对象
return !!(typeof obj === 'object' && Object.keys(obj).length === 0)
}
// 判断系统配置色是否是黑暗的
export function checkPrefersColorSchemeIsDark(): boolean {
try {
return window.matchMedia('(prefers-color-scheme: dark)').matches
} catch (e) {
return false
}
}

View File

@@ -52,7 +52,7 @@ export default defineComponent({
'main',
{ class: 'layout-page-content' },
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true },
h('section', { class: 'page-content-container' }, slots.default?.()),
() => h('section', { class: 'page-content-container' }, slots.default?.()),
),
)

View File

@@ -1,38 +1,31 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import api from '@/api'
import store from './store'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
const { global: globalTheme } = useTheme()
// 提示框
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
// 生效主题
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
}
// SSE持续接收消息
function startSSEMessager() {
const token = store.state.auth.token
if (token) {
const eventSource = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`,
)
const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`)
eventSource.addEventListener('message', (event) => {
eventSource.addEventListener('message', event => {
const message = event.data
if (message)
$toast.info(message)
if (message) $toast.info(message)
})
onBeforeUnmount(() => {
@@ -41,10 +34,30 @@ function startSSEMessager() {
}
}
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem('MP_DASHBOARD')) {
localStorage.setItem('MP_DASHBOARD', data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem('MP_DASHBOARD')) {
return
}
await loadDashboardConfig()
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
setTheme()
startSSEMessager()
await tryLoadDashboardConfig()
})
</script>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,9 +1,8 @@
<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 FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import type { EndPoints } from '@/api/types'
// 输入参数
@@ -100,7 +99,7 @@ onMounted(() => {
<template>
<VCard class="mx-auto" :loading="loading > 0 || !path">
<div v-if="path">
<Toolbar
<FileToolbar
:path="path"
:storages="storagesArray"
:storage="activeStorage"
@@ -111,7 +110,7 @@ onMounted(() => {
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<List
<FileList
:path="path"
:storage="activeStorage"
:icons="fileIcons"

View File

@@ -25,7 +25,7 @@ function goPlay() {
// 计算图片地址
const getImgUrl = computed(() => {
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
</script>

View File

@@ -1,95 +0,0 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { BangumiPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<BangumiPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.images)
return personIcon
return personInfo.value?.images?.medium
}
// 使用、拼装人物角色
function getPersonCharacter() {
if (!personInfo.value?.career)
return ''
return personInfo.value?.career.join('、')
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://bangumi.tv/person/${personInfo.value?.id}`, '_blank')
}
</script>
<template>
<VHover v-bind="personProps">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
class="rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
size="120"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>
</div>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.person-card {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
}
.person-card:hover {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { kFormatter } from '@core/utils/formatters'
interface Props {
title: string
color?: string
icon: string
stats: number
change: number
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
size="44"
rounded
:color="props.color"
variant="tonal"
class="me-4"
>
<VIcon
:icon="props.icon"
size="30"
/>
</VAvatar>
<div>
<span class="text-caption">{{ props.title }}</span>
<div class="d-flex align-center flex-wrap">
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
<div
v-if="props.change"
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
>
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -1,56 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
color?: string
icon: string
stats: string
change: number
subtitle: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
v-if="props.icon"
size="38"
:color="props.color"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<VSpacer />
<MoreBtn class="me-n3 mt-n1" />
</VCardText>
<VCardText>
<h6 class="text-sm font-weight-semibold mb-2">
{{ props.title }}
</h6>
<div
v-if="props.change"
class="d-flex align-center mb-2"
>
<span class="font-weight-semibold text-h5 me-2">{{ props.stats }}</span>
<span
:class="isPositive ? 'text-success' : 'text-error'"
class="text-caption"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<span class="text-caption">{{ props.subtitle }}</span>
</VCardText>
</VCard>
</template>

View File

@@ -1,65 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
subtitle: string
stats: string
change: number
image: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard class="overflow-visible">
<div class="d-flex position-relative">
<VCardText>
<h6 class="text-base font-weight-semibold mb-4">
{{ props.title }}
</h6>
<div class="d-flex align-center flex-wrap mb-4">
<h5 class="text-h5 font-weight-semibold me-2">
{{ props.stats }}
</h5>
<span
class="text-caption"
:class="isPositive ? 'text-success' : 'text-error'"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<VChip
v-if="props.subtitle"
size="small"
:color="props.color"
>
{{ props.subtitle }}
</VChip>
</VCardText>
<VSpacer />
<div class="illustrator-img">
<VImg
v-if="props.image"
:src="props.image"
:width="110"
/>
</div>
</div>
</VCard>
</template>
<style lang="scss">
.illustrator-img {
position: absolute;
inset-block-end: 0;
inset-inline-end: 5%;
}
</style>

View File

@@ -1,88 +0,0 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { DoubanPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<DoubanPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.avatar)
return personIcon
return personInfo.value?.avatar?.large
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://movie.douban.com/celebrity/${personInfo.value?.id}/`, '_blank')
}
</script>
<template>
<VHover v-bind="personProps">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
class="rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
size="120"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ personInfo?.character }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>
</div>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.person-card {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
}
.person-card:hover {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
}
</style>

View File

@@ -56,7 +56,7 @@ function getImgUrl(url: string) {
if (!url)
return getDefaultImage()
else
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(url).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
}
// 根据多张图片生成媒体库封面
@@ -68,7 +68,7 @@ async function drawImages(imageList: string[]) {
// 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(IMAGES[i]).replace(/%2F/g, '/')}`
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
// canvas
const canvas = canvasRef.value

View File

@@ -1,13 +1,16 @@
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
import router from '@/router'
import noImage from '@images/no-image.jpeg'
import tmdbImage from '@images/logos/tmdb.png'
import doubanImage from '@images/logos/douban-black.png'
import bangumiImage from '@images/logos/bangumi.png'
// 输入参数
const props = defineProps({
@@ -52,31 +55,35 @@ const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([])
// 来源角标字典
const sourceIconDict = {
themoviedb: tmdbImage,
douban: doubanImage,
bangumi: bangumiImage,
}
// 获得mediaid
function getMediaId() {
return props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: props.media?.douban_id
? `douban:${props.media?.douban_id}`
: `bangumi:${props.media?.bangumi_id}`
? `douban:${props.media?.douban_id}`
: `bangumi:${props.media?.bangumi_id}`
}
// 订阅弹窗选择的多季
function subscribeSeasons() {
subscribeSeasonDialog.value = false
seasonsSelected.value.forEach((season) => {
seasonsSelected.value.forEach(season => {
addSubscribe(season.season_number)
})
}
// 角标颜色
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'
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'
}
// 添加订阅处理
@@ -93,26 +100,22 @@ async function handleAddSubscribe() {
// 检查各季的缺失状态
await checkSeasonsNotExists()
if (!tmdbFlag.value)
return
if (!tmdbFlag.value) return
if (seasonInfos.value.length === 1) {
// 添加订阅
addSubscribe(1)
}
else {
} else {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
}
else if (props.media?.type === '电视剧') {
} else if (props.media?.type === '电视剧') {
// 豆瓣电视剧,只会有一季
const season = props.media?.season ?? 1
// 添加订阅
addSubscribe(season)
}
else {
} else {
// 电影
addSubscribe()
}
@@ -147,46 +150,32 @@ async function addSubscribe(season = 0) {
}
// 提示
showSubscribeAddToast(
result.success,
props.media?.title ?? '',
season,
result.message,
best_version,
)
showSubscribeAddToast(result.success, props.media?.title ?? '', season, result.message, best_version)
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
const show_edit_dialog = await querySubscribeRules()
const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
}
catch (error) {
} catch (error) {
console.error(error)
} finally {
doneNProgress()
}
doneNProgress()
}
// 弹出添加订阅提示
function showSubscribeAddToast(result: boolean,
title: string,
season: number,
message: string,
best_version: number) {
if (season)
title = `${title} ${formatSeason(season.toString())}`
function showSubscribeAddToast(result: boolean, title: string, season: number, message: string, best_version: number) {
if (season) title = `${title} ${formatSeason(season.toString())}`
let subname = '订阅'
if (best_version > 0)
subname = '洗版订阅'
if (best_version > 0) subname = '洗版订阅'
if (result && seasonsSelected.value.length > 1)
$toast.success(`${title} 添加${subname}成功`)
else if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
if (result && seasonsSelected.value.length > 1) $toast.success(`${title} 添加${subname}成功!`)
else if (!result) $toast.error(`${title} 添加${subname}失败:${message}`)
}
// 调用API取消订阅
@@ -196,24 +185,19 @@ async function removeSubscribe() {
try {
const mediaid = getMediaId()
const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`,
{
params: {
season: props.media?.season,
},
const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
params: {
season: props.media?.season,
},
)
})
if (result.success) {
isSubscribed.value = false
$toast.success(`${props.media?.title} 已取消订阅!`)
}
else {
} else {
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
doneNProgress()
@@ -223,10 +207,8 @@ async function removeSubscribe() {
async function handleCheckSubscribe() {
try {
const result = await checkSubscribe(props.media?.season)
if (result)
isSubscribed.value = true
}
catch (error) {
if (result) isSubscribed.value = true
} catch (error) {
console.error(error)
}
}
@@ -244,10 +226,8 @@ async function handleCheckExists() {
},
})
if (result.success)
isExists.value = true
}
catch (error) {
if (result.success) isExists.value = true
} catch (error) {
console.error(error)
}
}
@@ -265,8 +245,7 @@ async function checkSubscribe(season = 0) {
})
return result.id || null
}
catch (error) {
} catch (error) {
console.error(error)
}
@@ -280,19 +259,16 @@ async function checkSeasonsNotExists() {
try {
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
if (result) {
result.forEach((item) => {
result.forEach(item => {
// 0-已入库 1-部分缺失 2-全部缺失
let state = 0
if (item.episodes.length === 0)
state = 2
else if (item.episodes.length < item.total_episode)
state = 1
if (item.episodes.length === 0) state = 2
else if (item.episodes.length < item.total_episode) state = 1
seasonsNotExisted.value[item.season] = state
})
}
}
catch (error) {
} catch (error) {
$toast.error(`${props.media?.title}无法识别TMDB媒体信息`)
tmdbFlag.value = false
}
@@ -305,22 +281,22 @@ async function checkSeasonsNotExists() {
async function getMediaSeasons() {
try {
seasonInfos.value = await api.get(`tmdb/seasons/${props.media?.tmdb_id}`)
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 查询订阅弹窗规则
async function querySubscribeRules() {
async function queryDefaultSubscribeConfig() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
catch (error) {
let subscribe_config_url = ''
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) return result.data.value.show_edit_dialog
} catch (error) {
console.log(error)
}
return false
@@ -328,49 +304,41 @@ async function querySubscribeRules() {
// 爱心订阅按钮响应
function handleSubscribe() {
if (isSubscribed.value)
removeSubscribe()
else
handleAddSubscribe()
if (isSubscribed.value) removeSubscribe()
else handleAddSubscribe()
}
// 计算存在状态的颜色
function getExistColor(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return 'success'
if (!state) return 'success'
if (state === 1)
return 'warning'
else if (state === 2)
return 'error'
else
return 'success'
if (state === 1) return 'warning'
else if (state === 2) return 'error'
else return 'success'
}
// 计算存在状态的文本
function getExistText(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return '已入库'
if (!state) return '已入库'
if (state === 1)
return '部分缺失'
else if (state === 2)
return '缺失'
else
return '已入库'
if (state === 1) return '部分缺失'
else if (state === 2) return '缺失'
else return '已入库'
}
// 打开详情页
function goMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
function goMediaDetail(isHovering = false) {
if (isHovering) {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
}
}
// 开始搜索
@@ -394,41 +362,37 @@ onBeforeMount(() => {
// 计算图片地址
const getImgUrl: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
// 如果地址中包含douban则使用中转代理
if (url.includes('doubanio.com'))
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
return url
})
// 拼装季图片地址
function getSeasonPoster(posterPath: string) {
if (!posterPath)
return ''
if (!posterPath) return ''
return `https://image.tmdb.org/t/p/w500${posterPath}`
}
// 将yyyy-mm-dd转换为yyyy年mm月dd日
function formatAirDate(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
if (!airDate) return ''
const date = new Date(airDate.replaceAll(/-/g, '/'))
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
}
// 从yyyy-mm-dd中提取年份
function getYear(airDate: string) {
if (!airDate)
return ''
const date = new Date(airDate)
if (!airDate) return ''
const date = new Date(airDate.replaceAll(/-/g, '/'))
return date.getFullYear()
}
</script>
<template>
<VHover v-bind="props">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -439,7 +403,7 @@ function getYear(airDate: string) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goMediaDetail"
@click.stop="goMediaDetail(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"
@@ -467,10 +431,10 @@ function getYear(airDate: string) {
{{ props.media?.type }}
</VChip>
<!-- 本地存在标识 -->
<ExistIcon v-if="isExists" />
<ExistIcon v-if="isExists && !hover.isHovering" />
<!-- 评分角标 -->
<VChip
v-if="isImageLoaded && props.media?.vote_average && !isExists"
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
variant="elevated"
size="small"
:class="getChipColor('rating')"
@@ -491,43 +455,30 @@ function getYear(airDate: string) {
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn
icon="mdi-magnify"
color="white"
@click.stop="handleSearch"
/>
<IconBtn
icon="mdi-heart"
:color="isSubscribed ? 'error' : 'white'"
@click.stop="handleSubscribe"
/>
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
</div>
</VCardText>
<VAvatar
size="24"
density="compact"
class="absolute bottom-1 right-1"
tile
v-if="!hover.isHovering && isImageLoaded"
>
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
</VAvatar>
</VCard>
</template>
</VHover>
<!-- 订阅季弹窗 -->
<VBottomSheet
v-if="subscribeSeasonDialog"
v-model="subscribeSeasonDialog"
inset
scrollable
>
<VBottomSheet v-if="subscribeSeasonDialog" v-model="subscribeSeasonDialog" inset scrollable>
<VCard class="rounded-t">
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardTitle class="pe-10">
订阅 - {{ props.media?.title }}
</VCardTitle>
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
<VCardText>
<VList
v-model:selected="seasonsSelected"
lines="three"
select-strategy="classic"
>
<VListItem
v-for="(item, i) in seasonInfos" :key="i"
:value="item"
>
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
<template #prepend>
<VImg
height="90"
@@ -544,16 +495,9 @@ function getYear(airDate: string) {
</template>
</VImg>
</template>
<VListItemTitle>
{{ item.season_number }}
</VListItemTitle>
<VListItemTitle> {{ item.season_number }} </VListItemTitle>
<VListItemSubtitle class="mt-1 me-2">
<VChip
v-if="item.vote_average"
color="primary"
size="small"
class="mb-1"
>
<VChip v-if="item.vote_average" color="primary" size="small" class="mb-1">
<VIcon icon="mdi-star" /> {{ item.vote_average }}
</VChip>
{{ getYear(item.air_date || '') }} {{ item.episode_count }}
@@ -562,12 +506,7 @@ function getYear(airDate: string) {
{{ media?.title }} {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播
</VListItemSubtitle>
<VListItemSubtitle>
<VChip
v-if="seasonsNotExisted"
class="mt-2"
size="small"
:color="getExistColor(item.season_number || 0)"
>
<VChip v-if="seasonsNotExisted" class="mt-2" size="small" :color="getExistColor(item.season_number || 0)">
{{ getExistText(item.season_number || 0) }}
</VChip>
</VListItemSubtitle>
@@ -580,24 +519,25 @@ function getYear(airDate: string) {
</VList>
</VCardText>
<div class="my-2 text-center">
<VBtn
:disabled="seasonsSelected.length === 0"
width="30%"
@click="subscribeSeasons"
>
<VBtn :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
</VBtn>
</div>
</VCard>
</VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
@remove="
() => {
subscribeEditDialog = false
handleCheckSubscribe()
}
"
/>
</template>

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { TmdbPerson } from '@/api/types'
import type { Person } from '@/api/types'
import router from '@/router'
const personProps = defineProps({
person: Object as PropType<TmdbPerson>,
person: Object as PropType<Person>,
width: String,
height: String,
})
@@ -17,26 +17,54 @@ const isImageLoaded = ref(false)
//
function getPersonImage() {
if (!personInfo.value?.profile_path)
if (personProps.person?.source === 'themoviedb') {
if (!personInfo.value?.profile_path) return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
} else if (personProps.person?.source === 'douban') {
if (!personInfo.value?.avatar) return personIcon
if (typeof personInfo.value?.avatar === 'object') {
return personInfo.value?.avatar?.normal
} else {
return personInfo.value?.avatar
}
} else if (personProps.person?.source === 'bangumi') {
if (!personInfo.value?.images) return personIcon
return personInfo.value?.images?.medium
} else {
return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
}
}
//
function getPersonName() {
return personInfo.value?.name
}
//
function getPersonCharacter() {
if (personProps.person?.source === 'bangumi') {
if (!personInfo.value?.career) return ''
return personInfo.value?.career.join('、')
} else {
return personInfo.value?.character
}
}
//
function goPersonDetail() {
if (!personInfo.value?.id)
return
if (!personInfo.value?.id) return
router.push({
path: '/person',
query: {
personid: personInfo.value?.id,
source: personInfo.value?.source,
},
})
}
</script>
<template>
<VHover v-bind="personProps">
<VHover>
<template #default="hover">
<VCard
v-bind="hover.props"
@@ -49,9 +77,9 @@ function goPersonDetail() {
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
class="person-card relative transform-gpu cursor-pointer rounded shadow transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div style="padding-block-end: 150%">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
@@ -60,19 +88,17 @@ function goPersonDetail() {
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
<VImg :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
{{ getPersonName() }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ personInfo?.character }}
<div
class="overflow-hidden whitespace-normal text-center text-sm"
style="display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2"
>
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '@/api/types'
import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image'
import { isNullOrEmptyObject } from '@/@core/utils'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
// 输入参数
const props = defineProps({
@@ -42,6 +43,12 @@ const imageLoadError = ref(false)
// 更新日志弹窗
const releaseDialog = ref(false)
// 计算插件标签
const pluginLabels = computed(() => {
if (!props.plugin?.plugin_label) return []
return props.plugin.plugin_label.split(',')
})
// 图片加载完成
async function imageLoaded() {
isImageLoaded.value = true
@@ -57,15 +64,12 @@ async function installPlugin() {
progressDialog.value = true
progressText.value = `正在安装 ${props.plugin?.plugin_name} v${props?.plugin?.plugin_version} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: props.plugin?.has_update,
},
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
params: {
repo_url: props.plugin?.repo_url,
force: props.plugin?.has_update,
},
)
})
// 隐藏等待提示框
progressDialog.value = false
@@ -75,23 +79,20 @@ async function installPlugin() {
// 通知父组件刷新
emit('install')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 计算图标路径
const iconPath: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})
@@ -102,22 +103,18 @@ function visitPluginPage() {
let repoUrl = props.plugin?.repo_url
if (repoUrl) {
if (repoUrl.includes('raw.githubusercontent.com')) {
if (!repoUrl.endsWith('/'))
repoUrl += '/'
if (!repoUrl.endsWith('/')) repoUrl += '/'
if (repoUrl.split('/').length < 6)
repoUrl = `${repoUrl}main/`
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) {
} catch (error) {
return
}
}
}
else {
} else {
repoUrl = props.plugin?.author_url
}
window.open(repoUrl, '_blank')
@@ -138,7 +135,8 @@ const dropdownItems = ref([
prependIcon: 'mdi-github',
click: visitPluginPage,
},
}, {
},
{
title: '更新说明',
value: 2,
show: !isNullOrEmptyObject(props.plugin?.history || {}),
@@ -151,22 +149,12 @@ const dropdownItems = ref([
</script>
<template>
<VCard
:width="props.width"
:height="props.height"
@click="installPlugin"
>
<div
class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${backgroundColor}` }"
>
<VCard :width="props.width" :height="props.height" @click="installPlugin">
<div class="relative pa-4 text-center card-cover-blurred" :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
>
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
@@ -184,9 +172,7 @@ const dropdownItems = ref([
</VMenu>
</IconBtn>
</div>
<VAvatar
size="8rem"
>
<VAvatar size="8rem">
<VImg
ref="imageRef"
:src="iconPath"
@@ -203,16 +189,17 @@ const dropdownItems = ref([
<span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
<VCardText class="pb-2">
{{ props.plugin?.plugin_desc }}
<div>{{ props.plugin?.plugin_desc }}</div>
<div>
<VChip v-for="label in pluginLabels" variant="tonal" size="small" class="me-1 my-1" color="info" label>
{{ label }}
</VChip>
</div>
</VCardText>
<VCardText class="flex items-center justify-start pb-2">
<span>
<VIcon icon="mdi-account" class="me-1" />
<a
:href="props.plugin?.author_url"
target="_blank"
@click.stop
>
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
{{ props.plugin?.plugin_author }}
</a>
</span>
@@ -223,31 +210,9 @@ const dropdownItems = ref([
</VCardText>
</VCard>
<!-- 安装插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog
v-if="releaseDialog"
v-model="releaseDialog"
width="600"
scrollable
>
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
<VCard>
<DialogCloseBtn @click="releaseDialog = false" />
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
@@ -263,7 +228,7 @@ const dropdownItems = ref([
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: rgba(29, 39, 59, 48%);
content: "";
content: '';
inset: 0;
}
</style>

View File

@@ -11,6 +11,11 @@ import { isNullOrEmptyObject } from '@core/utils'
import noImage from '@images/logos/plugin.png'
import { getDominantColor } from '@/@core/utils/image'
import store from '@/store'
import { useDisplay } from 'vuetify'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
@@ -58,7 +63,7 @@ const pluginInfoDialog = ref(false)
const progressText = ref('正在更新插件...')
// 插件数据页面配置项
let pluginPageItems = reactive([])
let pluginPageItems = ref([])
// 图片是否加载完成
const isImageLoaded = ref(false)
@@ -70,12 +75,15 @@ const imageLoadError = ref(false)
const releaseDialog = ref(false)
// 监听动作标识如为true则打开详情
watch(() => props.action, (newAction, oldAction) => {
if (newAction && !oldAction) {
openPluginDetail()
emit('actionDone')
}
})
watch(
() => props.action,
(newAction, oldAction) => {
if (newAction && !oldAction) {
openPluginDetail()
emit('actionDone')
}
},
)
// 图片加载完成
async function imageLoaded() {
@@ -90,7 +98,7 @@ function showUpdateHistory() {
// 检查当前版本是否有更新日志
if (isNullOrEmptyObject(props.plugin?.history)) {
updatePlugin()
} else{
} else {
releaseDialog.value = true
}
}
@@ -110,11 +118,10 @@ async function uninstallPlugin() {
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
try {
// 显示等待提示框
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
@@ -125,12 +132,10 @@ async function uninstallPlugin() {
// 通知父组件刷新
emit('remove')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -141,11 +146,9 @@ async function loadPluginForm() {
const result: { [key: string]: any } = await api.get(`plugin/form/${props.plugin?.id}`)
if (result) {
pluginFormItems = result.conf
if (result.model)
pluginConfigForm.value = result.model
if (result.model) pluginConfigForm.value = result.model
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -154,10 +157,8 @@ async function loadPluginForm() {
async function loadPluginPage() {
try {
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
if (result)
pluginPageItems = result
}
catch (error) {
if (result) pluginPageItems.value = result
} catch (error) {
console.error(error)
}
}
@@ -166,10 +167,8 @@ async function loadPluginPage() {
async function loadPluginConf() {
try {
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
if (!isNullOrEmptyObject(result))
pluginConfigForm.value = result
}
catch (error) {
if (!isNullOrEmptyObject(result)) pluginConfigForm.value = result
} catch (error) {
console.error(error)
}
}
@@ -187,13 +186,11 @@ async function savePluginConf() {
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
// 通知父组件刷新
emit('save')
}
else {
} else {
progressDialog.value = false
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -219,11 +216,10 @@ async function showPluginConfig() {
// 计算图标路径
const iconPath: Ref<string> = computed(() => {
if (imageLoadError.value)
return noImage
if (imageLoadError.value) return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})
@@ -243,8 +239,7 @@ async function resetPlugin() {
},
})
if (!isConfirmed)
return
if (!isConfirmed) return
try {
const result: { [key: string]: any } = await api.get(`plugin/reset/${props.plugin?.id}`)
@@ -252,12 +247,10 @@ async function resetPlugin() {
$toast.success(`插件 ${props.plugin?.plugin_name} 数据已重置`)
// 通知父组件刷新
emit('save')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 重置失败:${result.message}}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -270,15 +263,12 @@ async function updatePlugin() {
progressDialog.value = true
progressText.value = `正在更新 ${props.plugin?.plugin_name} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: true,
},
const result: { [key: string]: any } = await api.get(`plugin/install/${props.plugin?.id}`, {
params: {
repo_url: props.plugin?.repo_url,
force: true,
},
)
})
// 隐藏等待提示框
progressDialog.value = false
@@ -288,12 +278,10 @@ async function updatePlugin() {
// 通知父组件刷新
emit('save')
}
else {
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 更新失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -306,16 +294,16 @@ function visitAuthorPage() {
// 查看日志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`
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')
}
// 打开插件详情
function openPluginDetail() {
if (props.plugin?.has_page)
showPluginInfo()
else
showPluginConfig()
if (props.plugin?.has_page) showPluginInfo()
else showPluginConfig()
}
// 弹出菜单
@@ -391,41 +379,26 @@ const dropdownItems = ref([
])
// 监听插件状态变化
watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1)
dropdownItems.value[updateItemIndex].show = newHasUpdate
})
watch(
() => props.plugin?.has_update,
(newHasUpdate, oldHasUpdate) => {
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
if (updateItemIndex !== -1) dropdownItems.value[updateItemIndex].show = newHasUpdate
},
)
</script>
<template>
<!-- 插件卡片 -->
<VCard
v-if="isVisible"
:width="props.width"
:height="props.height"
@click="openPluginDetail"
>
<div
class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${backgroundColor}` }"
>
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 left-1"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail">
<div class="relative pa-4 text-center card-cover-blurred" :style="{ background: `${backgroundColor}` }">
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 left-1">
<VIcon icon="mdi-new-box" class="text-white" />
</div>
<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
>
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
@@ -444,9 +417,7 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
</VMenu>
</IconBtn>
</div>
<VAvatar
size="8rem"
>
<VAvatar size="8rem">
<VImg
ref="imageRef"
:src="iconPath"
@@ -465,116 +436,56 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
<VCardItem class="py-2">
<VCardTitle class="flex items-center flex-row">
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
{{ props.plugin?.plugin_name
}}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
</VCardItem>
<VCardText>
{{ props.plugin?.plugin_desc }}
</VCardText>
</VCard>
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
scrollable
max-width="60rem"
>
<VCard
:title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
<DialogCloseBtn v-model="pluginConfigDialog" />
<VCardText>
<FormRender
v-for="(item, index) in pluginFormItems"
:key="index"
:config="item"
:form="pluginConfigForm"
/>
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
</VCardText>
<VCardActions>
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
查看数据
</VBtn>
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo"> 查看数据 </VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="savePluginConf"
>
保存
</VBtn>
<VBtn variant="tonal" @click="savePluginConf"> 保存 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 插件数据页面 -->
<VDialog
v-model="pluginInfoDialog"
scrollable
max-width="80rem"
>
<VCard
:title="`${props.plugin?.plugin_name}`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
<DialogCloseBtn v-model="pluginInfoDialog" />
<VCardText>
<PageRender
v-for="(item, index) in pluginPageItems"
:key="index"
:config="item"
/>
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
</VCardText>
<VCardActions>
<VBtn
@click="showPluginConfig"
>
配置
</VBtn>
<VBtn @click="showPluginConfig"> 配置 </VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭
</VBtn>
<VBtn variant="tonal" @click="pluginInfoDialog = false"> 关闭 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 更新插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
<!-- 更新日志 -->
<VDialog
v-if="releaseDialog"
v-model="releaseDialog"
width="600"
scrollable
>
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
<VCard>
<DialogCloseBtn @click="releaseDialog = false" />
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
<VersionHistory :history="props.plugin?.history" />
<VCardText>
<VBtn
@click="updatePlugin"
block
>
<VBtn @click="updatePlugin" block>
<template #prepend>
<VIcon icon="mdi-arrow-up-circle-outline" />
</template>
@@ -592,7 +503,7 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: rgba(29, 39, 59, 48%);
content: "";
content: '';
inset: 0;
}
</style>

View File

@@ -31,12 +31,12 @@ const getImgUrl = computed(() => {
if (imageLoadError.value)
return noImage
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
// 跳转播放
function goPlay() {
if (props.media?.link)
function goPlay(isHovering = false) {
if (props.media?.link && isHovering)
window.open(props.media?.link, '_blank')
}
</script>
@@ -53,7 +53,7 @@ function goPlay() {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goPlay"
@click.stop="goPlay(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"

View File

@@ -1,12 +1,17 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Site } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue'
import type { Site, SiteStatistic } from '@/api/types'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const cardProps = defineProps({
@@ -58,12 +63,14 @@ const userPwForm = ref({
code: '',
})
// 站点使用统计
const siteStats = ref<SiteStatistic>({})
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${cardProps.site?.id}`)).data.icon
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -75,15 +82,23 @@ async function testSite() {
testButtonDisable.value = true
const result: { [key: string]: any } = await api.get(`site/test/${cardProps.site?.id}`)
if (result.success)
$toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
else
$toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
if (result.success) $toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
else $toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
testButtonText.value = '测试'
testButtonDisable.value = false
getSiteStats()
} catch (error) {
console.error(error)
}
catch (error) {
}
// 查询站点使用统计
async function getSiteStats() {
try {
siteStats.value = await api.get(`site/statistic/${cardProps.site?.domain}`)
} catch (error) {
console.error(error)
}
}
@@ -101,8 +116,7 @@ async function handleResourceBrowse() {
// 调用API更新站点Cookie UA
async function updateSiteCookie() {
try {
if (!userPwForm.value.username || !userPwForm.value.password)
return
if (!userPwForm.value.username || !userPwForm.value.password) return
// 更新按钮状态
siteCookieDialog.value = false
@@ -111,26 +125,20 @@ async function updateSiteCookie() {
progressDialog.value = true
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
const result: { [key: string]: any } = await api.get(
`site/cookie/${cardProps.site?.id}`,
{
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
code: userPwForm.value.code,
},
const result: { [key: string]: any } = await api.get(`site/cookie/${cardProps.site?.id}`, {
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
code: userPwForm.value.code,
},
)
})
if (result.success)
$toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
else
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
if (result.success) $toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
progressDialog.value = false
updateButtonDisable.value = false
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -140,9 +148,29 @@ function openSitePage() {
window.open(cardProps.site?.url, '_blank')
}
// 根据站点状态显示不同的状态图标
const statColor = computed(() => {
if (isNullOrEmptyObject(siteStats.value)) {
return 'secondary'
}
if (siteStats.value?.lst_state == 1) {
return 'error'
} else if (siteStats.value?.lst_state == 0) {
if (!siteStats.value?.seconds) return 'secondary'
if (siteStats.value?.seconds >= 5) return 'warning'
return 'success'
}
})
// 监听resourceDialog如果为false则重新查询站点使用统计
watch(resourceDialog, value => {
if (!value) getSiteStats()
})
// 装载时查询站点图标
onMounted(() => {
getSiteIcon()
getSiteStats()
})
</script>
@@ -155,103 +183,58 @@ onMounted(() => {
@click="siteEditDialog = true"
>
<template #image>
<VAvatar
class="absolute right-2 bottom-2 rounded"
variant="flat"
rounded="0"
>
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem>
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
<VCardSubtitle>
{{ cardProps.site?.url }}
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
</VCardSubtitle>
</VCardItem>
<ExistIcon v-if="cardProps.site?.is_active" />
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
<VCardText class="py-2">
<VTooltip
v-if="cardProps.site?.render === 1"
text="浏览器仿真"
>
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-apple-safari"
/>
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.proxy === 1"
text="代理"
>
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-network-outline"
/>
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.limit_interval"
text="流控"
>
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-speedometer"
/>
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
</template>
</VTooltip>
<VTooltip
v-if="cardProps.site?.filter"
text="过滤"
>
<VTooltip v-if="cardProps.site?.filter" text="过滤">
<template #activator="{ props }">
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-filter-cog-outline"
/>
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
</template>
</VTooltip>
</VCardText>
<VDivider
class="opacity-75"
style="border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));"
/>
<VDivider class="opacity-75" style="border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity))" />
<VCardActions>
<VBtn
v-if="!cardProps.site?.public"
:disabled="updateButtonDisable"
@click.stop="handleSiteUpdate"
>
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
更新
</VBtn>
<VBtn
:disabled="testButtonDisable"
@click.stop="testSite"
>
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
<template #prepend>
<VIcon icon="mdi-link" />
</template>
@@ -266,49 +249,29 @@ onMounted(() => {
</VCardActions>
</VCard>
<!-- 更新站点Cookie & UA弹窗 -->
<VDialog
v-model="siteCookieDialog"
max-width="50rem"
>
<VDialog v-model="siteCookieDialog" max-width="50rem">
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
<DialogCloseBtn @click="siteCookieDialog = false" />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="userPwForm.username"
label="用户名"
:rules="[requiredValidator]"
/>
<VCol cols="12" md="4">
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="userPwForm.password"
label="密码"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keydown.enter="updateSiteCookie"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="userPwForm.code"
label="两步验证"
/>
<VCol cols="12" md="4">
<VTextField v-model="userPwForm.code" label="两步验证" />
</VCol>
</VRow>
</VForm>
@@ -316,20 +279,20 @@ onMounted(() => {
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新
</VBtn>
<VBtn variant="tonal" @click="updateSiteCookie"> 开始更新 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<SiteAddEditForm
<SiteAddEditDialog
v-if="siteEditDialog"
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')"
@save="
() => {
siteEditDialog = false
emit('update')
}
"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
@@ -340,6 +303,7 @@ onMounted(() => {
max-width="80rem"
scrollable
z-index="1010"
:fullscreen="!display.mdAndUp.value"
>
<!-- Dialog Content -->
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
@@ -349,24 +313,8 @@ onMounted(() => {
</VCardText>
</VCard>
</VDialog>
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</template>
<style lang="scss">

View File

@@ -1,7 +1,7 @@
<script lang='ts' setup>
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { calculateTimeDifference } from '@/@core/utils'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatDateDifference } from '@/@core/utils/formatters'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import type { Subscribe } from '@/api/types'
@@ -26,11 +26,9 @@ const subscribeEditDialog = ref(false)
// 上一次更新时间
const lastUpdateText = ref(
`${
props.media?.last_update
? `${calculateTimeDifference(props.media?.last_update || '')}`
: ''
}`,
props.media && props.media.last_update
? formatDateDifference(props.media.last_update)
: '',
)
// 图片加载完成响应
@@ -284,7 +282,7 @@ const dropdownItems = ref([
/>
</VCard>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="props.media?.id"

View File

@@ -293,7 +293,7 @@ onMounted(() => {
<VExpandTransition>
<div v-show="showMoreTorrents">
<VDivider />
<VChipGroup class="p-3">
<VChipGroup class="p-3" column>
<VChip
v-for="(item, index) in props.more"
:key="index"

View File

@@ -0,0 +1,45 @@
<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>
<VDialog
width="40rem"
scrollable
max-height="85vh"
>
<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>
</VDialog>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
const props = defineProps({
value: Number,
text: String,
})
</script>
<template>
<!-- 手动整理进度框 -->
<VDialog :scrim="false" width="25rem">
<VCard color="primary">
<VCardText class="text-center">
{{ props.text }}
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -1,9 +1,14 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
import TmdbSelector from '../misc/TmdbSelector.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
//
const display = useDisplay()
//
const props = defineProps({
@@ -55,7 +60,6 @@ const transferForm = reactive({
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
watchEffect(() => {
@@ -72,7 +76,7 @@ function startLoadingProgress() {
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
progressEventSource.value.onmessage = event => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
@@ -89,8 +93,7 @@ function stopLoadingProgress() {
//
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() {
if (!props.logids && !props.path)
return
if (!props.logids && !props.path) return
//
progressDialog.value = true
@@ -100,32 +103,33 @@ async function transfer() {
if (props.path) {
//
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
const result: { [key: string]: any } = await api.post(
'transfer/manual',
{},
{
params: transferForm,
},
)
//
if (result.success)
$toast.success(`${props.path} 整理完成`)
else
$toast.error(`${props.path} 整理失败:${result.message}`)
}
catch (e) {
if (result.success) $toast.success(`${props.path} 整理完成!`)
else $toast.error(`${props.path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
else if (props.logids) {
} else if (props.logids) {
//
for (const logid of props.logids) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
if (!result.success)
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
}
catch (e) {
const result: { [key: string]: any } = await api.post(
'transfer/manual',
{},
{
params: transferForm,
},
)
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
@@ -141,10 +145,7 @@ async function transfer() {
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
@@ -153,10 +154,7 @@ async function transfer() {
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.target"
label="目的路径"
@@ -164,10 +162,7 @@ async function transfer() {
hint="留空将自动整理到媒体库目录"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
@@ -184,20 +179,18 @@ async function transfer() {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
:items="[
{ title: '自动', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
@@ -209,10 +202,7 @@ async function transfer() {
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
@@ -267,49 +257,16 @@ async function transfer() {
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="emit('close')">
取消
</VBtn>
<VBtn depressed @click="emit('close')"> 取消 </VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="transfer"
>
开始整理
</VBtn>
<VBtn variant="tonal" @click="transfer"> 开始整理 </VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- TMDB ID搜索框 -->
<VDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
max-height="85vh"
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
<VDialog v-model="tmdbSelectorDialog" width="40rem" scrollable max-height="85vh">
<TmdbSelector v-model="transferForm.tmdbid" @close="tmdbSelectorDialog = false" />
</VDialog>
</VDialog>
</template>

View File

@@ -4,6 +4,10 @@ import type { Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import { useDisplay } from 'vuetify'
//
const display = useDisplay()
//
const props = defineProps({
@@ -48,8 +52,7 @@ const priorityItems = ref(
//
watchEffect(async () => {
if (props.siteid)
fetchSiteInfo()
if (props.siteid) fetchSiteInfo()
})
//
@@ -58,27 +61,24 @@ async function fetchSiteInfo() {
siteForm.value = await api.get(`site/${props.siteid}`)
siteForm.value.proxy = siteForm.value.proxy === 1
siteForm.value.render = siteForm.value.render === 1
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// API
async function addSite() {
if (!siteForm.value?.url)
return
if (!siteForm.value?.url) return
startNProgress()
try {
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
if (result.success) {
$toast.success('新增站点成功')
emit('save')
} else {
$toast.error(`新增站点失败:${result.message}`)
}
else { $toast.error(`新增站点失败:${result.message}`) }
}
catch (error) {
} catch (error) {
console.error(error)
}
doneNProgress()
@@ -88,12 +88,9 @@ async function addSite() {
async function deleteSiteInfo() {
try {
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
if (result.success)
emit('remove')
if (result.success) emit('remove')
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
}
catch (error) {
} catch (error) {
$toast.error(`${siteForm.value?.name} 删除失败!`)
console.error(error)
}
@@ -107,10 +104,10 @@ async function updateSiteInfo() {
if (result.success) {
$toast.success(`${siteForm.value?.name} 更新成功!`)
emit('save')
} else {
$toast.error(`${siteForm.value?.name} 更新失败:${result.message}`)
}
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
}
catch (error) {
} catch (error) {
$toast.error(`${siteForm.value?.name} 更新失败!`)
console.error(error)
}
@@ -119,13 +116,7 @@ async function updateSiteInfo() {
</script>
<template>
<VDialog
scrollable
:close-on-back="false"
persistent
eager
max-width="60rem"
>
<VDialog scrollable :close-on-back="false" persistent eager max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
class="rounded-t"
@@ -134,10 +125,7 @@ async function updateSiteInfo() {
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.url"
label="站点地址"
@@ -145,10 +133,7 @@ async function updateSiteInfo() {
hint="格式http://www.example.com/"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VCol cols="12" md="3">
<VSelect
v-model="siteForm.pri"
label="优先级"
@@ -157,15 +142,8 @@ async function updateSiteInfo() {
hint="站点资源下载优先级,优先级数字越小越优先下载"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
<VCol cols="12" md="3">
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
</VCol>
</VRow>
<VRow>
@@ -183,6 +161,16 @@ async function updateSiteInfo() {
hint="浏览器打开站点首页打开开发人员工具刷新页面后在网络选项中找到首页地址在请求头中获取Cookie信息"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="siteForm.token"
label="请求头Authorization"
hint="在开发人员工具网络请求头中获取Authorization仅个别站点需要"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="siteForm.apikey" label="令牌API Key" hint="站点的访问API Key仅个别站点需要" />
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
@@ -192,10 +180,7 @@ async function updateSiteInfo() {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
@@ -203,10 +188,7 @@ async function updateSiteInfo() {
hint="设定站点限流的单位周期单位为秒0为不限流"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_count"
label="访问次数"
@@ -214,10 +196,7 @@ async function updateSiteInfo() {
hint="设定单位周期内站点允许的访问次数0为不限制"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
@@ -227,20 +206,10 @@ async function updateSiteInfo() {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
hint="站点是否需要代理访问,需要设置好代理服务器信息"
/>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.proxy" label="代理" hint="站点是否需要代理访问,需要设置好代理服务器信息" />
</VCol>
<VCol
cols="12"
md="6"
>
<VCol cols="12" md="6">
<VSwitch
v-model="siteForm.render"
label="仿真"
@@ -251,36 +220,11 @@ async function updateSiteInfo() {
</VForm>
</VCardText>
<VCardActions>
<VBtn
v-if="props.oper === 'add'"
@click="emit('close')"
>
取消
</VBtn>
<VBtn
v-else
color="error"
@click="deleteSiteInfo"
>
删除
</VBtn>
<VBtn v-if="props.oper === 'add'" @click="emit('close')"> 取消 </VBtn>
<VBtn v-else color="error" @click="deleteSiteInfo"> 删除 </VBtn>
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="tonal"
@click="addSite"
>
新增
</VBtn>
<VBtn
v-else
color="primary"
variant="tonal"
@click="updateSiteInfo"
>
保存
</VBtn>
<VBtn v-if="props.oper === 'add'" color="primary" variant="tonal" @click="addSite"> 新增 </VBtn>
<VBtn v-else color="primary" variant="tonal" @click="updateSiteInfo"> 保存 </VBtn>
</VCardActions>
</VCard>
</VDialog>

View File

@@ -3,6 +3,10 @@ import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
import { useDisplay } from 'vuetify'
//
const display = useDisplay()
//
const props = defineProps({
@@ -43,6 +47,8 @@ const subscribeForm = ref<Subscribe>({
username: '',
current_priority: 0,
save_path: '',
date: '',
show_edit_dialog: false,
})
//
@@ -57,10 +63,10 @@ async function updateSubscribeInfo() {
$toast.success(`${subscribeForm.value.name} 更新成功!`)
//
emit('save')
} else {
$toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`)
}
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`) }
}
catch (e) {
} catch (e) {
console.log(e)
}
}
@@ -69,23 +75,16 @@ async function updateSubscribeInfo() {
async function saveDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (props.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
if (props.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.post(
subscribe_config_url,
subscribeForm.value)
if (result.success)
$toast.success(`${props.type}订阅默认规则保存成功`)
else
$toast.error(`${props.type}订阅默认规则保存失败!`)
const result: { [key: string]: any } = await api.post(subscribe_config_url, subscribeForm.value)
if (result.success) $toast.success(`${props.type}订阅默认规则保存成功`)
else $toast.error(`${props.type}订阅默认规则保存失败!`)
//
emit('save')
}
catch (error) {
} catch (error) {
console.log(error)
}
}
@@ -94,17 +93,13 @@ async function saveDefaultSubscribeConfig() {
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (props.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
if (props.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data.value)
subscribeForm.value = result.data?.value ?? ''
}
catch (error) {
if (result.data.value) subscribeForm.value = result.data?.value ?? ''
} catch (error) {
console.log(error)
}
}
@@ -116,8 +111,7 @@ async function loadSites() {
//
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -125,10 +119,9 @@ async function loadSites() {
//
async function getSiteList() {
//
if (!siteList.value.length)
await loadSites()
if (!siteList.value.length) await loadSites()
const maps = siteList.value.map((item) => {
const maps = siteList.value.map(item => {
return {
title: item.name,
value: item.id,
@@ -141,14 +134,11 @@ async function getSiteList() {
//
async function getSubscribeInfo() {
try {
const result: Subscribe = await api.get(
`subscribe/${props.subid}`,
)
const result: Subscribe = await api.get(`subscribe/${props.subid}`)
subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
}
catch (e) {
} catch (e) {
console.log(e)
}
}
@@ -156,16 +146,13 @@ async function getSubscribeInfo() {
//
async function removeSubscribe() {
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.subid}`,
)
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
if (result.success) {
//
emit('remove')
}
}
catch (e) {
} catch (e) {
console.log(e)
}
}
@@ -256,31 +243,27 @@ const effectOptions = ref([
onMounted(() => {
getSiteList()
if (props.subid)
getSubscribeInfo()
if (props.subid) getSubscribeInfo()
if (props.default)
queryDefaultSubscribeConfig()
if (props.default) queryDefaultSubscribeConfig()
})
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<VCard
:title="`${props.default ? `设置${props.type}默认订阅规则` : `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`}`"
:title="`${
props.default
? `${props.type}默认订阅规则`
: `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `${subscribeForm.season}` : ''}`
}`"
class="rounded-t"
>
<VCardText class="pt-2">
<DialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VCol cols="12" md="8">
<VTextField
v-if="!props.default"
v-model="subscribeForm.keyword"
@@ -288,11 +271,7 @@ onMounted(() => {
hint="设定搜索关键词后将使用此关键词搜索站点资源否则自动使用themoviedb中的名称搜索"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
@@ -300,11 +279,7 @@ onMounted(() => {
hint="设定剧集的总集数以应对themoviedb中剧集信息未维护完整导致提前结束订阅的情况"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
@@ -314,62 +289,32 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
/>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.quality" label="质量" :items="qualityOptions" />
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
/>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.resolution" label="分辨率" :items="resolutionOptions" />
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
/>
<VCol cols="12" md="4">
<VSelect v-model="subscribeForm.effect" label="特效" :items="effectOptions" />
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
hint="支持正则表达式,多个关键字用 | 分隔表示或"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
@@ -381,9 +326,7 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
>
<VCol cols="12">
<VTextField
v-model="subscribeForm.save_path"
label="保存路径"
@@ -392,39 +335,35 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
hint="开启后不管媒体库是否存在,均会根据洗版优先级进行过滤下载,直到下载到了最高优先级的资源为止"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VCol cols="12" md="4">
<VSwitch
v-model="subscribeForm.search_imdbid"
label="使用 ImdbID 搜索"
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
/>
</VCol>
<VCol v-if="props.default" cols="12" md="4">
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn v-if="!props.default" color="error" @click="removeSubscribe">
取消订阅
</VBtn>
<VBtn v-if="!props.default" color="error" @click="removeSubscribe"> 取消订阅 </VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
>
<VBtn variant="tonal" @click=";`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`">
保存
</VBtn>
</VCardActions>

View File

@@ -0,0 +1,210 @@
<script lang="ts" setup>
import api from '@/api'
import { Subscribe } from '@/api/types'
import { formatDateDifference } from '@core/utils/formatters'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
type: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'save'])
// 订阅历史列表
const historyList = ref<Subscribe[]>([])
// 当前加载数据
const currData = ref<Subscribe[]>([])
// 当前页
const currentPage = ref(1)
// 每页数量
const pageSize = ref(30)
// 是否加载中
const loading = ref(false)
// 是否加载完成
const isRefreshed = ref(false)
// 进度框
const progressDialog = ref(false)
// 进度文字
const progressText = ref('正在重新订阅...')
// 调用API查询列表
async function loadHistory({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 调用API查询列表
try {
// 设置加载中
loading.value = true
currData.value = await api.get(`subscribe/history/${props.type}`, {
params: {
page: currentPage.value,
count: pageSize.value,
},
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
historyList.value = [...historyList.value, ...currData.value]
// 页码+1
currentPage.value++
// 返回加载成功
done('ok')
}
// 取消加载中
loading.value = false
} catch (e) {
console.error(e)
// 返回加载失败
done('error')
}
}
// 重新订阅
async function reSubscribe(item: Subscribe) {
if (item.type === '电影') progressText.value = `正在重新订阅 ${item.name} ...`
else progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: { [key: string]: any } = await api.post('subscribe', item)
if (result.success) {
emit('save')
}
} catch (e) {
console.error(e)
}
progressDialog.value = false
}
// 删除记录
async function deleteHistory(item: Subscribe) {
try {
const result: { [key: string]: any } = await api.delete(`subscribe/history/${item.id}`)
if (result.success) {
historyList.value = historyList.value.filter(i => i.id !== item.id)
}
} catch (e) {
console.error(e)
}
}
// 弹出菜单
const dropdownItems = ref([
{
title: '重新订阅',
value: 1,
color: '',
props: {
prependIcon: 'mdi-redo',
click: reSubscribe,
},
},
{
title: '删除',
value: 2,
color: 'error',
props: {
prependIcon: 'mdi-delete',
click: deleteHistory,
},
},
])
</script>
<template>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard class="mx-auto" width="100%">
<VCardItem class="pb-0">
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<DialogCloseBtn
@click="
() => {
emit('close')
}
"
/>
<VList lines="two">
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
<template #loading>
<LoadingBanner />
</template>
<template #empty />
<template v-for="(item, i) in historyList" :key="i">
<VListItem>
<template #prepend>
<VImg
height="75"
width="50"
:src="item.poster"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</template>
<VListItemTitle v-if="item.type == '电视剧'">
{{ item.name }} <span class="text-sm"> {{ item.season }} </span>
</VListItemTitle>
<VListItemTitle v-else>
{{ item.name }}
</VListItemTitle>
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
<template #append>
<div class="me-n3">
<IconBtn>
<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.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>
</VInfiniteScroll>
</VList>
</VCard>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</VDialog>
</template>

View File

@@ -4,12 +4,13 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios'
import { useToast } from 'vue-toast-notification'
import ReorganizeForm from '../form/ReorganizeForm.vue'
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
//
const inProps = defineProps({
@@ -77,14 +78,10 @@ const nameTestDialog = ref(false)
const defer = (_: number) => true
//
const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
)
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)))
//
const files = computed(() =>
items.value.filter(item => item.type === 'file' && item.basename.includes(filter.value)),
)
const files = computed(() => items.value.filter(item => item.type === 'file' && item.basename.includes(filter.value)))
//
const isDir = computed(() => inProps.path?.endsWith('/'))
@@ -113,7 +110,7 @@ async function load() {
method: inProps.endpoints?.list.method || 'get',
}
//
items.value = await axiosInstance.value.request(config) ?? []
items.value = (await axiosInstance.value.request(config)) ?? []
emit('loading', false)
loading.value = false
}
@@ -122,9 +119,7 @@ async function load() {
async function deleteItem(item: FileItem) {
const confirmed = await createConfirm({
title: '确认',
content: `是否确认删除${
item.type === 'dir' ? '目录' : '文件'
} ${item.basename}`,
content: `是否确认删除${item.type === 'dir' ? '目录' : '文件'} ${item.basename}`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
@@ -161,8 +156,7 @@ function changePath(_path: string) {
//
function download(path: string) {
if (!path)
return
if (!path) return
const token = store.state.auth.token
const url_path = inProps.endpoints?.download.url
.replace(/{storage}/g, storage.value)
@@ -174,8 +168,7 @@ function download(path: string) {
//
function getImgLink(path: string) {
if (!path)
return ''
if (!path) return ''
const token = store.state.auth.token
const url_path = inProps.endpoints?.image.url
.replace(/{storage}/g, storage.value)
@@ -261,11 +254,9 @@ async function recognize(path: string) {
})
//
progressDialog.value = false
if (!nameTestResult.value)
$toast.error(`${path} 识别失败!`)
if (!nameTestResult.value) $toast.error(`${path} 识别失败!`)
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -283,12 +274,9 @@ async function scrape(path: string) {
})
//
progressDialog.value = false
if (!result.success)
$toast.error(result.message)
else
$toast.success(`${path}削刮完成!`)
}
catch (error) {
if (!result.success) $toast.error(result.message)
else $toast.success(`${path}削刮完成!`)
} catch (error) {
console.error(error)
}
}
@@ -303,7 +291,8 @@ const dropdownItems = ref([
recognize(_item.path || '')
},
},
}, {
},
{
title: '刮削',
value: 2,
props: {
@@ -312,7 +301,8 @@ const dropdownItems = ref([
scrape(_item.path || '')
},
},
}, {
},
{
title: '重命名',
value: 3,
props: {
@@ -361,49 +351,26 @@ onMounted(() => {
/>
<VSpacer v-if="isFile" />
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
<VIcon color="primary">
mdi-text-recognition
</VIcon>
<VIcon color="primary"> mdi-text-recognition </VIcon>
</IconBtn>
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
<VIcon color="primary">
mdi-download
</VIcon>
<VIcon color="primary"> mdi-download </VIcon>
</IconBtn>
<IconBtn v-if="!isFile" @click="load">
<VIcon color="primary">
mdi-refresh
</VIcon>
<VIcon color="primary"> mdi-refresh </VIcon>
</IconBtn>
</VToolbar>
<VCardText
v-if="loading"
class="text-center flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
<VCardText v-if="loading" class="text-center flex flex-col items-center">
<VProgressCircular size="48" indeterminate color="primary" />
</VCardText>
<VCardText
v-if="!path"
class="grow d-flex justify-center align-center grey--text"
>
选择目录或文件
</VCardText>
<VCardText
v-else-if="isFile && !isImage"
class="text-center break-all"
>
<strong>{{ items[0]?.name }}</strong><br>
大小{{ formatBytes(items[0]?.size || 0) }}<br>
<VCardText v-if="!path" class="grow d-flex justify-center align-center grey--text"> 选择目录或文件 </VCardText>
<VCardText v-else-if="isFile && !isImage" class="text-center break-all">
<strong>{{ items[0]?.name }}</strong
><br />
大小{{ formatBytes(items[0]?.size || 0) }}<br />
修改时间{{ formatTime(items[0]?.modify_time || 0) }}
</VCardText>
<VCardText
v-else-if="isFile && isImage"
class="grow d-flex justify-center align-center"
>
<VCardText v-else-if="isFile && isImage" class="grow d-flex justify-center align-center">
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
</VCardText>
<VCardText v-else-if="dirs.length || files.length" class="p-0">
@@ -412,13 +379,12 @@ onMounted(() => {
<template #default="{ item }">
<VHover>
<template #default="hover">
<VListItem
v-bind="hover.props"
class="px-3 pe-1"
@click="changePath(item.path)"
>
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="changePath(item.path)">
<template #prepend>
<VIcon v-if="inProps.icons && item.extension" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
<VIcon
v-if="inProps.icons && item.extension"
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
/>
<VIcon v-else icon="mdi-folder-outline" />
</template>
<VListItemTitle v-text="item.name" />
@@ -427,13 +393,8 @@ onMounted(() => {
</VListItemSubtitle>
<template #append>
<IconBtn class="d-sm-none">
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
@@ -495,79 +456,41 @@ onMounted(() => {
</VVirtualScroll>
</VList>
</VCardText>
<VCardText
v-else-if="filter"
class="grow d-flex justify-center align-center grey--text py-5"
>
<VCardText v-else-if="filter" class="grow d-flex justify-center align-center grey--text py-5">
没有目录或文件
</VCardText>
<VCardText
v-else-if="!loading"
class="grow d-flex justify-center align-center grey--text py-5"
>
空目录
</VCardText>
<VCardText v-else-if="!loading" class="grow d-flex justify-center align-center grey--text py-5"> 空目录 </VCardText>
</VCard>
<!-- 重命名弹窗 -->
<VDialog
v-if="renamePopper"
v-model="renamePopper"
max-width="50rem"
>
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="50rem">
<VCard title="重命名">
<VCardText>
<VTextField v-model="newName" label="名称" />
</VCardText>
<VCardActions>
<VBtn depressed @click="renamePopper = false">
取消
</VBtn>
<VBtn depressed @click="renamePopper = false"> 取消 </VBtn>
<VSpacer />
<VBtn
:disabled="!newName"
depressed
variant="tonal"
@click="rename"
>
重命名
</VBtn>
<VBtn :disabled="!newName" depressed variant="tonal" @click="rename"> 重命名 </VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<ReorganizeForm
<ReorganizeDialog
v-if="transferPopper"
v-model="transferPopper"
:path="currentItem?.path"
@done="transferPopper = false; load()"
@done="
() => {
transferPopper = false
load()
}
"
@close="transferPopper = false"
/>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- 识别结果对话框 -->
<VDialog
v-if="nameTestDialog"
v-model="nameTestDialog"
width="50rem"
>
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" width="50rem">
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
@@ -579,14 +502,20 @@ onMounted(() => {
<style lang="scss" scoped>
.v-card {
block-size: 100%;
block-size: 100%;
}
.v-toolbar{
.v-toolbar {
background: rgb(var(--v-table-header-background));
}
.virtual-scroll-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.virtual-scroll-div {
block-size: calc(100vh - 17rem);
}
}
</style>

View File

@@ -1,39 +0,0 @@
<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>

View File

@@ -1,5 +1,11 @@
<script lang="ts" setup>
import { type PropType, ref } from 'vue'
import { isNullOrEmptyObject } from '@/@core/utils'
import api from '@/api'
import { type PropType } from 'vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
// 定议外部事件
const emit = defineEmits(['action'])
// 组件接口
interface RenderProps {
@@ -7,7 +13,9 @@ interface RenderProps {
text: string
html: string
content?: any
slots?: any
props?: any
events?: any
}
// 输入参数
@@ -15,33 +23,78 @@ const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
// 配置元素
const formItem = ref<RenderProps>(elementProps.config ?? {
component: 'div',
text: '',
html: '',
props: {},
content: [],
// 进度框
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在处理...')
// 元素API事件响应
async function commonAction(api_path: string, method: string, params = {}) {
if (!api_path || !method) return
progressDialog.value = true
try {
if (method.toUpperCase() === 'GET') {
await api.get(api_path, {
params: params,
})
} else {
await api.post(api_path, params)
}
emit('action')
} catch (error) {
console.error(error)
}
progressDialog.value = false
}
// 组装事件
let componentEvents = reactive<{ [key: string]: any }>({})
watchEffect(() => {
if (!isNullOrEmptyObject(elementProps.config?.events)) {
for (const key in elementProps.config?.events) {
const attr = elementProps.config?.events[key]
const func = async () => {
await commonAction(attr['api'], attr['method'], attr['params'])
}
componentEvents[key] = func
}
}
})
</script>
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-bind="formItem.props"
:is="elementProps.config?.component"
v-if="!elementProps.config?.html"
v-bind="elementProps.config?.props"
v-on="componentEvents"
>
{{ formItem.text }}
{{ elementProps.config?.text }}
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
<slot :name="name" v-bind="_props">
<PageRender
v-for="(slotItem, slotIndex) in content || []"
:key="slotIndex"
:config="slotItem"
@action="emit('action')"
/>
</slot>
</template>
<PageRender
v-for="(innerItem, innerIndex) in (formItem.content || [])"
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
:key="innerIndex"
:config="innerItem"
@action="emit('action')"
/>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"
v-html="elementProps.config?.html"
v-on="componentEvents"
/>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</template>

View File

@@ -1,12 +1,6 @@
<script lang="ts" setup>
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
// 输入参数
const props = defineProps({
linkurl: String,
title: String,
})
// 元素
const slideview_content = ref()
// 分页切换状态
@@ -95,7 +89,7 @@ onActivated(() => {
<template>
<div class="flex justify-between mt-3">
<slot name="title">
<SlideViewTitle v-bind="props" />
<SlideViewTitle />
</slot>
<div v-if="disabled !== 3" class="me-1 d-none d-md-flex">
<VBtn

View File

@@ -1,9 +1,7 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
linkurl: String,
title: String,
})
const props = inject('rankingPropsKey')
</script>
<template>

View File

@@ -3,7 +3,7 @@
const router = useRouter()
// 搜索词
const searchWord = ref<string>('')
const searchWord = ref(null)
// 搜索弹窗
const searchDialog = ref(false)
@@ -11,20 +11,31 @@ const searchDialog = ref(false)
// ref
const searchWordInput = ref<HTMLElement | null>(null)
// 当前的搜索类型 media/person
const searchType = ref('media')
// 搜索提示词列表
const searchHintList = ref<string[]>([])
// Search
function search() {
if (!searchWord.value)
return
if (!searchWord.value) return
if (!searchHintList.value.includes(searchWord.value)) searchHintList.value.push(searchWord.value)
searchDialog.value = false
router.push({
path: '/browse/media/search',
query: {
title: searchWord.value,
type: searchType.value,
},
})
}
// 切换搜索类型
function switchSearchType() {
searchType.value = searchType.value === 'media' ? 'person' : 'media'
}
// 打开搜索弹窗
function openSearchDialog() {
searchDialog.value = true
@@ -36,25 +47,22 @@ function openSearchDialog() {
<template>
<!-- 👉 Search Button -->
<div
class="d-flex align-center cursor-pointer"
style="user-select: none;"
>
<VDialog
v-model="searchDialog"
max-width="50rem"
transition="dialog-top-transition"
>
<div class="d-flex align-center cursor-pointer" style="user-select: none">
<VDialog v-model="searchDialog" max-width="50rem" transition="dialog-top-transition">
<!-- Dialog Content -->
<VCard title="搜索">
<VCardText>
<VRow>
<VCol cols="12">
<VTextField
<VCombobox
ref="searchWordInput"
v-model="searchWord"
label="电影、电视剧名称"
:items="searchHintList"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
@keydown.enter="search"
@click:prepend-inner="switchSearchType"
clearable
/>
</VCol>
</VRow>
@@ -62,45 +70,40 @@ function openSearchDialog() {
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="search"
>
搜索
</VBtn>
<VBtn variant="tonal" @click="search"> 搜索 </VBtn>
</VCardActions>
</VCard>
</VDialog>
</div>
<!-- 👉 Search Icon -->
<IconBtn
class="d-lg-none"
@click="openSearchDialog"
>
<IconBtn class="d-md-none" @click="openSearchDialog">
<VIcon icon="mdi-magnify" />
</IconBtn>
<!-- 👉 Search Textfield -->
<span class="w-1/5">
<VTextField
<span class="w-full me-3">
<VCombobox
key="search_navbar"
v-model="searchWord"
class="d-none d-lg-block text-disabled search-box"
:items="searchHintList"
class="d-none d-md-block text-disabled search-box"
density="compact"
variant="solo"
label="搜索电影、电视剧"
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
append-inner-icon="mdi-magnify"
single-line
hide-details
flat
rounded
@click:append-inner="search"
@click:prepend-inner="switchSearchType"
@keydown.enter="search"
/>
</span>
</template>
<style lang="scss">
.search-box div.v-input__control div[role="textbox"] {
.search-box div.v-input__control div[role='textbox'] {
border: 1px solid rgb(var(--v-theme-background));
}
</style>

View File

@@ -7,6 +7,10 @@ import ModuleTestView from '@/views/system/ModuleTestView.vue'
import MessageView from '@/views/system/MessageView.vue'
import store from '@/store'
import api from '@/api'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// App捷径
const appsMenu = ref(false)
@@ -63,8 +67,7 @@ async function sendMessage() {
user_message.value = ''
sendButtonDisabled.value = false
scrollMessageToEnd()
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -88,10 +91,7 @@ onMounted(() => {
>
<!-- Menu Activator -->
<template #activator="{ props }">
<IconBtn
class="me-2"
v-bind="props"
>
<IconBtn class="me-2" v-bind="props">
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
</IconBtn>
</template>
@@ -107,132 +107,61 @@ onMounted(() => {
</VCardItem>
<div class="ps ps--active-y">
<VRow class="ma-0 mt-n1">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
>
<VListItem
class="pa-4"
@click="nameTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e">
<VListItem class="pa-4" @click="nameTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-text-recognition" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
识别
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">识别</h6>
<span class="text-sm">名称识别测试</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="ruleTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="ruleTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-filter-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
优先级
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">优先级</h6>
<span class="text-sm">优先级规则测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="loggingDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="loggingDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-file-document-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
日志
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">日志</h6>
<span class="text-sm">实时日志</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="netTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon" @click="() => {}">
<VListItem class="pa-4" @click="netTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-network-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
网络
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">网络</h6>
<span class="text-sm">网速连通性测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="systemTestDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="systemTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
系统
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">系统</h6>
<span class="text-sm">健康检查</span>
</VListItem>
</VCol>
<VCol
cols="6"
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
@click="() => {}"
>
<VListItem
class="pa-4"
@click="messageDialog = true"
>
<VAvatar
size="48"
variant="tonal"
>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="messageDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-message-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
消息
</h6>
<h6 class="text-base font-weight-medium mt-2 mb-0">消息</h6>
<span class="text-sm">消息中心</span>
</VListItem>
</VCol>
@@ -241,37 +170,30 @@ onMounted(() => {
</VCard>
</VMenu>
<!-- 名称测试弹窗 -->
<VDialog
v-if="nameTestDialog"
v-model="nameTestDialog"
max-width="50rem"
>
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="50rem" scrollable>
<VCard title="名称识别测试">
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<VCardText>
<NameTestView />
</VCardItem>
</VCardText>
</VCard>
</VDialog>
<!-- 网络测试弹窗 -->
<VDialog
v-if="netTestDialog"
v-model="netTestDialog"
max-width="35rem"
>
<VDialog v-if="netTestDialog" v-model="netTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VCard title="网络测试">
<DialogCloseBtn @click="netTestDialog = false" />
<VCardItem>
<VCardText>
<NetTestView />
</VCardItem>
</VCardText>
</VCard>
</VDialog>
<!-- 实时日志弹窗 -->
<VDialog
v-if="loggingDialog"
v-model="loggingDialog"
class="w-full lg:w-4/5"
scrollable
max-width="70rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<DialogCloseBtn @click="loggingDialog = false" />
@@ -279,7 +201,9 @@ onMounted(() => {
<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">
<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>
@@ -292,12 +216,7 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 规则测试弹窗 -->
<VDialog
v-if="ruleTestDialog"
v-model="ruleTestDialog"
max-width="50rem"
scrollable
>
<VDialog v-if="ruleTestDialog" v-model="ruleTestDialog" max-width="50rem" scrollable>
<VCard title="优先级测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VCardText>
@@ -306,12 +225,7 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 系统健康检查弹窗 -->
<VDialog
v-if="systemTestDialog"
v-model="systemTestDialog"
max-width="50rem"
scrollable
>
<VDialog v-if="systemTestDialog" v-model="systemTestDialog" max-width="35rem" max-height="85vh" scrollable>
<VCard title="系统健康检查">
<DialogCloseBtn @click="systemTestDialog = false" />
<VCardText>
@@ -325,6 +239,7 @@ onMounted(() => {
v-model="messageDialog"
max-width="60rem"
scrollable
:fullscreen="!display.mdAndUp.value"
>
<VCard title="消息中心">
<DialogCloseBtn @click="messageDialog = false" />
@@ -345,13 +260,7 @@ onMounted(() => {
@keydown.enter="sendMessage"
>
<template #append>
<VBtn
color="primary"
:disabled="sendButtonDisabled"
@click="sendMessage"
>
发送
</VBtn>
<VBtn color="primary" :disabled="sendButtonDisabled" @click="sendMessage"> 发送 </VBtn>
</template>
</VTextField>
</VCardItem>

View File

@@ -5,6 +5,7 @@ import { useToast } from 'vue-toast-notification'
import router from '@/router'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
// Vuex Store
const store = useStore()
@@ -56,8 +57,7 @@ async function restart() {
$toast.error(result.message)
return
}
}
catch (error) {
} catch (error) {
console.error(error)
}
// 注销
@@ -72,53 +72,33 @@ const avatar = store.state.auth.avatar
</script>
<template>
<VAvatar
class="cursor-pointer"
color="primary"
variant="tonal"
>
<VAvatar class="cursor-pointer" color="primary" variant="tonal">
<VImg :src="avatar ?? avatar1" />
<!-- SECTION Menu -->
<VMenu
activator="parent"
width="230"
location="bottom end"
offset="14px"
>
<VMenu activator="parent" width="230" location="bottom end" offset="14px">
<VList>
<!-- 👉 User Avatar & Name -->
<VListItem>
<template #prepend>
<VListItemAction start>
<VAvatar
color="primary"
variant="tonal"
>
<VAvatar color="primary" variant="tonal">
<VImg :src="avatar ?? avatar1" />
</VAvatar>
</VListItemAction>
</template>
<VListItemTitle class="font-weight-semibold">
{{ superUser ? "管理员" : "普通用户" }}
{{ superUser ? '管理员' : '普通用户' }}
</VListItemTitle>
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<!-- 👉 Profile -->
<VListItem
v-if="superUser"
link
to="setting"
>
<VListItem v-if="superUser" link to="setting">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-account-outline"
size="22"
/>
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
</template>
<VListItemTitle>设定</VListItemTitle>
@@ -128,32 +108,18 @@ const avatar = store.state.auth.avatar
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem
v-if="superUser"
@click="restart"
>
<VListItem v-if="superUser" @click="restart">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
<VIcon class="me-2" icon="mdi-restart" size="22" />
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
target="_blank"
>
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-help-circle-outline"
size="22"
/>
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
</template>
<VListItemTitle>帮助</VListItemTitle>
@@ -162,11 +128,7 @@ const avatar = store.state.auth.avatar
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-logout"
size="22"
/>
<VIcon class="me-2" icon="mdi-logout" size="22" />
</template>
<VListItemTitle>注销</VListItemTitle>
@@ -176,21 +138,5 @@ const avatar = store.state.auth.avatar
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<VDialog
v-model="progressDialog"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
正在重启 ...
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
</template>

View File

@@ -17,7 +17,13 @@ import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
import 'vue3-perfect-scrollbar/style.css';
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts()
// 创建Vue实例
@@ -26,6 +32,7 @@ const app = createApp(App)
// 注册全局组件
app.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts)
.component('VDialogCloseBtn', DialogCloseBtn)
// 注册插件
app

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import api from '@/api'
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
@@ -11,14 +13,16 @@ const props = defineProps({
const route = useRoute()
// 标题
const title = route.query?.title?.toString()
let title = route.query?.title?.toString()
// 类型
const type = route.query?.type?.toString()
if (type === 'person') title = '演员:' + title
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths))
return paths.join('/')
else
return paths
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
</script>
@@ -26,14 +30,15 @@ function getApiPath(paths: string[] | string) {
<div>
<div v-if="title" class="mt-3 md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1 mx-0">
<h2 class="mb-4 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0" data-testid="page-header">
<h2
class="mb-4 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0"
data-testid="page-header"
>
<span class="text-moviepilot">{{ title }}</span>
</h2>
</div>
</div>
<MediaCardListView
:apipath="getApiPath(props.paths || '')"
:params="route.query"
/>
<PersonCardListView v-if="type === 'person'" :apipath="getApiPath(props.paths || '')" :params="route.query" />
<MediaCardListView v-else :apipath="getApiPath(props.paths || '')" :params="route.query" />
</div>
</template>

View File

@@ -9,6 +9,12 @@ 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'
import api from '@/api'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// 仪表盘配置
const dashboard_names = {
@@ -40,146 +46,86 @@ const default_config = {
playing: true,
latest: true,
}
// 初始化默认值
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
if (Object.keys(config.value).length === 0) {
if (isNullOrEmptyObject(config.value)) {
config.value = default_config
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
}
// 设置项目
function setDashboardConfig() {
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
const data = JSON.stringify(config.value)
localStorage.setItem('MP_DASHBOARD', data)
// 保存到服务端
api.post('/user/config/Dashboard', data, {
headers: {
'Content-Type': 'application/json',
},
})
dialog.value = false
}
</script>
<template>
<!-- 底部操作按钮 -->
<VFab
icon="mdi-view-dashboard-edit"
location="bottom end"
size="x-large"
fixed
app
appear
@click="dialog = true"
/>
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
<VRow class="match-height">
<VCol
v-if="config.storage"
cols="12"
md="4"
>
<VCol v-if="config.storage" cols="12" md="4">
<AnalyticsStorage />
</VCol>
<VCol
v-if="config.mediaStatistic"
cols="12"
md="8"
>
<VCol v-if="config.mediaStatistic" cols="12" md="8">
<AnalyticsMediaStatistic />
</VCol>
<VCol
v-if="config.weeklyOverview"
cols="12"
md="4"
>
<VCol v-if="config.weeklyOverview" cols="12" md="4">
<AnalyticsWeeklyOverview />
</VCol>
<VCol
v-if="config.speed"
cols="12"
md="4"
>
<VCol v-if="config.speed" cols="12" md="4">
<AnalyticsSpeed />
</VCol>
<VCol
v-if="config.scheduler"
cols="12"
md="4"
>
<VCol v-if="config.scheduler" cols="12" md="4">
<AnalyticsScheduler />
</VCol>
<VCol
v-if="config.cpu"
cols="12"
md="6"
>
<VCol v-if="config.cpu" cols="12" md="6">
<AnalyticsCpu />
</VCol>
<VCol
v-if="config.memory"
cols="12"
md="6"
>
<VCol v-if="config.memory" cols="12" md="6">
<AnalyticsMemory />
</VCol>
<VCol
v-if="config.library"
cols="12"
>
<VCol v-if="config.library" cols="12">
<MediaServerLibrary />
</VCol>
<VCol
v-if="config.playing"
cols="12"
>
<VCol v-if="config.playing" cols="12">
<MediaServerPlaying />
</VCol>
<VCol
v-if="config.latest"
cols="12"
>
<VCol v-if="config.latest" cols="12">
<MediaServerLatest />
</VCol>
</VRow>
<!-- 弹窗根据配置生成选项 -->
<VDialog
v-model="dialog"
max-width="600"
scrollable
>
<VDialog v-model="dialog" max-width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
<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 v-for="(item, key) in dashboard_names" :key="key" cols="12" md="4" sm="4">
<VCheckbox v-model="config[key]" :label="dashboard_names[key]" />
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VBtn
color="primary"
@click="dialog = false"
>
取消
</VBtn>
<VBtn color="primary" @click="dialog = false"> 取消 </VBtn>
<VSpacer />
<VBtn
color="primary"
variant="tonal"
@click="setDashboardConfig"
>
保存
</VBtn>
<VBtn color="primary" variant="tonal" @click="setDashboardConfig"> 保存 </VBtn>
</VCardActions>
</VCard>
</vdialog>
</VDialog>
</template>

View File

@@ -1,10 +1,15 @@
<script setup lang="ts">
import { debounce } from 'lodash'
import { VForm } from 'vuetify/components/VForm'
import { useStore } from 'vuex'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import router from '@/router'
import logo from '@images/logo.png'
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
const { global: globalTheme } = useTheme()
// Vuex Store
const store = useStore()
@@ -48,9 +53,8 @@ async function fetchBackgroundImage() {
console.log(error)
})
}
// 查询是否开启双重验证
async function fetchOTP() {
const fetchOTP = debounce(async () => {
const userid = usernameInput.value?.value
if (!userid) {
isOTP.value = false
@@ -64,6 +68,53 @@ async function fetchOTP() {
.catch((error: any) => {
console.log(error)
})
}, 500)
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem('MP_DASHBOARD')) {
localStorage.setItem('MP_DASHBOARD', data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem('MP_DASHBOARD')) {
return
}
await loadDashboardConfig()
}
// 获取用户主题配置
async function fetchThemeConfig() {
const response = await api.get('/user/config/theme')
if (response && response.data && response.data.value) {
return response.data.value
}
return null
}
// 生效主题
async function setTheme() {
let themeValue = (await fetchThemeConfig()) || localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 存储主题到本地
localStorage.setItem('theme', themeValue)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
async function afterLogin() {
// 生效主题配置
await setTheme()
// 尝试加载用户监控面板配置(本地无配置时才加载)
await tryLoadDashboardConfig()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
}
// 登录获取token事件
@@ -103,21 +154,16 @@ function login() {
store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
// 登录后处理
afterLogin()
})
.catch((error: any) => {
// 登录失败,显示错误提示
if (!error.response)
errorMessage.value = '登录失败,请检查网络连接'
else if (error.response.status === 401)
errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
else if (error.response.status === 403)
errorMessage.value = '登录失败,您没有权限访问'
else if (error.response.status === 500)
errorMessage.value = '登录失败,服务器错误'
else
errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
if (!error.response) errorMessage.value = '登录失败,请检查网络连接'
else if (error.response.status === 401) errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
else if (error.response.status === 403) errorMessage.value = '登录失败,您没有权限访问'
else if (error.response.status === 500) errorMessage.value = '登录失败,服务器错误'
else errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
})
}
@@ -130,8 +176,7 @@ onMounted(() => {
// 如果token存在且保持登录状态为true则跳转到首页
if (token && remember) {
router.push('/')
}
else {
} else {
// 获取背景图片
fetchBackgroundImage()
}
@@ -160,16 +205,11 @@ onMounted(() => {
</div>
</template>
<VCardTitle class="font-weight-semibold text-2xl text-uppercase">
MoviePilot
</VCardTitle>
<VCardTitle class="font-weight-semibold text-2xl text-uppercase"> MoviePilot </VCardTitle>
</VCardItem>
<VCardText>
<VForm
ref="refForm"
@submit.prevent="() => {}"
>
<VForm ref="refForm" @submit.prevent="() => {}">
<VRow>
<!-- username -->
<VCol cols="12">
@@ -188,42 +228,22 @@ onMounted(() => {
v-model="form.password"
label="密码"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol cols="12">
<VTextField
v-if="isOTP"
v-model="form.otp_password"
label="双重验证码"
type="input"
/>
<VTextField v-if="isOTP" v-model="form.otp_password" label="双重验证码" type="input" />
<!-- remember me checkbox -->
<div class="d-flex align-center justify-space-between flex-wrap">
<VCheckbox
v-model="form.remember"
label="保持登录"
required
/>
<VCheckbox v-model="form.remember" label="保持登录" required />
</div>
</VCol>
<VCol cols="12">
<!-- login button -->
<VBtn
block
type="submit"
@click="login"
>
登录
</VBtn>
<div
v-if="errorMessage"
class="text-error mt-2 text-shadow"
>
<VBtn block type="submit" @click="login"> 登录 </VBtn>
<div v-if="errorMessage" class="text-error mt-2 text-shadow">
{{ errorMessage }}
</div>
</VCol>
@@ -236,7 +256,7 @@ onMounted(() => {
</template>
<style lang="scss">
@use "@core/scss/pages/page-auth.scss";
@use '@core/scss/pages/page-auth.scss';
.v-card-item__prepend {
padding-inline-end: 0 !important;

View File

@@ -7,15 +7,15 @@ const route = useRoute()
// Person Id
const personid = route.query?.personid?.toString()
// 来源
const source = route.query?.source?.toString()
// 类型
const type = route.query?.type?.toString()
</script>
<template>
<div>
<PersonDetailView
:personid="personid"
:type="type"
/>
<PersonDetailView :personid="personid" :type="type" :source="source" />
</div>
</template>

View File

@@ -1,85 +1,82 @@
<script setup lang="ts">
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
const viewList = reactive<{apipath: string, linkurl: string, title: string}[]>([
{
apipath: 'tmdb/trending',
linkurl: "/browse/tmdb/trending?title=流行趋势",
title: "流行趋势",
},
{
apipath: "douban/showing",
linkurl: "/browse/douban/showing?title=正在热映",
title: "正在热映"
},
{
apipath: "bangumi/calendar",
linkurl: "/browse/bangumi/calendar?title=Bangumi每日放送",
title: "Bangumi每日放送"
},
{
apipath: "tmdb/movies",
linkurl: "/browse/tmdb/movies?title=热门电影",
title: "热门电影"
},
{
apipath: "tmdb/tvs?with_original_language=zh|en|ja|ko",
linkurl: "/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧",
title: "热门电视剧"
},
{
apipath: "douban/movie_hot",
linkurl: "/browse/douban/movie_hot?title=热门电影",
title: "热门电影"
},
{
apipath: "douban/tv_hot",
linkurl: "/browse/douban/tv_hot?title=热门电视剧",
title: "热门电视剧"
},
{
apipath: "douban/tv_animation",
linkurl: "/browse/douban/tv_animation?title=热门动漫",
title: "热门动漫"
},
{
apipath: "douban/movies",
linkurl: "/browse/douban/movies?title=最新电影",
title: "最新电影"
},
{
apipath: "douban/tvs",
linkurl: "/browse/douban/tvs?title=最新电视剧",
title: "最新电视剧"
},
{
apipath: "douban/movie_top250",
linkurl: "/browse/douban/movie_top250?title=电影TOP250",
title: "电影TOP250"
},
{
apipath: "douban/tv_weekly_chinese",
linkurl: "/browse/douban/tv_weekly_chinese?title=国产剧集榜",
title: "国产剧集榜"
},
{
apipath: "douban/tv_weekly_global",
linkurl: "/browse/douban/tv_weekly_global?title=全球剧集榜",
title: "全球剧集榜"
}
])
</script>
<template>
<div>
<MediaCardSlideView
apipath="tmdb/trending"
linkurl="/browse/tmdb/trending?title=流行趋势"
title="流行趋势"
/>
<MediaCardSlideView
apipath="douban/showing"
linkurl="/browse/douban/showing?title=正在热映"
title="正在热映"
/>
<MediaCardSlideView
apipath="bangumi/calendar"
linkurl="/browse/bangumi/calendar?title=Bangumi每日放送"
title="Bangumi每日放送"
/>
<MediaCardSlideView
apipath="tmdb/movies"
linkurl="/browse/tmdb/movies?title=热门电影"
title="热门电影"
/>
<MediaCardSlideView
apipath="tmdb/tvs?with_original_language=zh|en|ja|ko"
linkurl="/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧"
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/movie_hot"
linkurl="/browse/douban/movie_hot?title=热门电影"
title="热门电影"
/>
<MediaCardSlideView
apipath="douban/tv_hot"
linkurl="/browse/douban/tv_hot?title=热门电视剧"
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"
title="热门动漫"
/>
<MediaCardSlideView
apipath="douban/movies"
linkurl="/browse/douban/movies?title=最新电影"
title="最新电影"
/>
<MediaCardSlideView
apipath="douban/tvs"
linkurl="/browse/douban/tvs?title=最新电视剧"
title="最新电视剧"
/>
<MediaCardSlideView
apipath="douban/movie_top250"
linkurl="/browse/douban/movie_top250?title=电影TOP250"
title="电影TOP250"
/>
<MediaCardSlideView
apipath="douban/tv_weekly_chinese"
linkurl="/browse/douban/tv_weekly_chinese?title=国产剧集榜"
title="国产剧集榜"
/>
<MediaCardSlideView
apipath="douban/tv_weekly_global"
linkurl="/browse/douban/tv_weekly_global?title=全球剧集榜"
title="全球剧集榜"
<MediaCardSlideView
v-for="item in viewList"
:key="item.apipath"
v-bind="item"
/>
</div>
</template>

View File

@@ -120,11 +120,12 @@ onMounted(() => {
</script>
<template>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
</div>
<LoadingBanner
v-if="!isRefreshed"
class="mt-12"
:text="progressText"
:progress="progressValue"
/>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
:error-title="errorTitle"

View File

@@ -66,10 +66,7 @@ const tabs = [
<template>
<div>
<VTabs
v-model="activeTab"
show-arrows
>
<VTabs v-model="activeTab" show-arrows>
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
@@ -77,11 +74,7 @@ const tabs = [
</VTabs>
<VDivider />
<VWindow
v-model="activeTab"
class="mt-5 disable-tab-transition"
:touch="false"
>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 用户 -->
<VWindowItem value="account">
<transition name="fade-slide" appear>

View File

@@ -12,11 +12,7 @@ const props = defineProps({
// 判断是否有滚动条
function hasScroll() {
return (
document.body.scrollHeight
- (window.innerHeight || document.documentElement.clientHeight)
> 2
)
return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
}
// 当前页码
@@ -37,8 +33,7 @@ function getParams() {
let params = {
page: page.value,
}
if (props.params)
params = { ...params, ...props.params }
if (props.params) params = { ...params, ...props.params }
return params
}
@@ -46,76 +41,66 @@ function getParams() {
// 获取列表数据
async function fetchData({ done }: { done: any }) {
try {
if (!props.apipath)
return
if (!props.apipath) return
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错
if (!hasScroll()) {
// 加载多次
while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: getParams(),
})
// 取消加载中
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('ok')
done('empty')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
}
}
else {
} else {
// 加载一次
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: getParams(),
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
}
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
}
catch (error) {
} catch (error) {
console.error(error)
// 返回加载失败
done('error')
}
@@ -123,34 +108,12 @@ async function fetchData({ done }: { done: any }) {
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll
mode="intersect"
side="end"
:items="dataList"
class="overflow-hidden"
@load="fetchData"
>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
<template #loading />
<div
v-if="dataList.length > 0"
class="grid gap-4 grid-media-card mx-3"
tabindex="0"
>
<MediaCard
v-for="data in dataList"
:key="data.tmdb_id || data.douban_id"
:media="data"
/>
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0">
<MediaCard v-for="data in dataList" :key="data.tmdb_id || data.douban_id" :media="data" />
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"

View File

@@ -11,6 +11,8 @@ const props = defineProps({
title: String,
})
provide('rankingPropsKey', reactive({...props}))
// 组件加载完成
const componentLoaded = ref(false)
@@ -39,7 +41,6 @@ onMounted(fetchData)
<template>
<SlideView
v-if="componentLoaded"
v-bind="props"
>
<template #content>
<template

View File

@@ -8,7 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
// 输入参数
const mediaProps = defineProps({
@@ -46,11 +46,6 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref<number>()
// 订阅规则
const subscribeRules = ref({
show_edit_dialog: false,
})
// 获得mediaid
function getMediaId() {
return mediaDetail.value?.tmdb_id
@@ -230,9 +225,12 @@ async function addSubscribe(season = 0) {
)
// 显示编辑弹窗
if (result.success && subscribeRules.value.show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
if (result.success) {
const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
}
catch (error) {
@@ -290,20 +288,6 @@ 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)
@@ -450,23 +434,35 @@ async function handlePlay() {
}
}
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (mediaProps.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
catch (error) {
console.log(error)
}
return false
}
onBeforeMount(() => {
getMediaDetail()
querySubscribeRules()
})
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
@@ -506,7 +502,7 @@ onBeforeMount(() => {
</span>
</div>
<div class="media-actions">
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" variant="tonal" color="info" class="mb-2">
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2">
<template #prepend>
<VIcon icon="mdi-magnify" />
</template>
@@ -532,6 +528,12 @@ onBeforeMount(() => {
</VList>
</VMenu>
</VBtn>
<VBtn v-if="(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) && !mediaDetail.imdb_id" variant="tonal" color="info" class="mb-2" @click="handleSearch('title')">
<template #prepend>
<VIcon icon="mdi-magnify" />
</template>
搜索资源
</VBtn>
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
<template #prepend>
<VIcon :icon="getSubscribeIcon" />
@@ -638,16 +640,10 @@ onBeforeMount(() => {
</VExpansionPanelTitle>
<VExpansionPanelText>
<template #default>
<div
<LoadingBanner
v-if="!seasonEpisodesInfo[season.season_number || 0]"
class="mt-3 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-3"
/>
<div class="flex flex-col justify-center divide-y divide-gray-700">
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
<div class="flex-1">
@@ -849,7 +845,7 @@ onBeforeMount(() => {
error-description="未识别到媒体信息"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
import api from '@/api'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import PersonCard from '@/components/cards/PersonCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
// 输入参数
@@ -13,11 +12,7 @@ const props = defineProps({
// 判断是否有滚动条
function hasScroll() {
return (
document.body.scrollHeight
- (window.innerHeight || document.documentElement.clientHeight)
> 2
)
return document.body.scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
}
// 当前页码
@@ -33,83 +28,80 @@ const isRefreshed = ref(false)
const dataList = ref<any>([])
const currData = ref<any>([])
// 拼装参数
function getParams() {
let params = {
page: page.value,
}
if (props.params) params = { ...params, ...props.params }
return params
}
// 获取列表数据
async function fetchData({ done }: { done: any }) {
try {
if (!props.apipath)
return
if (!props.apipath) return
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错
if (!hasScroll()) {
// 加载多次
while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: {
page: page.value,
},
params: getParams(),
})
// 取消加载中
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('ok')
done('empty')
return
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
}
}
else {
} else {
// 加载一次
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: {
page: page.value,
},
params: getParams(),
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 取消加载中
loading.value = false
}
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
}
catch (error) {
} catch (error) {
console.error(error)
// 返回加载失败
done('error')
}
@@ -117,45 +109,12 @@ async function fetchData({ done }: { done: any }) {
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll
mode="intersect"
side="end"
:items="dataList"
class="overflow-hidden"
@load="fetchData"
>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
<template #loading />
<div
v-if="dataList.length > 0 && props.type === 'tmdb'"
class="grid gap-4 grid-media-card mx-3"
tabindex="0"
>
<TmdbPersonCard
v-for="data in dataList"
:key="data.id"
:person="data"
/>
</div>
<div
v-if="dataList.length > 0 && props.type === 'douban'"
class="grid gap-4 grid-media-card mx-3"
tabindex="0"
>
<DoubanPersonCard
v-for="data in dataList"
:key="data.id"
:person="data"
/>
<template #empty />
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0">
<PersonCard v-for="data in dataList" :key="data.id" :person="data" />
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"

View File

@@ -1,9 +1,7 @@
<script lang="ts" setup>
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import PersonCard from '@/components/cards/PersonCard.vue'
import api from '@/api'
import SlideView from '@/components/slide/SlideView.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
// 输入参数
const props = defineProps({
@@ -13,6 +11,8 @@ const props = defineProps({
type: String,
})
provide('rankingPropsKey', reactive({ ...props }))
// 组件加载完成
const componentLoaded = ref(false)
@@ -22,14 +22,11 @@ const dataList = ref<any>([])
// 获取订阅列表数据
async function fetchData() {
try {
if (!props.apipath)
return
if (!props.apipath) return
dataList.value = await api.get(props.apipath)
if (dataList.value.length > 0)
componentLoaded.value = true
}
catch (error) {
if (dataList.value.length > 0) componentLoaded.value = true
} catch (error) {
console.error(error)
}
}
@@ -39,33 +36,10 @@ onMounted(fetchData)
</script>
<template>
<SlideView
v-if="componentLoaded"
v-bind="props"
>
<SlideView v-if="componentLoaded">
<template #content>
<template
v-for="data in dataList"
:key="data.id"
>
<TmdbPersonCard
v-if="props.type === 'tmdb'"
:person="data"
height="15rem"
width="10rem"
/>
<DoubanPersonCard
v-if="props.type === 'douban'"
:person="data"
height="15rem"
width="10rem"
/>
<BangumiPersonCard
v-if="props.type === 'bangumi'"
:person="data"
height="15rem"
width="10rem"
/>
<template v-for="data in dataList" :key="data.id">
<PersonCard :person="data" height="15rem" width="10rem" />
</template>
</template>
</SlideView>

View File

@@ -2,17 +2,18 @@
import MediaCardListView from './MediaCardListView.vue'
import api from '@/api'
import personIcon from '@images/misc/person.png'
import type { TmdbPerson } from '@/api/types'
import type { Person } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
// 输入参数
const personProps = defineProps({
personid: String,
type: String,
source: String,
})
// 媒体详情
const personDetail = ref<TmdbPerson>({} as TmdbPerson)
const personDetail = ref<Person>({} as Person)
// 是否已加载完成
const isRefreshed = ref(false)
@@ -23,23 +24,67 @@ const isImageLoaded = ref(false)
// 调用API查询详情
async function getPersonDetail() {
if (personProps.personid) {
personDetail.value = await api.get(`tmdb/person/${personProps.personid}`)
if (personProps.source === 'themoviedb') {
personDetail.value = await api.get(`tmdb/person/${personProps.personid}`)
} else if (personProps.source === 'douban') {
personDetail.value = await api.get(`douban/person/${personProps.personid}`)
} else if (personProps.source === 'bangumi') {
personDetail.value = await api.get(`bangumi/person/${personProps.personid}`)
}
isRefreshed.value = true
}
}
// 人物图片地址
function getPersonImage() {
if (!personDetail.value?.profile_path)
if (personProps.source === 'themoviedb') {
if (!personDetail.value?.profile_path) return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personDetail.value?.profile_path}`
} else if (personProps.source === 'douban') {
if (!personDetail.value?.avatar) return personIcon
if (typeof personDetail.value?.avatar === 'object') {
return personDetail.value?.avatar?.normal
} else {
return personDetail.value?.avatar
}
} else if (personProps.source === 'bangumi') {
if (!personDetail.value?.images) return personIcon
return personDetail.value?.images?.medium
} else {
return personIcon
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personDetail.value?.profile_path}`
}
}
// 将别名数组拆分为、分隔的字符串
function getAlsoKnownAs() {
if (!personDetail.value?.also_known_as)
return ''
return personDetail.value.also_known_as.join('、')
if (!personDetail.value?.also_known_as) return ''
if (personProps.source === 'themoviedb') {
return '别名:' + personDetail.value.also_known_as.join('、')
} else {
return personDetail.value.also_known_as.join('')
}
}
// 参演作品路由地址
function getPersonCreditsPath() {
let apipath = 'tmdb'
if (personProps.source === 'douban') {
apipath = 'douban'
} else if (personProps.source === 'bangumi') {
apipath = 'bangumi'
}
return `/browse/${apipath}/person/credits/${personDetail.value.id}?title=参演作品`
}
// 参演作品API路径
function getPersonCreditsApiPath() {
let apipath = 'tmdb'
if (personProps.source === 'douban') {
apipath = 'douban'
} else if (personProps.source === 'bangumi') {
apipath = 'bangumi'
}
return `${apipath}/person/credits/${personDetail.value.id}`
}
onBeforeMount(() => {
@@ -48,62 +93,46 @@ onBeforeMount(() => {
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="personDetail.id" class="max-w-8xl mx-auto px-4">
<div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row ">
<div class="relative z-10 mt-4 mb-8 flex flex-col items-center flex-md-row">
<VAvatar
size="200"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
<VImg v-img :src="getPersonImage()" cover @load="isImageLoaded = true" />
</VAvatar>
<div class="text-start ms-3 md:text-center">
<h1 class="text-3xl lg:text-4xl">
<div class="ms-3">
<h1 class="text-3xl lg:text-4xl text-center text-lg-left">
{{ personDetail.name }}
</h1>
<div class="mt-1 mb-2 space-y-1 text-xs sm:text-sm lg:text-base">
<div class="mt-1 mb-2 space-y-1 text-xs sm:text-sm lg:text-base text-center text-lg-left">
<div>
<span v-if="personDetail.birthday">{{ personDetail.birthday }}</span>
<span v-if="personDetail.place_of_birth"> | </span>
<span v-if="personDetail.place_of_birth">{{ personDetail.place_of_birth }}</span>
</div>
<div v-if="personDetail.also_known_as">
别名{{ getAlsoKnownAs() }}
</div>
<div v-if="personDetail.also_known_as">{{ getAlsoKnownAs() }}</div>
</div>
</div>
</div>
<div class="relative text-left">
<div class="group outline-none ring-0" role="button" tabindex="-1">
<p class="pt-2 text-sm lg:text-base" style="overflow-wrap: break-word;">
<p class="pt-2 text-sm lg:text-base" style="overflow-wrap: break-word">
{{ personDetail.biography }}
</p>
</div>
</div>
<div>
<div class="slider-header">
<RouterLink :to="`/browse/tmdb/person/credits/${personDetail.id}?title=参演作品`" class="slider-title">
<RouterLink :to="getPersonCreditsPath()" class="slider-title">
<span>参演作品</span>
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
</RouterLink>
</div>
<MediaCardListView :apipath="`tmdb/person/credits/${personDetail.id}`" />
<MediaCardListView :apipath="getPersonCreditsApiPath()" />
</div>
</div>
<NoDataFound

View File

@@ -6,6 +6,45 @@ import NoDataFound from '@/components/NoDataFound.vue'
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import PluginCard from '@/components/cards/PluginCard.vue'
import noImage from '@images/logos/plugin.png'
import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDefer } from '@/@core/utils/dom'
const route = useRoute()
// 显示器宽度
const display = useDisplay()
// 延迟加载
let defer = (_: number) => true
let deferApp = (_: number) => true
// 当前标签
const activeTab = ref(route.params.tab)
// 标签页
const tabs = [
{
title: '我的插件',
tab: 'myplugin',
},
{
title: '插件市场',
tab: 'pluginmarket',
},
]
// 当前排序字段
const activeSort = ref(null)
// 排序选项
const sortOptions = [
{ title: '热门', value: 'count' },
{ title: '插件名称', value: 'plugin_name' },
{ title: '作者', value: 'plugin_author' },
{ title: '插件仓库', value: 'repo_url' },
{ title: '最新发布', value: 'add_time' },
]
// 已安装插件列表
const dataList = ref<Plugin[]>([])
@@ -13,6 +52,9 @@ const dataList = ref<Plugin[]>([])
// 未安装插件列表
const uninstalledList = ref<Plugin[]>([])
// 插件市场插件列表
const marketList = ref<Plugin[]>([])
// 是否刷新过
const isRefreshed = ref(false)
@@ -46,6 +88,38 @@ const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
// 过滤表单
const filterForm = reactive({
// 名称
name: '' as string,
// 作者
author: [] as string[],
// 标签
label: [] as string[],
// 插件库
repo: [] as string[],
})
// 作者过滤项
const authorFilterOptions = ref<string[]>([])
// 标签过滤项
const labelFilterOptions = ref<string[]>([])
// 插件库过滤项
const repoFilterOptions = ref<string[]>([])
// 初始化过滤选项
function initOptions(item: Plugin) {
const optionValue = (options: Array<string>, value: string | undefined) => {
value && !options.includes(value) && options.push(value)
}
const optionMutipleValue = (options: Array<string>, value: string | undefined) => {
value && value.split(',').forEach(v => !options.includes(v) && options.push(v))
}
optionValue(authorFilterOptions.value, item.plugin_author)
optionMutipleValue(labelFilterOptions.value, item.plugin_label)
optionValue(repoFilterOptions.value, handleRepoUrl(item.repo_url))
}
// 关闭插件市场窗口
function pluginDialogClose() {
PluginAppDialog.value = false
@@ -58,15 +132,12 @@ async function installPlugin(item: Plugin) {
progressDialog.value = true
progressText.value = `正在安装 ${item?.plugin_name} v${item?.plugin_version} ...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${item?.id}`,
{
params: {
repo_url: item?.repo_url,
force: item?.has_update,
},
const result: { [key: string]: any } = await api.get(`plugin/install/${item?.id}`, {
params: {
repo_url: item?.repo_url,
force: item?.has_update,
},
)
})
// 隐藏等待提示框
progressDialog.value = false
@@ -76,12 +147,10 @@ async function installPlugin(item: Plugin) {
// 刷新
refreshData()
}
else {
} else {
$toast.error(`插件 ${item?.plugin_name} 安装失败:${result.message}`)
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -92,8 +161,7 @@ function openPlugin(item: Plugin) {
if (item.installed === true) {
// 标记插件动作
pluginActions.value[item.id || '0'] = true
}
else {
} else {
// 如果是未安装插件则安装
installPlugin(item)
}
@@ -113,11 +181,10 @@ function pluginIconError(item: Plugin) {
// 插件图标地址
function pluginIcon(item: Plugin) {
// 如果图片加载错误
if (pluginIconLoaded.value[item.id || '0'] === false)
return noImage
if (pluginIconLoaded.value[item.id || '0'] === false) return noImage
// 如果是网络图片则使用代理后返回
if (item?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(item?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}`
return `./plugin_icon/${item?.plugin_icon}`
}
@@ -126,16 +193,15 @@ function pluginIcon(item: Plugin) {
const filterPlugins = computed(() => {
const all_list = [...dataList.value, ...uninstalledList.value]
return all_list.filter((item: Plugin) => {
return item.plugin_name?.includes(keyword.value) || item.plugin_desc?.includes(keyword.value)
// 需要忽略大小写
return (
item.plugin_name?.toLowerCase().includes(keyword.value.toLowerCase()) ||
item.plugin_desc?.toLowerCase().includes(keyword.value.toLowerCase()) ||
!keyword
)
})
})
// 新安装了插件
function pluginInstalled() {
pluginDialogClose()
refreshData()
}
// 获取插件列表数据
async function fetchInstalledPlugins() {
try {
@@ -144,9 +210,9 @@ async function fetchInstalledPlugins() {
state: 'installed',
},
})
defer = useDefer(dataList.value.length)
isRefreshed.value = true
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -159,8 +225,6 @@ async function fetchUninstalledPlugins() {
state: 'market',
},
})
// 设置APP市场加载完成
isAppMarketLoaded.value = true
// 设置更新状态
for (const uninstalled of uninstalledList.value) {
for (const data of dataList.value) {
@@ -171,8 +235,14 @@ async function fetchUninstalledPlugins() {
}
}
}
}
catch (error) {
// 更新插件市场列表
// 排除已安装且有更新的,上面的问题在于“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示
marketList.value = uninstalledList.value.filter(item => !(item.has_update && item.installed))
// 初始化过滤选项
marketList.value.forEach(initOptions)
// 设置APP市场加载完成
isAppMarketLoaded.value = true
} catch (error) {
console.error(error)
}
}
@@ -181,139 +251,192 @@ async function fetchUninstalledPlugins() {
async function getPluginStatistics() {
try {
PluginStatistics.value = await api.get('plugin/statistic')
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 加载所有数据
function refreshData() {
fetchInstalledPlugins()
async function refreshData() {
await fetchInstalledPlugins()
fetchUninstalledPlugins()
}
// 对uninstalledList进行排序按PluginStatistics倒序
const sortedUninstalledList = computed(() => {
const list = uninstalledList.value.filter(item => !item.has_update)
if (PluginStatistics.value.length === 0)
return list
return list.sort((a, b) => {
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
const matchMultiple = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && value.split(',').some(v => filter.includes(v)))
const filterText = (filter: string, value: string | undefined) =>
!filter || (value && value.toLowerCase().includes(filter.toLowerCase()))
// 过滤后的数据列表
const ret_list: Plugin[] = []
// 过滤
marketList.value.forEach(value => {
if (value) {
if (
filterText(filterForm.name, `${value.plugin_name} ${value.plugin_desc}`) &&
match(filterForm.author, value.plugin_author) &&
matchMultiple(filterForm.label, value.plugin_label) &&
match(filterForm.repo, handleRepoUrl(value.repo_url))
) {
ret_list.push(value)
}
}
})
deferApp = useDefer(ret_list.length)
if (isNullOrEmptyObject(PluginStatistics.value)) return ret_list
// 数据排序
if (!activeSort.value || activeSort.value === 'count') {
return ret_list.sort((a, b) => {
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
})
} else if (activeSort.value) {
return ret_list.sort((a: any, b: any) => {
return a[activeSort.value ?? ''] > b[activeSort.value ?? ''] ? 1 : -1
})
}
})
// 加载时获取数据
onBeforeMount(() => {
// 标签转换
function pluginLabels(label: string | undefined) {
if (!label) return []
return label.split(',')
}
// 新安装了插件
function pluginInstalled() {
pluginDialogClose()
refreshData()
}
// 处理掉github地址的前缀
function handleRepoUrl(url: string | undefined) {
if (!url) return ''
return url.replace('https://github.com/', '').replace('https://raw.githubusercontent.com/', '')
}
// 加载时获取数据
onBeforeMount(async () => {
await refreshData()
getPluginStatistics()
})
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div
v-if="dataList.length > 0"
class="grid gap-4 grid-plugin-card"
>
<PluginCard
v-for="data in dataList"
:key="`${data.id}_v${data.plugin_version}`"
:count="PluginStatistics[data.id || '0']"
:plugin="data"
:action="pluginActions[data.id || '0']"
@remove="refreshData"
@save="refreshData"
@action-done="pluginActions[data.id || '0'] = false"
/>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有安装插件"
error-description="点击右下角按钮前往插件市场安装插件"
/>
<!-- App市场 -->
<VFab
icon="mdi-store-plus"
location="bottom end"
size="x-large"
fixed
app
appear
@click="PluginAppDialog = true"
/>
<VDialog
v-if="PluginAppDialog"
v-model="PluginAppDialog"
fullscreen
scrollable
:scrim="false"
:z-index="1010"
transition="dialog-bottom-transition"
>
<!-- Dialog Content -->
<VCard>
<!-- Toolbar -->
<div>
<VToolbar color="primary">
<VToolbarTitle>插件市场</VToolbarTitle>
<div>
<VTabs v-model="activeTab">
<VTab v-for="item in tabs" :value="item.tab">
<span class="mx-5">{{ item.title }}</span>
</VTab>
</VTabs>
<VSpacer />
<VDivider />
<VToolbarItems>
<VBtn
size="x-large"
@click="pluginDialogClose"
>
<VIcon
color="white"
icon="mdi-close"
/>
</VBtn>
</VToolbarItems>
</VToolbar>
</div>
<VCardText>
<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 sortedUninstalledList"
:key="`${data.id}_v${data.plugin_version}`"
:plugin="data"
:count="PluginStatistics[data.id || '0']"
@install="pluginInstalled"
/>
</div>
<NoDataFound
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
error-code="404"
error-title="没有未安装插件"
error-description="所有可用插件均已安装"
/>
</VCardText>
</VCard>
</VDialog>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<!-- 我的插件 -->
<VWindowItem value="myplugin">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card">
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
<PluginCard
v-if="defer(index)"
:count="PluginStatistics[data.id || '0']"
:plugin="data"
:action="pluginActions[data.id || '0']"
@remove="refreshData"
@save="refreshData"
@action-done="pluginActions[data.id || '0'] = false"
/>
</template>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有安装插件"
error-description="点击右下角按钮前往插件市场安装插件"
/>
</div>
</transition>
</VWindowItem>
<!-- 插件市场 -->
<VWindowItem value="pluginmarket">
<transition name="fade-slide" appear>
<div>
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
<!-- 过滤表单 -->
<div v-if="isAppMarketLoaded" class="bg-transparent mb-3 shadow-none">
<VRow>
<VCol cols="6" md="">
<VTextField v-model="filterForm.name" size="small" density="compact" label="名称" clearable />
</VCol>
<VCol v-if="authorFilterOptions.length > 0" cols="6" md="">
<VSelect
v-model="filterForm.author"
:items="authorFilterOptions"
size="small"
density="compact"
chips
label="作者"
multiple
/>
</VCol>
<VCol v-if="labelFilterOptions.length > 0" cols="6" md="">
<VSelect
v-model="filterForm.label"
:items="labelFilterOptions"
size="small"
density="compact"
chips
label="标签"
multiple
/>
</VCol>
<VCol v-if="repoFilterOptions.length > 0" cols="6" md="">
<VSelect
v-model="filterForm.repo"
:items="repoFilterOptions"
size="small"
density="compact"
chips
label="插件库"
multiple
/>
</VCol>
<VCol v-if="repoFilterOptions.length > 0" cols="6" md="">
<VSelect v-model="activeSort" :items="sortOptions" size="small" density="compact" label="排序" />
</VCol>
</VRow>
</div>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
<PluginAppCard
v-if="deferApp(index)"
:plugin="data"
:count="PluginStatistics[data.id || '0']"
@install="pluginInstalled"
/>
</template>
</div>
<NoDataFound
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
error-code="404"
error-title="没有未安装插件"
error-description="所有可用插件均已安装"
/>
</div>
</transition>
</VWindowItem>
</VWindow>
</div>
<!-- 插件搜索 -->
<VFab
@@ -333,12 +456,10 @@ onBeforeMount(() => {
scrollable
:z-index="1010"
max-width="40rem"
max-height="85vh"
:max-height="!display.mdAndUp.value ? '' : '85vh'"
:fullscreen="!display.mdAndUp.value"
>
<VCard
class="mx-auto"
width="100%"
>
<VCard class="mx-auto" width="100%">
<VToolbar flat class="p-0">
<VTextField
v-model="keyword"
@@ -352,60 +473,50 @@ onBeforeMount(() => {
/>
</VToolbar>
<DialogCloseBtn @click="closeSearchDialog" />
<VList
v-if="filterPlugins.length > 0"
lines="two"
>
<template v-for="(item, i) in filterPlugins" :key="i">
<VListItem
@click="openPlugin(item)"
>
<template #prepend>
<VAvatar>
<VImg
:src="pluginIcon(item)"
@error="pluginIconError(item)"
<VList v-if="filterPlugins.length > 0" lines="three">
<VVirtualScroll :items="filterPlugins">
<template #default="{ item }">
<VListItem @click="openPlugin(item)">
<template #prepend>
<VAvatar>
<VImg :src="pluginIcon(item)" @error="pluginIconError(item)">
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
</div>
</template>
</VImg>
</VAvatar>
</template>
<VListItemTitle>
{{ item.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ item?.plugin_version }}</span>
<VIcon v-if="item.installed" color="success" icon="mdi-check-circle" class="ms-2" size="small" />
</VListItemTitle>
<VListItemSubtitle class="mt-2">
<VChip
v-for="label in pluginLabels(item.plugin_label)"
variant="tonal"
size="small"
class="me-1 my-1"
color="info"
label
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
</div>
</template>
</VImg>
</VAvatar>
</template>
<VListItemTitle>
{{ item.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ item?.plugin_version }}</span>
<VIcon
v-if="item.installed"
color="success"
icon="mdi-check-circle"
class="ms-2"
size="small"
/>
</VListItemTitle>
<VListItemSubtitle class="mt-2" v-html="item.plugin_desc" />
</VListItem>
</template>
{{ label }}
</VChip>
{{ item.plugin_desc }}
</VListItemSubtitle>
</VListItem>
</template>
</VVirtualScroll>
</VList>
</VCard>
</VDialog>
<!-- 安装插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VDialog v-model="progressDialog" :scrim="false" width="25rem">
<VCard color="primary">
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
<VProgressLinear indeterminate color="white" class="mb-0 mt-1" />
</VCardText>
</VCard>
</VDialog>

View File

@@ -67,17 +67,10 @@ onUnmounted(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { debounce } from 'lodash'
import { ref, unref } from 'vue'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
// 提示框
const $toast = useToast()
@@ -28,27 +29,27 @@ const headers = [
{
title: '标题',
key: 'title',
sortable: false,
sortable: true,
},
{
title: '目录',
key: 'src',
sortable: false,
sortable: true,
},
{
title: '转移方式',
key: 'mode',
sortable: false,
sortable: true,
},
{
title: '时间',
key: 'date',
sortable: false,
sortable: true,
},
{
title: '状态',
key: 'status',
sortable: false,
sortable: true,
},
{
title: '',
@@ -58,11 +59,13 @@ const headers = [
]
const pageRange = [
{title: '25', value: 25},
{title: '50', value: 50},
{title: '100', value: 100},
{title: '1000', value: 1000},
{title: 'All', value: -1}]
{ title: '25', value: 25 },
{ title: '50', value: 50 },
{ title: '100', value: 100 },
{ title: '500', value: 500 },
{ title: '1000', value: 1000 },
{ title: 'All', value: -1 },
]
// 数据列表
const dataList = ref<TransferHistory[]>([])
@@ -112,30 +115,32 @@ const TransferDict: { [key: string]: string } = {
// 分页提示
const pageTip = computed(() => {
const begin = unref(itemsPerPage) * (unref(currentPage) - 1) + 1
const end = unref(itemsPerPage) * unref(currentPage) === -1 ? 'ALL' : unref(itemsPerPage) * unref(currentPage)
const begin = unref(itemsPerPage) * (unref(currentPage) - 1) + 1
const end = unref(itemsPerPage) * unref(currentPage) === -1 ? 'ALL' : unref(itemsPerPage) * unref(currentPage)
return {
begin,
end
end,
}
})
// 分页总数
const totalPage = computed(() => {
const total = Math.ceil(unref(totalItems) /unref(itemsPerPage))
const total = Math.ceil(unref(totalItems) / unref(itemsPerPage))
return total
})
// 切换页签和搜索词
watch(
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
async () => {
debounce(async () => {
await fetchData()
})
}, 1000),
)
// 获取订阅列表数据
async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
loading.value = true
try {
const result: { [key: string]: any } = await api.get('history/transfer', {
params: {
@@ -150,8 +155,7 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
title => title !== '',
)
}
catch (error) {
} catch (error) {
console.error(error)
}
loading.value = false
@@ -159,12 +163,9 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
// 根据 type 返回不同的图标
function getIcon(type: string) {
if (type === '电影')
return 'mdi-movie'
else if (type === '电视剧')
return 'mdi-television-classic'
else
return 'mdi-help-circle'
if (type === '电影') return 'mdi-movie'
else if (type === '电视剧') return 'mdi-television-classic'
else return 'mdi-help-circle'
}
// 删除历史记录
@@ -184,10 +185,8 @@ async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boo
data: item,
})
if (!result.success)
$toast.error(`删除失败: ${result.msg}`)
}
catch (error) {
if (!result.success) $toast.error(`删除失败: ${result.msg}`)
} catch (error) {
console.error(error)
}
}
@@ -196,8 +195,7 @@ async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boo
async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
// 关闭弹窗
deleteConfirmDialog.value = false
if (!currentHistory.value)
return
if (!currentHistory.value) return
// 删除
await remove(currentHistory.value, deleteSrc, deleteDest)
@@ -211,8 +209,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
deleteConfirmDialog.value = false
// 总条数
const total = selected.value.length
if (total === 0)
return
if (total === 0) return
// 已处理条数
let handled = 0
@@ -237,16 +234,13 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
// 响应删除操作
async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
if (currentHistory.value)
await removeSingle(deleteSrc, deleteDest)
else
await removeBatch(deleteSrc, deleteDest)
if (currentHistory.value) await removeSingle(deleteSrc, deleteDest)
else await removeBatch(deleteSrc, deleteDest)
}
// 批量删除历史记录
async function removeHistoryBatch() {
if (selected.value.length === 0)
return
if (selected.value.length === 0) return
// 清空当前操作记录
currentHistory.value = undefined
@@ -257,26 +251,20 @@ async function removeHistoryBatch() {
// 计算根路径
function getRootPath(path: string, type: string, category: string) {
if (!path)
return ''
if (!path) return ''
let index = -2
if (type !== '电影')
index = -3
if (type !== '电影') index = -3
if (category)
index -= 1
if (category) index -= 1
if (path.includes('/'))
return path.split('/').slice(0, index).join('/')
else
return path.split('\\').slice(0, index).join('\\')
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
if (selected.value.length === 0) return
// 清空当前操作记录
currentHistory.value = undefined
@@ -292,8 +280,7 @@ async function retransferBatch() {
const category = selected.value[0].category ?? ''
// 计算根路径
redoTarget.value = getRootPath(dest, mediaType, category)
}
else {
} else {
redoTarget.value = ''
}
// 打开识别弹窗
@@ -329,7 +316,6 @@ const dropdownItems = ref([
// 初始加载数据
onMounted(fetchData)
</script>
<template>
@@ -337,9 +323,7 @@ onMounted(fetchData)
<VCardItem>
<VCardTitle>
<VRow>
<VCol cols="4" md="6">
历史记录
</VCol>
<VCol cols="4" md="6"> 历史记录 </VCol>
<VCol cols="8" md="6" class="flex">
<VCombobox
key="search_navbar"
@@ -378,15 +362,18 @@ onMounted(fetchData)
<VIcon :icon="getIcon(item.type || '')" />
</VAvatar>
<div class="d-flex flex-column ms-1">
<span class="d-block text-high-emphasis min-w-20">
<span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
{{ item?.title }} {{ item?.seasons }}{{ item?.episodes }}
</span>
<span v-else class="d-block text-high-emphasis min-w-20">
{{ item?.title }}
</span>
<small>{{ item?.category }}</small>
</div>
</div>
</template>
<template #item.src="{ item }">
<small>{{ item?.src }} <br>=> {{ item?.dest }}</small>
<small>{{ item?.src }} <br />=> {{ item?.dest }}</small>
</template>
<template #item.mode="{ item }">
<VChip variant="outlined" color="primary" size="small">
@@ -394,16 +381,12 @@ onMounted(fetchData)
</VChip>
</template>
<template #item.status="{ item }">
<VChip v-if="item?.status" color="success" size="small">
成功
</VChip>
<v-tooltip v-else :text="item?.errmsg">
<VChip v-if="item?.status" color="success" size="small"> 成功 </VChip>
<VTooltip v-else :text="item?.errmsg">
<template #activator="{ props }">
<VChip v-bind="props" color="error" size="small">
失败
</VChip>
<VChip v-bind="props" color="error" size="small"> 失败 </VChip>
</template>
</v-tooltip>
</VTooltip>
</template>
<template #item.date="{ item }">
<small>{{ item?.date }}</small>
@@ -429,22 +412,13 @@ onMounted(fetchData)
</VMenu>
</IconBtn>
</template>
<template #no-data>
没有数据
</template>
<template #no-data> 没有数据 </template>
</VDataTableVirtual>
<div class="flex items-center justify-end">
<div class="w-auto">
<VSelect
v-model="itemsPerPage"
:items="pageRange"
density="compact"
variant="solo"
flat
size="small"
/>
<VSelect v-model="itemsPerPage" :items="pageRange" density="compact" variant="solo" flat />
</div>
<div class="w-auto text-sm">{{pageTip.begin}}-{{pageTip.end}} / {{totalItems}}</div>
<div class="w-auto text-sm">{{ pageTip.begin }}-{{ pageTip.end }} / {{ totalItems }}</div>
<VPagination
v-model="currentPage"
show-first-last-page
@@ -463,12 +437,8 @@ onMounted(fetchData)
{{ 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)">
仅删除历史记录
</VBtn>
<VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)">
删除历史记录和源文件
</VBtn>
<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>
<VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)">
删除历史记录和媒体库文件
</VBtn>
@@ -479,7 +449,7 @@ onMounted(fetchData)
</VCard>
</VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
<ReorganizeDialog
v-if="redoDialog"
v-model="redoDialog"
:logids="redoIds"
@@ -531,4 +501,10 @@ onMounted(fetchData)
.data-table-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.data-table-div {
block-size: calc(100vh - 17rem);
}
}
</style>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { calculateTimeDifference } from '@/@core/utils'
import { formatDateDifference } from '@/@core/utils/formatters'
import api from '@/api'
// 系统环境变量
@@ -30,13 +30,10 @@ function showReleaseDialog(title: string, body: string) {
// 查询系统环境变量
async function querySystemEnv() {
try {
const result: { [key: string]: any } = await api.get(
'system/env',
)
const result: { [key: string]: any } = await api.get('system/env')
systemEnv.value = result.data
}
catch (error) {
} catch (error) {
console.log(error)
}
}
@@ -44,17 +41,13 @@ async function querySystemEnv() {
// 查询所有Release
async function queryAllRelease() {
try {
const result: { [key: string]: any } = await api.get(
'system/versions',
)
const result: { [key: string]: any } = await api.get('system/versions')
allRelease.value = result.data ?? []
// 最新版本
if (allRelease.value.length > 0)
latestRelease.value = allRelease.value[0].tag_name
}
catch (error) {
if (allRelease.value.length > 0) latestRelease.value = allRelease.value[0].tag_name
} catch (error) {
console.log(error)
}
}
@@ -62,7 +55,7 @@ async function queryAllRelease() {
// 计算发布时间
function releaseTime(releaseDate: string) {
// 上一次更新时间
return `${calculateTimeDifference(releaseDate)}`
return formatDateDifference(releaseDate)
}
onMounted(() => {
@@ -75,22 +68,25 @@ onMounted(() => {
<div class="px-3">
<div class="section">
<div>
<h3 class="heading">
关于 MoviePilot
</h3>
<h3 class="heading">关于 MoviePilot</h3>
</div>
<div class="section border-t border-gray-800">
<dl>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
软件版本
</dt>
<dt class="block text-sm font-bold">软件版本</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.VERSION }}</code>
<a v-if="latestRelease === systemEnv.VERSION" href="https://github.com/jxxghp/MoviePilot/releases" target="_blank" rel="noopener noreferrer">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 ml-2 !cursor-pointer transition hover:bg-green-400">
<a
v-if="latestRelease === systemEnv.VERSION"
href="https://github.com/jxxghp/MoviePilot/releases"
target="_blank"
rel="noopener noreferrer"
>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 ml-2 !cursor-pointer transition hover:bg-green-400"
>
最新
</span>
</a>
@@ -98,11 +94,19 @@ onMounted(() => {
</dd>
</div>
</div>
<div v-if="systemEnv.FRONTEND_VERSION">
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">前端版本</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.FRONTEND_VERSION }}</code>
</span>
</dd>
</div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
认证资源版本
</dt>
<dt class="block text-sm font-bold">认证资源版本</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
@@ -112,9 +116,7 @@ onMounted(() => {
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
站点资源版本
</dt>
<dt class="block text-sm font-bold">站点资源版本</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
@@ -124,9 +126,7 @@ onMounted(() => {
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
配置目录
</dt>
<dt class="block text-sm font-bold">配置目录</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined">
<code>{{ systemEnv.CONFIG_DIR }}</code>
@@ -134,9 +134,7 @@ onMounted(() => {
</dd>
</div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
数据目录
</dt>
<dt class="block text-sm font-bold">数据目录</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined"><code>/moviepilot</code></span>
</dd>
@@ -144,9 +142,7 @@ onMounted(() => {
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
时区
</dt>
<dt class="block text-sm font-bold">时区</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined">
<code>{{ systemEnv.TZ }}</code>
@@ -159,44 +155,55 @@ onMounted(() => {
</div>
<div class="section">
<div>
<h3 class="heading">
支援
</h3>
<h3 class="heading">支援</h3>
</div>
<div class="section border-t border-gray-800">
<dl>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
文档
</dt><dd class="flex text-sm sm:col-span-2 sm:mt-0">
<dt class="block text-sm font-bold">文档</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined">
<a href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank" rel="noreferrer" class="text-indigo-500 transition duration-300 hover:underline">
<a
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
target="_blank"
rel="noreferrer"
class="text-indigo-500 transition duration-300 hover:underline"
>
https://github.com/jxxghp/MoviePilot/blob/main/README.md
</a>
</span>
</dd>
</div>
</div><div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
问题反馈
</dt><dd class="flex text-sm sm:col-span-2 sm:mt-0">
<dt class="block text-sm font-bold">问题反馈</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined">
<a href="https://github.com/jxxghp/MoviePilot/issues/new/choose" target="_blank" rel="noreferrer" class="text-indigo-500 transition duration-300 hover:underline">
<a
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
target="_blank"
rel="noreferrer"
class="text-indigo-500 transition duration-300 hover:underline"
>
https://github.com/jxxghp/MoviePilot/issues/new/choose
</a>
</span>
</dd>
</div>
</div><div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
发布频道
</dt>
<dt class="block text-sm font-bold">发布频道</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow undefined">
<a href="https://t.me/moviepilot_channel" target="_blank" rel="noreferrer" class="text-indigo-500 transition duration-300 hover:underline">
<a
href="https://t.me/moviepilot_channel"
target="_blank"
rel="noreferrer"
class="text-indigo-500 transition duration-300 hover:underline"
>
https://t.me/moviepilot_channel
</a>
</span>
@@ -208,21 +215,31 @@ onMounted(() => {
</div>
<div class="section">
<div>
<h3 class="heading">
软件版本
</h3>
<h3 class="heading">软件版本</h3>
<div class="section space-y-3">
<div>
<div v-for="release in allRelease" :key="release.tag_name" class="mb-3 flex w-full flex-col space-y-3 rounded-md px-4 py-2 shadow-md ring-1 ring-gray-400 sm:flex-row sm:space-y-0 sm:space-x-3">
<div
v-for="release in allRelease"
:key="release.tag_name"
class="mb-3 flex w-full flex-col space-y-3 rounded-md px-4 py-2 shadow-md ring-1 ring-gray-400 sm:flex-row sm:space-y-0 sm:space-x-3"
>
<div class="flex w-full flex-grow items-center justify-start space-x-2 truncate sm:justify-start">
<span class="truncate text-lg font-bold">
<span class="mr-2 whitespace-nowrap text-xs font-normal">{{ releaseTime(release.published_at) }}</span>
<span class="mr-2 whitespace-nowrap text-xs font-normal">{{
releaseTime(release.published_at)
}}</span>
{{ release.tag_name }}
</span>
<span v-if="release.tag_name === latestRelease" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-green-500 bg-opacity-80 border border-green-500 !text-green-100">
<span
v-if="release.tag_name === latestRelease"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-green-500 bg-opacity-80 border border-green-500 !text-green-100"
>
最新软件版本
</span>
<span v-if="release.tag_name === systemEnv.VERSION" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100">
<span
v-if="release.tag_name === systemEnv.VERSION"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100"
>
当前版本
</span>
</div>

View File

@@ -252,362 +252,364 @@ onMounted(() => {
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="个人信息">
<VCardText class="d-flex">
<!-- 👉 Avatar -->
<VAvatar
rounded="lg"
size="100"
class="me-6"
:image="accountInfo.avatar"
/>
<div>
<VRow>
<VCol cols="12">
<VCard title="个人信息">
<VCardText class="d-flex">
<!-- 👉 Avatar -->
<VAvatar
rounded="lg"
size="100"
class="me-6"
:image="accountInfo.avatar"
/>
<!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2">
<VBtn
color="primary"
@click="refInputEl?.click()"
>
<VIcon
icon="mdi-cloud-upload-outline"
/>
<span class="d-none d-sm-block ms-2">上传头像</span>
</VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
>
<VBtn
type="reset"
color="error"
variant="tonal"
@click="resetAvatar"
>
<VIcon
icon="mdi-refresh"
/>
<span class="d-none d-sm-block ms-2">重置</span>
</VBtn>
<VBtn
:color="accountInfo.is_otp ? 'error' : 'info'"
variant="tonal"
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
>
<VIcon
icon="mdi-account-key"
/>
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? "关闭验证" : "双重验证" }}</span>
</VBtn>
</div>
<p class="text-body-1 mb-0">
允许 JPGGIF PNG 格式 最大尺寸 800K
</p>
</form>
</VCardText>
<VDivider />
<VCardText>
<!-- 👉 Form -->
<VForm class="mt-6">
<VRow>
<!-- 👉 Name -->
<VCol
md="6"
cols="12"
>
<VTextField
v-model="accountInfo.name"
readonly
label="用户名"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="accountInfo.email"
label="邮箱"
type="email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
label="新密码"
autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="确认新密码"
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
/>
</VCol>
<!-- 👉 Form Actions -->
<VCol
cols="12"
class="d-flex flex-wrap gap-4"
>
<VBtn @click="saveAccountInfo">
保存
<!-- 👉 Upload Photo -->
<form class="d-flex flex-column justify-center gap-5">
<div class="d-flex flex-wrap gap-2">
<VBtn
color="primary"
@click="refInputEl?.click()"
>
<VIcon
icon="mdi-cloud-upload-outline"
/>
<span class="d-none d-sm-block ms-2">上传头像</span>
</VBtn>
<input
ref="refInputEl"
type="file"
name="file"
accept=".jpeg,.png,.jpg,GIF"
hidden
@input="changeAvatar"
>
<VBtn
type="reset"
color="error"
variant="tonal"
@click="resetAvatar"
>
<VIcon
icon="mdi-refresh"
/>
<span class="d-none d-sm-block ms-2">重置</span>
</VBtn>
<VBtn
:color="accountInfo.is_otp ? 'error' : 'info'"
variant="tonal"
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
>
<VIcon
icon="mdi-account-key"
/>
<span class="d-none d-sm-block ms-2">{{ accountInfo.is_otp ? "关闭验证" : "双重验证" }}</span>
</VBtn>
</div>
<p class="text-body-1 mb-0">
允许 JPGGIF PNG 格式 最大尺寸 800K
</p>
</form>
</VCardText>
<VDivider />
<VCardText>
<!-- 👉 Form -->
<VForm class="mt-6">
<VRow>
<!-- 👉 Name -->
<VCol
md="6"
cols="12"
>
<VTextField
v-model="accountInfo.name"
readonly
label="用户名"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="accountInfo.email"
label="邮箱"
type="email"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 new password -->
<VTextField
v-model="newPassword"
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
label="新密码"
autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<!-- 👉 confirm password -->
<VTextField
v-model="confirmPassword"
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
label="确认新密码"
@click:append-inner="
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
/>
</VCol>
<!-- 👉 Form Actions -->
<VCol
cols="12"
class="d-flex flex-wrap gap-4"
>
<VBtn @click="saveAccountInfo">
保存
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol
v-if="accountInfo.is_superuser"
cols="12"
>
<!-- 👉 Accounts -->
<VCard title="所有用户">
<template #append>
<IconBtn @click.stop="addUserDialog = true">
<VIcon icon="mdi-plus" />
</IconBtn>
</template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
用户名
</th>
<th scope="col">
邮箱
</th>
<th scope="col">
状态
</th>
<th scope="col">
管理员
</th>
<th
scope="col"
class="w-5"
/>
</tr>
</thead>
<tbody>
<tr
v-for="user in allUsers"
:key="user.name"
>
<td>
{{ user.name }}
</td>
<td>{{ user.email }}</td>
<td>
<VChip
v-if="user.is_active"
color="success"
text-color="white"
>
激活
</VChip>
<VChip
v-else
color="error"
text-color="white"
>
冻结
</VChip>
</td>
<td>{{ user.is_superuser ? "是" : "否" }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="deactivateUser(user)"
>
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{
user.is_active ? "冻结" : "解冻"
}}
</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
base-color="error"
@click="deleteUser(user)"
>
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</VCol>
</VRow>
<!-- =弹窗 -->
<VDialog
v-model="addUserDialog"
max-width="50rem"
persistent
z-index="1010"
>
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.name"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.email"
:rules="[requiredValidator]"
label="邮箱"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="addUser">
确定
</VBtn>
</VCardActions>
</VCard>
</VCol>
</VDialog>
<VCol
v-if="accountInfo.is_superuser"
cols="12"
<!-- 双重验证弹窗 -->
<VDialog
v-model="otpDialog"
max-width="45rem"
persistent
z-index="1010"
>
<!-- 👉 Accounts -->
<VCard title="所有用户">
<template #append>
<IconBtn @click.stop="addUserDialog = true">
<VIcon icon="mdi-plus" />
</IconBtn>
</template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
用户名
</th>
<th scope="col">
邮箱
</th>
<th scope="col">
状态
</th>
<th scope="col">
管理员
</th>
<th
scope="col"
class="w-5"
/>
</tr>
</thead>
<tbody>
<tr
v-for="user in allUsers"
:key="user.name"
>
<td>
{{ user.name }}
</td>
<td>{{ user.email }}</td>
<td>
<VChip
v-if="user.is_active"
color="success"
text-color="white"
>
激活
</VChip>
<VChip
v-else
color="error"
text-color="white"
>
冻结
</VChip>
</td>
<td>{{ user.is_superuser ? "是" : "否" }}</td>
<td>
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
<VIcon icon="mdi-dots-vertical" />
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="deactivateUser(user)"
>
<template #prepend>
<VIcon icon="mdi-lock" />
</template>
<VListItemTitle>
{{
user.is_active ? "冻结" : "解冻"
}}
</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
base-color="error"
@click="deleteUser(user)"
>
<template #prepend>
<VIcon icon="mdi-delete" />
</template>
<VListItemTitle>删除</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</td>
</tr>
</tbody>
</VTable>
</VCard>
</VCol>
</VRow>
<!-- =弹窗 -->
<VDialog
v-model="addUserDialog"
max-width="50rem"
persistent
z-index="1010"
>
<!-- Dialog Content -->
<VCard title="新增用户">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.name"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.password"
label="密码"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'"
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userForm.email"
:rules="[requiredValidator]"
label="邮箱"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="addUserDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="addUser">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 双重验证弹窗 -->
<VDialog
v-model="otpDialog"
max-width="45rem"
persistent
z-index="1010"
>
<!-- 开启双重验证弹窗内容 -->
<VCard>
<DialogCloseBtn @click="otpDialog = false" />
<VCardText>
<h4 class="text-h4 text-center mb-6 mt-5">
登录双重验证
</h4><h5 class="text-h5 font-weight-medium mb-2">
身份验证器
</h5>
<p class="mb-6">
使用像Google AuthenticatorMicrosoft AuthenticatorAuthy或1Password这样的身份验证器应用程序扫描二维码它将为您生成一个6位数的代码供您在下方输入
</p>
<div class="my-6">
<QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
</div>
<VAlert
:title="secret"
variant="tonal"
type="warning"
class="my-4"
text="如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。"
>
<template #prepend />
</VAlert>
<VForm>
<VTextField
v-model="otpPassword"
type="text"
label="输入验证码以确认开启双重验证"
autocomplete=""
class="mb-8"
variant="outlined"
/>
<div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false">
取消
</VBtn>
<VBtn @click="judgeOtpPassword">
确定
<template #append>
<VIcon icon="mdi-check" />
</template>
</VBtn>
<!-- 开启双重验证弹窗内容 -->
<VCard>
<DialogCloseBtn @click="otpDialog = false" />
<VCardText>
<h4 class="text-h4 text-center mb-6 mt-5">
登录双重验证
</h4><h5 class="text-h5 font-weight-medium mb-2">
身份验证器
</h5>
<p class="mb-6">
使用像Google AuthenticatorMicrosoft AuthenticatorAuthy或1Password这样的身份验证器应用程序扫描二维码它将为您生成一个6位数的代码供您在下方输入
</p>
<div class="my-6">
<QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
<VAlert
:title="secret"
variant="tonal"
type="warning"
class="my-4"
text="如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。"
>
<template #prepend />
</VAlert>
<VForm>
<VTextField
v-model="otpPassword"
type="text"
label="输入验证码以确认开启双重验证"
autocomplete=""
class="mb-8"
variant="outlined"
/>
<div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false">
取消
</VBtn>
<VBtn @click="judgeOtpPassword">
确定
<template #append>
<VIcon icon="mdi-check" />
</template>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</div>
</template>

View File

@@ -4,7 +4,7 @@ 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'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型
interface FilterCard {
@@ -30,8 +30,29 @@ const selectedSites = ref<number[]>([])
const defaultFilterRules = ref({
include: '',
exclude: '',
min_seeders: 0,
min_seeders_time: 0,
})
// 媒体信息数据源字典
const mediaSourcesDict = [
{
title: 'TheMovieDb',
value: 'themoviedb',
},
{
title: '豆瓣',
value: 'douban',
},
{
title: 'Bangumi',
value: 'bangumi',
},
]
// 当前选中的媒体信息数据源
const selectedMediaSource = ref([])
// 导入代码弹窗
const importCodeDialog = ref(false)
@@ -54,8 +75,7 @@ async function queryCustomFilters() {
}
})
}
}
catch (error) {
} catch (error) {
console.log(error)
}
}
@@ -73,17 +93,11 @@ async function saveCustomFilters() {
.join('>')
}
// 保存
const result: { [key: string]: any } = await api.post(
'system/setting/SearchFilterRules',
value,
)
const result: { [key: string]: any } = await api.post('system/setting/SearchFilterRules', value)
if (result.success)
$toast.success('搜索优先级保存成功')
else
$toast.error('搜索优先级保存失败!')
}
catch (error) {
if (result.success) $toast.success('搜索优先级保存成功')
else $toast.error('搜索优先级保存失败!')
} catch (error) {
console.log(error)
}
}
@@ -91,8 +105,7 @@ async function saveCustomFilters() {
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = filterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
if (card) card.rules = rules
}
// 移除卡片
@@ -128,8 +141,7 @@ async function querySites() {
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
querySelectedSites()
}
catch (error) {
} catch (error) {
console.log(error)
}
}
@@ -140,8 +152,7 @@ async function querySelectedSites() {
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
selectedSites.value = result.data?.value ?? []
}
catch (error) {
} catch (error) {
console.log(error)
}
}
@@ -152,12 +163,9 @@ async function saveSelectedSites() {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
if (result.success)
$toast.success('搜索站点保存成功')
else
$toast.error('搜索站点保存失败!')
}
catch (error) {
if (result.success) $toast.success('搜索站点保存成功')
else $toast.error('搜索站点保存失败!')
} catch (error) {
console.log(error)
}
}
@@ -166,13 +174,11 @@ async function saveSelectedSites() {
function onLevelUp(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
if (!card) return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.value.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
if (!prevCard) return
// 交换两张卡片的优先级
const temp = card.pri
@@ -187,13 +193,11 @@ function onLevelUp(pri: string) {
function onLevelDown(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
if (!card) return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.value.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
if (!nextCard) return
// 交换两张卡片的优先级
const temp = card.pri
@@ -207,13 +211,9 @@ function onLevelDown(pri: string) {
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultSearchFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
const result: { [key: string]: any } = await api.get('system/setting/DefaultSearchFilterRules')
if (result.data?.value) defaultFilterRules.value = result.data?.value
} catch (error) {
console.log(error)
}
}
@@ -225,12 +225,9 @@ async function saveDefaultFilter() {
'system/setting/DefaultSearchFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
if (result.success) $toast.success('默认包含/排除规则保存成功')
else $toast.error('默认包含/排除规则保存失败!')
} catch (error) {
console.log(error)
}
}
@@ -238,8 +235,7 @@ async function saveDefaultFilter() {
// 分享规则
function shareRules() {
// 有值才处理
if (filterCards.value.length === 0)
return
if (filterCards.value.length === 0) return
// 将卡片规则接装为字符串
const value = filterCards.value
@@ -251,22 +247,18 @@ function shareRules() {
try {
copyToClipboard(value)
$toast.success('优先级规则已复制到剪贴板')
}
catch (error) {
} catch (error) {
$toast.error('优先级规则复制失败!')
}
}
// 监听导入代码变化
watchEffect(() => {
if (!importCodeString.value)
return
if (!importCodeString.value) return
// 导入代码需要以空格开头和结束,没有则拼接
if (!importCodeString.value.startsWith(' '))
importCodeString.value = ` ${importCodeString.value}`
if (!importCodeString.value.endsWith(' '))
importCodeString.value = `${importCodeString.value} `
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
// 将导入的代码转换为规则卡片
const groups = importCodeString.value.split('>')
@@ -278,15 +270,67 @@ watchEffect(() => {
})
})
// 调用API查询下载器设置
async function loadMediaSourceSetting() {
try {
const result1: { [key: string]: any } = await api.get('system/setting/SEARCH_SOURCE')
if (result1.success) selectedMediaSource.value = result1.data?.value?.split(',')
} catch (error) {
console.log(error)
}
}
// 调用API保存下载器设置
async function saveMediaSourceSetting() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/SEARCH_SOURCE',
selectedMediaSource.value.join(','),
)
if (result.success) {
$toast.success('保存媒体数据源设置成功')
} else {
$toast.error('保存媒体数据源设置失败!')
}
} catch (error) {
console.log(error)
}
}
onMounted(() => {
queryCustomFilters()
querySites()
queryDefaultFilter()
loadMediaSourceSetting()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="媒体数据源">
<VCardSubtitle> 设定搜索时展示哪些源的媒体信息</VCardSubtitle>
<VCardText>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedMediaSource"
multiple
chips
:items="mediaSourcesDict"
label="当前使用数据源"
hint="选中多项时会同时展示来自不同数据源的搜索结果,选择的数据源顺序将会影响搜索结果的排序"
/>
</VCol>
</VRow>
</VCardText>
<VCardItem>
<VBtn type="submit" @click="saveMediaSourceSetting"> 保存 </VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="搜索站点">
<VCardSubtitle> 只有选中的站点才会在搜索中使用</VCardSubtitle>
@@ -307,9 +351,7 @@ onMounted(() => {
</VCardItem>
<VCardItem>
<VBtn type="submit" @click="saveSelectedSites">
保存
</VBtn>
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
</VCardItem>
</VCard>
</VCol>
@@ -318,24 +360,15 @@ onMounted(() => {
<template #append>
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu
activator="parent"
close-on-content-click
>
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
variant="plain"
@click="shareRules"
>
<VListItem variant="plain" @click="shareRules">
<template #prepend>
<VIcon icon="mdi-share" />
</template>
<VListItemTitle>分享</VListItemTitle>
</VListItem>
<VListItem
variant="plain"
@click="importCodeDialog = true"
>
<VListItem variant="plain" @click="importCodeDialog = true">
<template #prepend>
<VIcon icon="mdi-import" />
</template>
@@ -362,18 +395,8 @@ onMounted(() => {
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters()"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard()"
>
<VBtn type="submit" class="me-2" @click="saveCustomFilters()"> 保存 </VBtn>
<VBtn color="success" variant="tonal" @click="addFilterCard()">
<VIcon icon="mdi-plus" />
</VBtn>
</VCardItem>
@@ -401,30 +424,35 @@ onMounted(() => {
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.min_seeders"
type="text"
label="最小做种数"
placeholder="0"
hint="小于该值的资源将被过滤掉0表示不过滤"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.min_seeders_time"
type="text"
label="最少做种人数生效发布时间(分钟)"
placeholder="0"
hint="发布时间距现在大于该值的资源将生效最小做种数规则0表示不生效"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
<VDialog
v-model="importCodeDialog"
width="60rem"
scrollable
>
<ImportCodeForm
v-model="importCodeString"
title="导入优先级规则"
@close="importCodeDialog = false"
/>
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
</VDialog>
</template>

View File

@@ -4,7 +4,7 @@ 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'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型
interface FilterCard {
@@ -42,7 +42,7 @@ const defaultFilterRules = ref({
movie_size: '',
tv_size: '',
min_seeders: 0,
show_edit_dialog: false,
min_seeders_time: 0
})
// 订阅模式选择项
@@ -595,7 +595,7 @@ onMounted(() => {
hint="支持正式表达式,多个关键字用 | 分隔表示或"
/>
</VCol>
<VCol cols="12" md="4">
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.movie_size"
type="text"
@@ -604,7 +604,7 @@ onMounted(() => {
hint="格式0-30表示0到30GB之间的资源"
/>
</VCol>
<VCol cols="12" md="4">
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.tv_size"
type="text"
@@ -613,7 +613,7 @@ onMounted(() => {
hint="格式0-10表示0到10GB之间的资源"
/>
</VCol>
<VCol cols="12" md="4">
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.min_seeders"
type="text"
@@ -623,10 +623,12 @@ onMounted(() => {
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="defaultFilterRules.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后,添加订阅时将自动弹出订阅编辑框,要设置更多订阅选项"
<VTextField
v-model="defaultFilterRules.min_seeders_time"
type="text"
label="最少做种人数生效发布时间(分钟)"
placeholder="0"
hint="发布时间距现在大于该值的资源将生效最小做种数规则0表示不生效"
/>
</VCol>
</VRow>
@@ -648,7 +650,7 @@ onMounted(() => {
width="60rem"
scrollable
>
<ImportCodeForm
<ImportCodeDialog
v-model="importCodeString"
title="导入优先级规则"
@close="importCodeDialog = false"

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import { useDefer } from '@/@core/utils/dom'
// 数据列表
@@ -24,8 +24,7 @@ async function fetchData() {
dataList.value = await api.get('site/')
isRefreshed.value = true
defer = useDefer(dataList.value.length)
}
catch (error) {
} catch (error) {
console.error(error)
}
}
@@ -35,32 +34,10 @@ onBeforeMount(fetchData)
</script>
<template>
<div
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div
v-if="dataList.length > 0"
class="grid gap-3 grid-site-card"
>
<div
v-for="(data, index) in dataList"
:key="index"
>
<SiteCard
v-if="defer(index)"
:key="data.id"
:site="data"
@remove="fetchData"
@update="fetchData"
/>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="dataList.length > 0" class="grid gap-3 grid-site-card">
<div v-for="(data, index) in dataList" :key="index">
<SiteCard v-if="defer(index)" :key="data.id" :site="data" @remove="fetchData" @update="fetchData" />
</div>
</div>
<NoDataFound
@@ -70,21 +47,18 @@ onBeforeMount(fetchData)
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- 新增站点按钮 -->
<VFab
icon="mdi-plus"
location="bottom end"
size="x-large"
fixed
app
appear
@click="siteAddDialog = true"
/>
<VFab icon="mdi-plus" location="bottom end" size="x-large" fixed app appear @click="siteAddDialog = true" />
<!-- 新增站点弹窗 -->
<SiteAddEditForm
<SiteAddEditDialog
v-if="siteAddDialog"
v-model="siteAddDialog"
oper="add"
@save="siteAddDialog = false; fetchData()"
@save="
() => {
siteAddDialog = false
fetchData()
}
"
@close="siteAddDialog = false"
/>
</template>

View File

@@ -155,7 +155,7 @@ onMounted(() => {
</VCard>
</div>
<div class="md:hidden">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<template #activator="{ props }">
<VImg
height="60"
@@ -384,8 +384,8 @@ onMounted(() => {
}
.v-application .fc .fc-daygrid-day-number {
padding-block: 0rem;
padding-inline: 0rem;
padding-block: 0;
padding-inline: 0;
}
.v-application .fc .fc-list-event-dot {
@@ -435,7 +435,7 @@ onMounted(() => {
margin-inline-end: 0.25rem;
}
@media (max-width: 1264px) {
@media (width <= 1264px) {
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button {
display: block !important;
}
@@ -481,10 +481,10 @@ onMounted(() => {
}
.v-application .fc .fc-button-primary {
background-color: transparent;
border: none;
outline: none;
background-color: transparent;
color: var(--v-theme-on-surface);
outline: none;
}
.v-application .fc .fc-button-primary:hover {
@@ -492,7 +492,7 @@ onMounted(() => {
color: rgb(var(--v-theme-primary));
}
@media (max-width: 776px) {
@media (width <= 776px) {
.fc-daygrid-event-harness {
display: flex;
align-items: center;

View File

@@ -4,7 +4,8 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import store from '@/store'
// 输入参数
@@ -21,6 +22,9 @@ const dataList = ref<Subscribe[]>([])
// 弹窗
const subscribeEditDialog = ref(false)
// 历史记录弹窗
const historyDialog = ref(false)
// 获取订阅列表数据
async function fetchData() {
try {
@@ -58,17 +62,10 @@ const filteredDataList = computed(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"
@@ -102,8 +99,19 @@ const filteredDataList = computed(() => {
appear
@click="subscribeEditDialog = true"
/>
<VFab
icon="mdi-history"
color="info"
location="bottom end"
class="mb-2"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:default="true"
@@ -111,6 +119,14 @@ const filteredDataList = computed(() => {
@save="subscribeEditDialog = false"
@close="subscribeEditDialog = false"
/>
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"
v-model="historyDialog"
:type="props.type"
@close="historyDialog = false"
@save="() => {historyDialog = false; fetchData()}"
/>
</template>
<style lang="scss">

View File

@@ -55,38 +55,39 @@ async function loadMessages({ done }: { done: any }) {
done('ok')
return
}
// 设置加载中
loading.value = true
try {
// 设置加载中
loading.value = true
currData.value = await api.get('message/web', {
params: {
page: page.value,
size: 20,
},
})
// 已加载过
isLoaded.value = true
if (currData.value.length > 0) {
// 取最后一条时间为存量消息最新时间
lastTime.value = currData.value[currData.value.length - 1].reg_time ?? ''
// 合并数据
messages.value = [...currData.value, ...messages.value]
// 加载完成
done('ok')
if (page.value === 1) {
// 滚动到底部
emit('scroll')
// 监听SSE消息
startSSEMessager()
}
// 页码+1
page.value++
// 完成
done('ok')
}
else {
done('ok')
// 监听SSE消息
startSSEMessager()
// 没有新数据
done('empty')
}
// 取消加载中
loading.value = false
isLoaded.value = true
// 监听SSE消息
startSSEMessager()
}
catch (error) {
console.error(error)
@@ -99,7 +100,7 @@ function compareTime(time1: string, time2: string) {
return -1
if (!time2)
return 1
return new Date(time1).getTime() - new Date(time2).getTime()
return new Date(time1.replaceAll(/-/g, '/')).getTime() - new Date(time2.replaceAll(/-/g, '/')).getTime()
}
onBeforeUnmount(() => {
@@ -110,20 +111,18 @@ onBeforeUnmount(() => {
<template>
<VInfiniteScroll
mode="intersect"
:mode="!isLoaded ? 'intersect' : 'manual'"
side="start"
:items="messages"
class="overflow-hidden"
@load="loadMessages"
load-more-text="加载更多 ..."
>
<template #loading>
<VProgressCircular
v-if="loading"
indeterminate
size="48"
class="mb-5"
color="primary"
/>
<LoadingBanner />
</template>
<template #empty>
没有更多数据
</template>
<div>
<VRow

View File

@@ -32,26 +32,22 @@ async function moduleTest(index: number) {
if (result.success) {
target.state = 'success'
target.name = `${target.name} - 正常`
}
else if (result.message?.includes('模块未加载')) {
} else if (result.message?.includes('模块未加载')) {
target.state = ''
target.name = `${target.name} - 未启用`
}
else {
} else {
target.state = 'error'
target.name = `${target.name} - 错误!`
target.errmsg = result.message
}
}
catch (error) {
} catch (error) {
console.error(error)
}
}
// 加载
onMounted(async () => {
// 逐个检查所有模块
for (let i = 0; i < modules.value.length; i++)
await moduleTest(i)
for (let i = 0; i < modules.value.length; i++) await moduleTest(i)
})
</script>
@@ -66,10 +62,7 @@ onMounted(async () => {
>
{{ module.errmsg }}
<template #append>
<VProgressCircular
v-if="module.loading"
indeterminate
/>
<VProgressCircular v-if="module.loading" indeterminate />
</template>
</VAlert>
</template>

View File

@@ -2228,7 +2228,7 @@
dependencies:
upath "^2.0.1"
"@vueuse/core@^10.1.2":
"@vueuse/core@^10.0.0", "@vueuse/core@^10.1.2":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.9.0.tgz#7d779a95cf0189de176fee63cee4ba44b3c85d64"
integrity sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==
@@ -3057,6 +3057,11 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -5970,6 +5975,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
ramda@>=0.28.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.0.tgz#3cc4f0ddddfa6334dad2f371bd72c33237d92cd0"
integrity sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -6208,6 +6218,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rxjs@^7.8.0:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
safe-array-concat@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb"
@@ -6482,6 +6499,7 @@ stop-iteration-iterator@^1.0.0:
internal-slot "^1.0.4"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6555,6 +6573,7 @@ stringify-object@^3.3.0:
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6989,7 +7008,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.3.1:
tslib@^2.1.0, tslib@^2.3.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@@ -7438,6 +7457,15 @@ vue-tsc@^2.0.10:
"@vue/language-core" "2.0.10"
semver "^7.5.4"
vue-virtual-scroll-grid@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-grid/-/vue-virtual-scroll-grid-1.11.0.tgz#83daa6af439f0b66283356faa4d45156eda2ffaf"
integrity sha512-f3hBqQgdbSVg8srhun/7nDkGBE/GOAXzyZH3fcgqO59y2iCfBoll2kUe9T42GAqXvslHzlTwNSPcwEyMsiKz3A==
dependencies:
"@vueuse/core" "^10.0.0"
ramda ">=0.28.0"
rxjs "^7.8.0"
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"
@@ -7468,10 +7496,10 @@ vue@^3.3.2:
"@vue/server-renderer" "3.4.21"
"@vue/shared" "3.4.21"
vuetify-use-dialog@^0.6.0:
version "0.6.9"
resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.9.tgz#d1ba116331f885af75b6d68ddbebbdb12c3facc6"
integrity sha512-wInJvEMZQAuqYGwqniuiuvpxfwUdEe9ZDpezc2rPdrwm/Wx6JoTIva0qTXeKBIovik/IbuZ1wIV1W3mLP+ksow==
vuetify-use-dialog@^0.6.11:
version "0.6.11"
resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.11.tgz#8800cc56b234dae1dfa44a7f06a6bb1a33ad4b39"
integrity sha512-iPAu6MsN8suuNAS1M6JN2CaOXRgr7LZ2u+UNtAw0Fi3AjianzVIrnRNhQcAZjmE8Hu6ZwAbgte1p47qU6OazLw==
dependencies:
defu "^6.1.4"