mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 01:50:10 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdbcef5232 | ||
|
|
d5d6bfdc56 | ||
|
|
75ae7f0c15 | ||
|
|
6931451f18 | ||
|
|
f5625e1354 | ||
|
|
d1be4a30b6 | ||
|
|
5c13362db2 | ||
|
|
6c71dce80c | ||
|
|
790c397951 | ||
|
|
e28e74b874 | ||
|
|
b99ea22d89 | ||
|
|
8938195c5d | ||
|
|
887b4a7862 | ||
|
|
7c9c39fa0e | ||
|
|
3b800753ec | ||
|
|
647119052c | ||
|
|
e9ce6bbd4e | ||
|
|
1fee27f78e | ||
|
|
e7a334861d | ||
|
|
267ae3436d | ||
|
|
60ff9f1891 | ||
|
|
f83efd23df | ||
|
|
db60f02745 | ||
|
|
3e109bd27c | ||
|
|
c4ccf6e3fa | ||
|
|
fb1a246e4a | ||
|
|
a418b03c06 | ||
|
|
e9fee000ca | ||
|
|
71c13e0653 | ||
|
|
32d7f933f8 | ||
|
|
f28dd810ce | ||
|
|
aaedd88ca7 | ||
|
|
00dee40917 | ||
|
|
019248b605 | ||
|
|
826f37bcc4 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -39,6 +39,7 @@ jobs:
|
||||
run: |
|
||||
yarn
|
||||
yarn build
|
||||
echo "$frontend_version" > dist/version.txt
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Generate Release
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.8.3-1",
|
||||
"version": "1.8.4",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
// 是否显示
|
||||
innerClass: String,
|
||||
})
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['click', 'update:modelValue'])
|
||||
// 按钮点击
|
||||
@@ -9,10 +14,7 @@ function onClick() {
|
||||
</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>
|
||||
|
||||
@@ -11,6 +11,10 @@ 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'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -58,7 +62,7 @@ const pluginInfoDialog = ref(false)
|
||||
const progressText = ref('正在更新插件...')
|
||||
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
let pluginPageItems = ref([])
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
@@ -70,12 +74,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 +97,7 @@ function showUpdateHistory() {
|
||||
// 检查当前版本是否有更新日志
|
||||
if (isNullOrEmptyObject(props.plugin?.history)) {
|
||||
updatePlugin()
|
||||
} else{
|
||||
} else {
|
||||
releaseDialog.value = true
|
||||
}
|
||||
}
|
||||
@@ -110,11 +117,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 +131,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 +145,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 +156,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 +166,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 +185,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,8 +215,7 @@ 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?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
@@ -243,8 +238,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 +246,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 +262,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 +277,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 +293,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 +378,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 +416,7 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
>
|
||||
<VAvatar size="8rem">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
@@ -465,116 +435,63 @@ 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 v-model='pluginConfigDialog' />
|
||||
<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 v-model='pluginInfoDialog' />
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<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 +509,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>
|
||||
|
||||
@@ -8,6 +8,10 @@ import api from '@/api'
|
||||
import type { Site, SiteStatistic } from '@/api/types'
|
||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const cardProps = defineProps({
|
||||
@@ -66,8 +70,7 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -79,17 +82,14 @@ 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) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,8 @@ async function testSite() {
|
||||
// 查询站点使用统计
|
||||
async function getSiteStats() {
|
||||
try {
|
||||
siteStats.value = (await api.get(`site/statistic/${cardProps.site?.domain}`))
|
||||
}
|
||||
catch (error) {
|
||||
siteStats.value = await api.get(`site/statistic/${cardProps.site?.domain}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -117,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
|
||||
@@ -127,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)
|
||||
}
|
||||
}
|
||||
@@ -158,25 +150,21 @@ function openSitePage() {
|
||||
|
||||
// 根据站点状态显示不同的状态图标
|
||||
const statColor = computed(() => {
|
||||
if (isNullOrEmptyObject(siteStats.value)){
|
||||
if (isNullOrEmptyObject(siteStats.value)) {
|
||||
return 'secondary'
|
||||
}
|
||||
if (siteStats.value?.lst_state == 1){
|
||||
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'
|
||||
} 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()
|
||||
watch(resourceDialog, value => {
|
||||
if (!value) getSiteStats()
|
||||
})
|
||||
|
||||
// 装载时查询站点图标
|
||||
@@ -195,11 +183,7 @@ 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>
|
||||
@@ -216,83 +200,41 @@ onMounted(() => {
|
||||
<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>
|
||||
@@ -307,49 +249,29 @@ onMounted(() => {
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteCookieDialog"
|
||||
max-width="50rem"
|
||||
>
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<!-- 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>
|
||||
@@ -357,12 +279,7 @@ onMounted(() => {
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="updateSiteCookie"
|
||||
>
|
||||
开始更新
|
||||
</VBtn>
|
||||
<VBtn variant="tonal" @click="updateSiteCookie"> 开始更新 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
@@ -370,7 +287,12 @@ onMounted(() => {
|
||||
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"
|
||||
/>
|
||||
@@ -381,6 +303,7 @@ onMounted(() => {
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
@@ -390,21 +313,11 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</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>
|
||||
|
||||
@@ -4,6 +4,10 @@ import TmdbSelector from '../misc/TmdbSelector.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -55,7 +59,6 @@ const transferForm = reactive({
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -72,7 +75,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 +92,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 +102,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 +144,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 +153,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 +161,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 +178,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 +201,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 +256,23 @@ 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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<VProgressLinear v-if="progressValue" color="white" class="mb-0 mt-1" :model-value="progressValue" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- TMDB ID搜索框 -->
|
||||
<VDialog
|
||||
v-model="tmdbSelectorDialog"
|
||||
width="40rem"
|
||||
scrollable
|
||||
max-height="85vh"
|
||||
>
|
||||
<TmdbSelector
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -192,10 +170,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 +178,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 +186,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 +196,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 +210,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>
|
||||
|
||||
@@ -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({
|
||||
@@ -44,7 +48,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
current_priority: 0,
|
||||
save_path: '',
|
||||
date: '',
|
||||
show_edit_dialog: false
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -59,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)
|
||||
}
|
||||
}
|
||||
@@ -71,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)
|
||||
}
|
||||
}
|
||||
@@ -96,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)
|
||||
}
|
||||
}
|
||||
@@ -118,8 +111,7 @@ async function loadSites() {
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
siteList.value = data.filter(item => item.is_active)
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -127,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,
|
||||
@@ -143,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)
|
||||
}
|
||||
}
|
||||
@@ -158,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)
|
||||
}
|
||||
}
|
||||
@@ -258,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"
|
||||
@@ -290,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="总集数"
|
||||
@@ -302,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="开始集数"
|
||||
@@ -316,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"
|
||||
@@ -383,9 +326,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="subscribeForm.save_path"
|
||||
label="保存路径"
|
||||
@@ -394,30 +335,21 @@ 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"
|
||||
>
|
||||
<VCol v-if="props.default" cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.show_edit_dialog"
|
||||
label="订阅时编辑更多规则"
|
||||
@@ -429,14 +361,9 @@ onMounted(() => {
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api';
|
||||
import { Subscribe } from '@/api/types';
|
||||
import api from '@/api'
|
||||
import { Subscribe } from '@/api/types'
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -77,14 +81,12 @@ async function loadHistory({ done }: { done: any }) {
|
||||
|
||||
// 重新订阅
|
||||
async function reSubscribe(item: Subscribe) {
|
||||
if (item.type === '电影')
|
||||
progressText.value = `正在重新订阅 ${item.name} ...`
|
||||
else
|
||||
progressText.value = `正在重新订阅 ${item.name} 第 ${item.season} 季 ...`
|
||||
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){
|
||||
const result: { [key: string]: any } = await api.post('subscribe', item)
|
||||
if (result.success) {
|
||||
emit('save')
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -96,9 +98,9 @@ async function reSubscribe(item: Subscribe) {
|
||||
// 删除记录
|
||||
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)
|
||||
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)
|
||||
@@ -124,35 +126,25 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-delete',
|
||||
click: deleteHistory,
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
max-width="50rem"
|
||||
max-height="90vh"
|
||||
>
|
||||
<VCard
|
||||
class="mx-auto"
|
||||
width="100%"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<DialogCloseBtn
|
||||
@click="
|
||||
() => {
|
||||
emit('close')
|
||||
}
|
||||
"
|
||||
/>
|
||||
<VList lines="two">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
|
||||
<template #loading>
|
||||
<LoadingBanner />
|
||||
</template>
|
||||
@@ -186,13 +178,8 @@ const dropdownItems = ref([
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<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"
|
||||
@@ -217,21 +204,11 @@ const dropdownItems = ref([
|
||||
</VList>
|
||||
</VCard>
|
||||
<!-- 进度框 -->
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import { type PropType, ref } from 'vue'
|
||||
|
||||
// 定议外部事件
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
// 组件接口
|
||||
interface RenderProps {
|
||||
component: string
|
||||
@@ -9,6 +14,7 @@ interface RenderProps {
|
||||
content?: any
|
||||
slots?: any
|
||||
props?: any
|
||||
events?: any
|
||||
}
|
||||
|
||||
// 输入参数
|
||||
@@ -16,43 +22,64 @@ const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
})
|
||||
|
||||
// 配置元素
|
||||
const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
props: {},
|
||||
content: [],
|
||||
slots: {},
|
||||
})
|
||||
// 元素API事件响应
|
||||
async function commonAction(api_path: string, method: string, params = {}) {
|
||||
if (!api_path || !method) return
|
||||
if (method.toUpperCase() === 'GET') {
|
||||
await api.get(api_path, {
|
||||
params: params,
|
||||
})
|
||||
} else {
|
||||
await api.post(api_path, params)
|
||||
}
|
||||
emit('action')
|
||||
}
|
||||
|
||||
// 组装事件
|
||||
let componentEvents: { [key: string]: any } = {}
|
||||
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
|
||||
}
|
||||
} else {
|
||||
componentEvents = {}
|
||||
}
|
||||
</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 }}
|
||||
<template v-for="(content, name) in (formItem.slots || [])" :key="name" v-slot:[name]="{_props}">
|
||||
{{ 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 || [])"
|
||||
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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,10 @@ 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 = {
|
||||
@@ -56,8 +60,8 @@ function setDashboardConfig() {
|
||||
// 保存到服务端
|
||||
api.post('/user/config/Dashboard', data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
dialog.value = false
|
||||
}
|
||||
@@ -65,131 +69,63 @@ function setDashboardConfig() {
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useStore } from 'vuex'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
@@ -48,9 +49,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,22 +64,22 @@ 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)
|
||||
if (data != localStorage.getItem('MP_DASHBOARD')) {
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试加载用户监控面板配置(本地无配置时才加载)
|
||||
async function tryLoadDashboardConfig() {
|
||||
if (localStorage.getItem("MP_DASHBOARD")) {
|
||||
if (localStorage.getItem('MP_DASHBOARD')) {
|
||||
return
|
||||
}
|
||||
await loadDashboardConfig()
|
||||
@@ -128,22 +128,17 @@ function login() {
|
||||
store.dispatch('auth/updateSuperUser', superuser)
|
||||
store.dispatch('auth/updateUserName', username)
|
||||
store.dispatch('auth/updateAvatar', avatar)
|
||||
|
||||
|
||||
// 登录后处理
|
||||
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},请检查用户名、密码或双重验证码是否正确`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,8 +151,7 @@ onMounted(() => {
|
||||
// 如果token存在,且保持登录状态为true,则跳转到首页
|
||||
if (token && remember) {
|
||||
router.push('/')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 获取背景图片
|
||||
fetchBackgroundImage()
|
||||
}
|
||||
@@ -186,16 +180,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">
|
||||
@@ -214,42 +203,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>
|
||||
@@ -262,7 +231,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;
|
||||
|
||||
@@ -502,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>
|
||||
@@ -528,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" />
|
||||
|
||||
@@ -6,6 +6,10 @@ 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'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 已安装插件列表
|
||||
const dataList = ref<Plugin[]>([])
|
||||
@@ -58,15 +62,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 +77,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 +91,7 @@ function openPlugin(item: Plugin) {
|
||||
if (item.installed === true) {
|
||||
// 标记插件动作
|
||||
pluginActions.value[item.id || '0'] = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 如果是未安装插件则安装
|
||||
installPlugin(item)
|
||||
}
|
||||
@@ -113,8 +111,7 @@ 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?imgurl=${encodeURIComponent(item?.plugin_icon)}`
|
||||
@@ -145,8 +142,7 @@ async function fetchInstalledPlugins() {
|
||||
},
|
||||
})
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -171,8 +167,7 @@ async function fetchUninstalledPlugins() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -181,8 +176,7 @@ async function fetchUninstalledPlugins() {
|
||||
async function getPluginStatistics() {
|
||||
try {
|
||||
PluginStatistics.value = await api.get('plugin/statistic')
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -196,8 +190,7 @@ function refreshData() {
|
||||
// 对uninstalledList进行排序,按PluginStatistics倒序
|
||||
const sortedUninstalledList = computed(() => {
|
||||
const list = uninstalledList.value.filter(item => !item.has_update)
|
||||
if (PluginStatistics.value.length === 0)
|
||||
return list
|
||||
if (PluginStatistics.value.length === 0) return list
|
||||
return list.sort((a, b) => {
|
||||
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
|
||||
})
|
||||
@@ -211,14 +204,8 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
/>
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-4 grid-plugin-card"
|
||||
>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<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}`"
|
||||
@@ -237,15 +224,7 @@ onBeforeMount(() => {
|
||||
error-description="点击右下角按钮,前往插件市场安装插件。"
|
||||
/>
|
||||
<!-- App市场 -->
|
||||
<VFab
|
||||
icon="mdi-store-plus"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="PluginAppDialog = true"
|
||||
/>
|
||||
<VFab icon="mdi-store-plus" location="bottom end" size="x-large" fixed app appear @click="PluginAppDialog = true" />
|
||||
<VDialog
|
||||
v-if="PluginAppDialog"
|
||||
v-model="PluginAppDialog"
|
||||
@@ -265,23 +244,14 @@ onBeforeMount(() => {
|
||||
<VSpacer />
|
||||
|
||||
<VToolbarItems>
|
||||
<VBtn
|
||||
size="x-large"
|
||||
@click="pluginDialogClose"
|
||||
>
|
||||
<VIcon
|
||||
color="white"
|
||||
icon="mdi-close"
|
||||
/>
|
||||
<VBtn size="x-large" @click="pluginDialogClose">
|
||||
<VIcon color="white" icon="mdi-close" />
|
||||
</VBtn>
|
||||
</VToolbarItems>
|
||||
</VToolbar>
|
||||
</div>
|
||||
<VCardText>
|
||||
<LoadingBanner
|
||||
v-if="!isAppMarketLoaded"
|
||||
class="mt-12"
|
||||
/>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||
<PluginAppCard
|
||||
v-for="data in sortedUninstalledList"
|
||||
@@ -319,12 +289,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"
|
||||
@@ -338,20 +306,12 @@ onBeforeMount(() => {
|
||||
/>
|
||||
</VToolbar>
|
||||
<DialogCloseBtn @click="closeSearchDialog" />
|
||||
<VList
|
||||
v-if="filterPlugins.length > 0"
|
||||
lines="two"
|
||||
>
|
||||
<VList v-if="filterPlugins.length > 0" lines="two">
|
||||
<template v-for="(item, i) in filterPlugins" :key="i">
|
||||
<VListItem
|
||||
@click="openPlugin(item)"
|
||||
>
|
||||
<VListItem @click="openPlugin(item)">
|
||||
<template #prepend>
|
||||
<VAvatar>
|
||||
<VImg
|
||||
:src="pluginIcon(item)"
|
||||
@error="pluginIconError(item)"
|
||||
>
|
||||
<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" />
|
||||
@@ -362,13 +322,7 @@ onBeforeMount(() => {
|
||||
</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"
|
||||
/>
|
||||
<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>
|
||||
@@ -377,21 +331,11 @@ onBeforeMount(() => {
|
||||
</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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { ref, unref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
@@ -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"
|
||||
@@ -389,7 +373,7 @@ onMounted(fetchData)
|
||||
</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">
|
||||
@@ -397,14 +381,10 @@ onMounted(fetchData)
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip v-if="item?.status" color="success" size="small">
|
||||
成功
|
||||
</VChip>
|
||||
<VChip v-if="item?.status" color="success" size="small"> 成功 </VChip>
|
||||
<v-tooltip 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>
|
||||
</template>
|
||||
@@ -432,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
|
||||
@@ -466,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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
允许 JPG、GIF 或 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">
|
||||
允许 JPG、GIF 或 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 Authenticator、Microsoft Authenticator、Authy或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 Authenticator、Microsoft Authenticator、Authy或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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user