mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 08:06:43 +08:00
Merge branch 'dev' into dev-user
This commit is contained in:
+1
-1
@@ -101,4 +101,4 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"postcss": "8"
|
"postcss": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,19 +60,25 @@ export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : valu
|
|||||||
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
|
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
|
||||||
|
|
||||||
// 格式化为xx[TGMK]B
|
// 格式化为xx[TGMK]B
|
||||||
export function formatFileSize(bytes: number, decimals = 2) {
|
export function formatFileSize(bytes: number, decimals = 2, prefix = false) {
|
||||||
if (bytes < 0) throw new Error('字节数不能为负数。')
|
// 负数标记
|
||||||
|
let negative = false
|
||||||
|
let size = bytes
|
||||||
|
if (bytes < 0) {
|
||||||
|
negative = true
|
||||||
|
size = Math.abs(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
let size = bytes
|
|
||||||
let unitIndex = 0
|
let unitIndex = 0
|
||||||
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
size /= 1024
|
size /= 1024
|
||||||
unitIndex++
|
unitIndex++
|
||||||
}
|
}
|
||||||
|
if (negative) return `-${size.toFixed(decimals)} ${units[unitIndex]}`
|
||||||
return `${size.toFixed(decimals)} ${units[unitIndex]}`
|
else
|
||||||
|
return prefix ? `+${size.toFixed(decimals)} ${units[unitIndex]}` : `${size.toFixed(decimals)} ${units[unitIndex]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将时间秒格式化为时分秒
|
// 将时间秒格式化为时分秒
|
||||||
|
|||||||
@@ -435,6 +435,48 @@ export interface SiteStatistic {
|
|||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 站点用户数据
|
||||||
|
export interface SiteUserData {
|
||||||
|
// 站点域名
|
||||||
|
domain?: string
|
||||||
|
// 用户名
|
||||||
|
username?: string
|
||||||
|
// 用户ID
|
||||||
|
userid?: number
|
||||||
|
// 用户等级
|
||||||
|
user_level?: string
|
||||||
|
// 加入时间
|
||||||
|
join_at?: string
|
||||||
|
// 积分
|
||||||
|
bonus?: number // 默认为 0.0
|
||||||
|
// 上传量
|
||||||
|
upload?: number // 默认为 0
|
||||||
|
// 下载量
|
||||||
|
download?: number // 默认为 0
|
||||||
|
// 分享率
|
||||||
|
ratio?: number // 默认为 0
|
||||||
|
// 做种数
|
||||||
|
seeding?: number // 默认为 0
|
||||||
|
// 下载数
|
||||||
|
leeching?: number // 默认为 0
|
||||||
|
// 做种体积
|
||||||
|
seeding_size?: number // 默认为 0
|
||||||
|
// 下载体积
|
||||||
|
leeching_size?: number // 默认为 0
|
||||||
|
// 做种人数, 种子大小
|
||||||
|
seeding_info?: any[] // 默认为空数组
|
||||||
|
// 未读消息
|
||||||
|
message_unread?: number // 默认为 0
|
||||||
|
// 未读消息内容
|
||||||
|
message_unread_contents?: any[] // 默认为空数组
|
||||||
|
// 错误信息
|
||||||
|
err_msg?: string | null // 默认为 null
|
||||||
|
// 更新日期
|
||||||
|
updated_day?: string
|
||||||
|
// 更新时间
|
||||||
|
updated_time?: string
|
||||||
|
}
|
||||||
|
|
||||||
// 正在下载
|
// 正在下载
|
||||||
export interface DownloadingInfo {
|
export interface DownloadingInfo {
|
||||||
// HASH
|
// HASH
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ const getImgUrl: Ref<string> = computed(() => {
|
|||||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||||
// 如果地址中包含douban则使用中转代理
|
// 如果地址中包含douban则使用中转代理
|
||||||
if (url.includes('doubanio.com'))
|
if (url.includes('doubanio.com'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const props = defineProps({
|
|||||||
// 所有媒体服务器
|
// 所有媒体服务器
|
||||||
mediaservers: {
|
mediaservers: {
|
||||||
type: Array as PropType<MediaServerConf[]>,
|
type: Array as PropType<MediaServerConf[]>,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
@@ -46,7 +46,12 @@ const infoItems = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
// 同步媒体库选项
|
// 同步媒体库选项
|
||||||
const librariesOptions = ref<{ title: string; value: string | undefined }[]>([])
|
const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: 'all',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
// 媒体服务器详情弹窗
|
// 媒体服务器详情弹窗
|
||||||
const mediaServerInfoDialog = ref(false)
|
const mediaServerInfoDialog = ref(false)
|
||||||
@@ -68,6 +73,9 @@ function openMediaServerInfoDialog() {
|
|||||||
mediaServerInfo.value = props.mediaserver
|
mediaServerInfo.value = props.mediaserver
|
||||||
mediaServerName.value = props.mediaserver.name
|
mediaServerName.value = props.mediaserver.name
|
||||||
mediaServerInfoDialog.value = true
|
mediaServerInfoDialog.value = true
|
||||||
|
if (!props.mediaserver.sync_libraries) {
|
||||||
|
mediaServerInfo.value.sync_libraries = ['all']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存详情数据
|
// 保存详情数据
|
||||||
@@ -78,7 +86,7 @@ function saveMediaServerInfo() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 重名判断
|
// 重名判断
|
||||||
if (props.mediaservers.some(item => item.name === mediaServerName.value && item!== props.mediaserver)) {
|
if (props.mediaservers.some(item => item.name === mediaServerName.value && item !== props.mediaserver)) {
|
||||||
$toast.error(`【${mediaServerName.value}】已存在,请替换为其他名称`)
|
$toast.error(`【${mediaServerName.value}】已存在,请替换为其他名称`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,7 +156,13 @@ async function loadLibrary(server: string) {
|
|||||||
title: item.name,
|
title: item.name,
|
||||||
value: item.id?.toString(),
|
value: item.id?.toString(),
|
||||||
}))
|
}))
|
||||||
|
} else {
|
||||||
|
librariesOptions.value = []
|
||||||
}
|
}
|
||||||
|
librariesOptions.value.unshift({
|
||||||
|
title: '全部',
|
||||||
|
value: 'all',
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
@@ -174,7 +188,7 @@ onMounted(() => {
|
|||||||
<VImg :src="getIcon" cover class="mt-5 me-3" max-width="3rem" min-width="3rem" />
|
<VImg :src="getIcon" cover class="mt-5 me-3" max-width="3rem" min-width="3rem" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent >
|
<VDialog v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
|
||||||
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|||||||
@@ -2,30 +2,24 @@
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
||||||
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
|
import SiteUserDataDialog from '../dialog/SiteUserDataDialog.vue'
|
||||||
import { requiredValidator } from '@/@validators'
|
import SiteResourceDialog from '../dialog/SiteResourceDialog.vue'
|
||||||
|
import SiteCookieUpdateDialog from '../dialog/SiteCookieUpdateDialog.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site, SiteStatistic } from '@/api/types'
|
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import { useDisplay } from 'vuetify'
|
import { VCardActions, VExpandTransition, VProgressLinear, VSpacer } from 'vuetify/lib/components/index.mjs'
|
||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const cardProps = defineProps({
|
const cardProps = defineProps({
|
||||||
site: Object as PropType<Site>,
|
site: Object as PropType<Site>,
|
||||||
width: String,
|
data: Object as PropType<SiteUserData>,
|
||||||
height: String,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['update', 'remove'])
|
const emit = defineEmits(['update', 'remove'])
|
||||||
|
|
||||||
// 密码输入
|
|
||||||
const isPasswordVisible = ref(false)
|
|
||||||
|
|
||||||
// 图标
|
// 图标
|
||||||
const siteIcon = ref<string>('')
|
const siteIcon = ref<string>('')
|
||||||
|
|
||||||
@@ -38,9 +32,6 @@ const testButtonText = ref('测试')
|
|||||||
// 测试按钮可用性
|
// 测试按钮可用性
|
||||||
const testButtonDisable = ref(false)
|
const testButtonDisable = ref(false)
|
||||||
|
|
||||||
// 更新按钮可用性
|
|
||||||
const updateButtonDisable = ref(false)
|
|
||||||
|
|
||||||
// 更新站点Cookie UA弹窗
|
// 更新站点Cookie UA弹窗
|
||||||
const siteCookieDialog = ref(false)
|
const siteCookieDialog = ref(false)
|
||||||
|
|
||||||
@@ -50,18 +41,11 @@ const siteEditDialog = ref(false)
|
|||||||
// 资源浏览弹窗
|
// 资源浏览弹窗
|
||||||
const resourceDialog = ref(false)
|
const resourceDialog = ref(false)
|
||||||
|
|
||||||
// 进度条
|
// 用户数据弹窗
|
||||||
const progressDialog = ref(false)
|
const siteUserDataDialog = ref(false)
|
||||||
|
|
||||||
// 进度文本
|
// 站点操作显示
|
||||||
const progressText = ref('请稍候 ...')
|
const siteActionShow = ref(false)
|
||||||
|
|
||||||
// 用户名密码表单
|
|
||||||
const userPwForm = ref({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
code: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 站点使用统计
|
// 站点使用统计
|
||||||
const siteStats = ref<SiteStatistic>({})
|
const siteStats = ref<SiteStatistic>({})
|
||||||
@@ -113,34 +97,9 @@ async function handleResourceBrowse() {
|
|||||||
resourceDialog.value = true
|
resourceDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API,更新站点Cookie UA
|
// 打开站点用户数据弹窗
|
||||||
async function updateSiteCookie() {
|
async function handleSiteUserData() {
|
||||||
try {
|
siteUserDataDialog.value = true
|
||||||
if (!userPwForm.value.username || !userPwForm.value.password) return
|
|
||||||
|
|
||||||
// 更新按钮状态
|
|
||||||
siteCookieDialog.value = false
|
|
||||||
updateButtonDisable.value = true
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开站点页面
|
// 打开站点页面
|
||||||
@@ -162,9 +121,10 @@ const statColor = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听resourceDialog,如果为false则重新查询站点使用统计
|
// 计算上传量和下载量的百分比
|
||||||
watch(resourceDialog, value => {
|
const getPercentage = computed(() => {
|
||||||
if (!value) getSiteStats()
|
if (cardProps.data?.upload === 0) return 100
|
||||||
|
return ((cardProps.data?.download ?? 0) / ((cardProps.data?.download ?? 0) + (cardProps.data?.upload ?? 0))) * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保存站点
|
// 保存站点
|
||||||
@@ -173,6 +133,18 @@ function saveSite() {
|
|||||||
emit('update')
|
emit('update')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新站点Cookie UA后的回调
|
||||||
|
function onSiteCookieUpdated() {
|
||||||
|
siteCookieDialog.value = false
|
||||||
|
getSiteStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 资源浏览弹窗关闭后的回调
|
||||||
|
function onSiteResourceDone() {
|
||||||
|
resourceDialog.value = false
|
||||||
|
getSiteStats()
|
||||||
|
}
|
||||||
|
|
||||||
// 装载时查询站点图标
|
// 装载时查询站点图标
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getSiteIcon()
|
getSiteIcon()
|
||||||
@@ -183,8 +155,6 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VCard
|
<VCard
|
||||||
:height="cardProps.height"
|
|
||||||
:width="cardProps.width"
|
|
||||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
@click="siteEditDialog = true"
|
@click="siteEditDialog = true"
|
||||||
@@ -194,7 +164,7 @@ onMounted(() => {
|
|||||||
<VImg :src="siteIcon" />
|
<VImg :src="siteIcon" />
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
</template>
|
</template>
|
||||||
<VCardItem style="padding-block-end: 0;">
|
<VCardItem style="padding-block-end: 0">
|
||||||
<VCardTitle class="font-bold">
|
<VCardTitle class="font-bold">
|
||||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
@@ -202,7 +172,7 @@ onMounted(() => {
|
|||||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText class="py-2" style="block-size: 36px;">
|
<VCardText class="py-1">
|
||||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||||
<template #activator="{ props }">
|
<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" />
|
||||||
@@ -224,67 +194,61 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VDivider />
|
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
<VBtn
|
||||||
<template #prepend>
|
:icon="siteActionShow ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||||
<VIcon icon="mdi-refresh" />
|
@click.stop="siteActionShow = !siteActionShow"
|
||||||
</template>
|
/>
|
||||||
更新
|
<span class="text-sm">
|
||||||
</VBtn>
|
↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }}
|
||||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
</span>
|
||||||
<template #prepend>
|
<VSpacer />
|
||||||
<VIcon icon="mdi-link" />
|
|
||||||
</template>
|
|
||||||
{{ testButtonText }}
|
|
||||||
</VBtn>
|
|
||||||
<VBtn @click.stop="handleResourceBrowse">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-web" />
|
|
||||||
</template>
|
|
||||||
浏览
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
|
<VDivider class="mb-1" v-if="siteActionShow" />
|
||||||
|
<VExpandTransition>
|
||||||
|
<div v-show="siteActionShow" class="py-1 pe-12">
|
||||||
|
<VBtn v-if="!cardProps.site?.public" @click.stop="handleSiteUpdate" variant="text">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-refresh" />
|
||||||
|
</template>
|
||||||
|
更新
|
||||||
|
</VBtn>
|
||||||
|
<VBtn :disabled="testButtonDisable" @click.stop="testSite" variant="text">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-link" />
|
||||||
|
</template>
|
||||||
|
{{ testButtonText }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn @click.stop="handleResourceBrowse" variant="text">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-web" />
|
||||||
|
</template>
|
||||||
|
浏览
|
||||||
|
</VBtn>
|
||||||
|
<VBtn @click.stop="handleSiteUserData" variant="text">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-chart-bell-curve" />
|
||||||
|
</template>
|
||||||
|
数据
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VExpandTransition>
|
||||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||||
<span class="absolute top-1 right-8">
|
<span class="absolute top-1 right-8">
|
||||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||||
</span>
|
</span>
|
||||||
|
<div class="w-full absolute bottom-0" v-if="(cardProps.data?.upload || cardProps.data?.download || 0) > 0">
|
||||||
|
<VProgressLinear :model-value="getPercentage" bg-color="success" color="warning" bg-opacity="0.5" height="3" />
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 更新站点Cookie & UA弹窗 -->
|
<!-- 更新站点Cookie & UA弹窗 -->
|
||||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
<SiteCookieUpdateDialog
|
||||||
<!-- Dialog Content -->
|
v-if="siteCookieDialog"
|
||||||
<VCard title="更新站点Cookie & UA">
|
v-model="siteCookieDialog"
|
||||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
:site="cardProps.site"
|
||||||
<VCardText>
|
@close="siteCookieDialog = false"
|
||||||
<VForm @submit.prevent="() => {}">
|
@done="onSiteCookieUpdated"
|
||||||
<VRow>
|
/>
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
|
||||||
</VCol>
|
|
||||||
<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'"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
|
||||||
@keydown.enter="updateSiteCookie"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<!-- 站点编辑弹窗 -->
|
<!-- 站点编辑弹窗 -->
|
||||||
<SiteAddEditDialog
|
<SiteAddEditDialog
|
||||||
v-if="siteEditDialog"
|
v-if="siteEditDialog"
|
||||||
@@ -294,30 +258,19 @@ onMounted(() => {
|
|||||||
@remove="emit('remove')"
|
@remove="emit('remove')"
|
||||||
@close="siteEditDialog = false"
|
@close="siteEditDialog = false"
|
||||||
/>
|
/>
|
||||||
|
<!-- 站点数据弹窗 -->
|
||||||
|
<SiteUserDataDialog
|
||||||
|
v-if="siteUserDataDialog"
|
||||||
|
v-model="siteUserDataDialog"
|
||||||
|
:site="cardProps.site"
|
||||||
|
@close="siteUserDataDialog = false"
|
||||||
|
/>
|
||||||
<!-- 站点资源弹窗 -->
|
<!-- 站点资源弹窗 -->
|
||||||
<VDialog
|
<SiteResourceDialog
|
||||||
v-if="resourceDialog"
|
v-if="resourceDialog"
|
||||||
v-model="resourceDialog"
|
v-model="resourceDialog"
|
||||||
max-width="80rem"
|
:site="cardProps.site"
|
||||||
scrollable
|
@close="onSiteResourceDone"
|
||||||
z-index="1010"
|
/>
|
||||||
:fullscreen="!display.mdAndUp.value"
|
|
||||||
>
|
|
||||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
|
||||||
<DialogCloseBtn @click="resourceDialog = false" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<!-- 进度框 -->
|
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.v-table th {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ const siteForm = ref<Site>({
|
|||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 维护类型
|
||||||
|
const siteType = ref('cookie')
|
||||||
|
|
||||||
|
// 是否限流
|
||||||
|
const isLimit = ref(false)
|
||||||
|
|
||||||
// 状态下拉项
|
// 状态下拉项
|
||||||
const statusItems = [
|
const statusItems = [
|
||||||
{ title: '启用', value: true },
|
{ title: '启用', value: true },
|
||||||
@@ -106,6 +112,15 @@ async function deleteSiteInfo() {
|
|||||||
async function updateSiteInfo() {
|
async function updateSiteInfo() {
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
|
if (isLimit.value) {
|
||||||
|
siteForm.value.limit_interval = siteForm.value.limit_interval || 0
|
||||||
|
siteForm.value.limit_count = siteForm.value.limit_count || 0
|
||||||
|
siteForm.value.limit_seconds = siteForm.value.limit_seconds || 0
|
||||||
|
} else {
|
||||||
|
siteForm.value.limit_interval = 0
|
||||||
|
siteForm.value.limit_count = 0
|
||||||
|
siteForm.value.limit_seconds = 0
|
||||||
|
}
|
||||||
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
||||||
@@ -120,9 +135,12 @@ async function updateSiteInfo() {
|
|||||||
doneNProgress()
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (props.oper !== 'add') {
|
if (props.oper !== 'add') {
|
||||||
fetchSiteInfo()
|
await fetchSiteInfo()
|
||||||
|
if (siteForm.value.limit_interval || siteForm.value.limit_count || siteForm.value.limit_seconds)
|
||||||
|
isLimit.value = true
|
||||||
|
if (siteForm.value.apikey) siteType.value = 'api'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -179,35 +197,69 @@ onMounted(() => {
|
|||||||
<VCol cols="12" md="3">
|
<VCol cols="12" md="3">
|
||||||
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
|
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
</VRow>
|
||||||
<VTextarea v-model="siteForm.cookie" label="站点Cookie" hint="站点请求头中的Cookie信息" persistent-hint />
|
<VTabs v-model="siteType" show-arrows class="v-tabs-pill mt-3">
|
||||||
</VCol>
|
<VTab selected-class="v-tab--selected">
|
||||||
<VCol cols="12" md="6">
|
<div>
|
||||||
<VTextField
|
<VIcon size="20" start icon="mdi-cookie" value="cookie" />
|
||||||
v-model="siteForm.token"
|
Cookie
|
||||||
label="请求头(Authorization)"
|
</div>
|
||||||
hint="站点请求头中的Authorization信息,特殊站点需要"
|
</VTab>
|
||||||
persistent-hint
|
<VTab selected-class="v-tab--selected">
|
||||||
/>
|
<div>
|
||||||
</VCol>
|
<VIcon size="20" start icon="mdi-api" value="api" />
|
||||||
<VCol cols="12" md="6">
|
API
|
||||||
<VTextField
|
</div>
|
||||||
v-model="siteForm.apikey"
|
</VTab>
|
||||||
label="令牌(API Key)"
|
</VTabs>
|
||||||
hint="站点的访问API Key,特殊站点需要"
|
<VWindow v-model="siteType" class="my-3 disable-tab-transition" :touch="false">
|
||||||
persistent-hint
|
<VWindowItem value="cookie">
|
||||||
/>
|
<VRow>
|
||||||
</VCol>
|
<VCol cols="12">
|
||||||
<VCol cols="12">
|
<VTextarea
|
||||||
<VTextField
|
v-model="siteForm.cookie"
|
||||||
v-model="siteForm.ua"
|
label="站点Cookie"
|
||||||
label="站点User-Agent"
|
hint="站点请求头中的Cookie信息"
|
||||||
hint="获取Cookie的浏览器对应的User-Agent"
|
persistent-hint
|
||||||
persistent-hint
|
/>
|
||||||
/>
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.ua"
|
||||||
|
label="站点User-Agent"
|
||||||
|
hint="获取Cookie的浏览器对应的User-Agent"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="api">
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.token"
|
||||||
|
label="请求头(Authorization)"
|
||||||
|
hint="站点请求头中的Authorization信息,特殊站点需要"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.apikey"
|
||||||
|
label="令牌(API Key)"
|
||||||
|
hint="站点的访问API Key,特殊站点需要"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VWindowItem>
|
||||||
|
</VWindow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSwitch v-model="isLimit" label="限制站点访问频率" />
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow v-if="isLimit">
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="4">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteForm.limit_interval"
|
v-model="siteForm.limit_interval"
|
||||||
@@ -238,10 +290,15 @@ onMounted(() => {
|
|||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch v-model="siteForm.proxy" label="代理" hint="使用代理服务器访问该站点" persistent-hint />
|
<VSwitch v-model="siteForm.proxy" label="使用代理访问" hint="使用代理服务器访问该站点" persistent-hint />
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch v-model="siteForm.render" label="仿真" hint="使用浏览器模拟真实访问该站点" persistent-hint />
|
<VSwitch
|
||||||
|
v-model="siteForm.render"
|
||||||
|
label="浏览器仿真"
|
||||||
|
hint="使用浏览器模拟真实访问该站点"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { Site } from '@/api/types'
|
||||||
|
import { requiredValidator } from '@/@validators'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const cardProps = defineProps({
|
||||||
|
site: Object as PropType<Site>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义触发的自定义事件
|
||||||
|
const emit = defineEmits(['close', 'done'])
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 用户名密码表单
|
||||||
|
const userPwForm = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
code: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 密码输入
|
||||||
|
const isPasswordVisible = ref(false)
|
||||||
|
|
||||||
|
// 更新按钮可用性
|
||||||
|
const updateButtonDisable = ref(false)
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
|
// 调用API,更新站点Cookie UA
|
||||||
|
async function updateSiteCookie() {
|
||||||
|
try {
|
||||||
|
if (!userPwForm.value.username || !userPwForm.value.password) return
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
updateButtonDisable.value = true
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
|
||||||
|
emit('done')
|
||||||
|
} else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
||||||
|
|
||||||
|
progressDialog.value = false
|
||||||
|
updateButtonDisable.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog max-width="50rem">
|
||||||
|
<!-- Dialog Content -->
|
||||||
|
<VCard title="更新站点Cookie & UA">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText>
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||||
|
</VCol>
|
||||||
|
<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'"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||||
|
@keydown.enter="updateSiteCookie"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardActions>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
variant="elevated"
|
||||||
|
@click="updateSiteCookie"
|
||||||
|
:disabled="updateButtonDisable"
|
||||||
|
prepend-icon="mdi-refresh"
|
||||||
|
class="px-5"
|
||||||
|
>
|
||||||
|
开始更新
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
<!-- 进度框 -->
|
||||||
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Site } from '@/api/types'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import api from '@/api'
|
||||||
|
import type { TorrentInfo } from '@/api/types'
|
||||||
|
import { formatFileSize } from '@core/utils/formatters'
|
||||||
|
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
site: Object as PropType<Site>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册事件
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const resourceDataList = ref<TorrentInfo[]>([])
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const resourceSearch = ref('')
|
||||||
|
|
||||||
|
// 总条数
|
||||||
|
const resourceTotalItems = ref(0)
|
||||||
|
|
||||||
|
// 每页条数
|
||||||
|
const resourceItemsPerPage = ref(25)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const resourceLoading = ref(false)
|
||||||
|
|
||||||
|
// 种子元数据
|
||||||
|
const torrent = ref<TorrentInfo>()
|
||||||
|
|
||||||
|
// 资源浏览表头
|
||||||
|
const resourceHeaders = [
|
||||||
|
{ title: '标题', key: 'title', sortable: false },
|
||||||
|
{ title: '时间', key: 'pubdate', sortable: true },
|
||||||
|
{ title: '大小', key: 'size', sortable: true },
|
||||||
|
{ title: '做种', key: 'seeders', sortable: true },
|
||||||
|
{ title: '下载', key: 'peers', sortable: true },
|
||||||
|
{ title: '', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 打开种子详情页面
|
||||||
|
function openTorrentDetail(page_url: string) {
|
||||||
|
window.open(page_url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载种子文件
|
||||||
|
async function downloadTorrentFile(enclosure: string) {
|
||||||
|
window.open(enclosure, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API,查询站点资源
|
||||||
|
async function getResourceList() {
|
||||||
|
resourceLoading.value = true
|
||||||
|
try {
|
||||||
|
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`)
|
||||||
|
resourceLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 促销Chip类
|
||||||
|
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||||
|
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||||
|
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||||
|
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||||
|
else return 'text-white bg-gray-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载
|
||||||
|
async function addDownload(_torrent: any) {
|
||||||
|
torrent.value = _torrent
|
||||||
|
addDownloadDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载对话框
|
||||||
|
const addDownloadDialog = ref(false)
|
||||||
|
|
||||||
|
// 添加下载成功
|
||||||
|
function addDownloadSuccess(url: string) {
|
||||||
|
addDownloadDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载失败
|
||||||
|
function addDownloadError(error: string) {
|
||||||
|
addDownloadDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 装载时查询站点图标
|
||||||
|
onMounted(() => {
|
||||||
|
getResourceList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog max-width="80rem" scrollable z-index="1010" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`浏览 - ${props.site?.name}`">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VDivider />
|
||||||
|
<VCardText class="pt-2">
|
||||||
|
<VDataTable
|
||||||
|
v-model:items-per-page="resourceItemsPerPage"
|
||||||
|
:headers="resourceHeaders"
|
||||||
|
:items="resourceDataList"
|
||||||
|
:items-length="resourceTotalItems"
|
||||||
|
:search="resourceSearch"
|
||||||
|
:loading="resourceLoading"
|
||||||
|
density="compact"
|
||||||
|
item-value="title"
|
||||||
|
return-object
|
||||||
|
fixed-header
|
||||||
|
hover
|
||||||
|
items-per-page-text="每页条数"
|
||||||
|
page-text="{0}-{1} 共 {2} 条"
|
||||||
|
loading-text="加载中..."
|
||||||
|
>
|
||||||
|
<template #item.title="{ item }">
|
||||||
|
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
||||||
|
<div class="text-high-emphasis pt-1">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm my-1">
|
||||||
|
{{ item.description }}
|
||||||
|
</div>
|
||||||
|
<VChip v-if="item.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="item.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||||
|
{{ item.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-for="(label, index) in item.labels"
|
||||||
|
:key="index"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
|
||||||
|
:class="getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ item.volume_factor }}
|
||||||
|
</VChip>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template #item.pubdate="{ item }">
|
||||||
|
<div>{{ item.date_elapsed }}</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{{ item.pubdate }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #item.size="{ item }">
|
||||||
|
<div class="text-nowrap whitespace-nowrap">
|
||||||
|
{{ formatFileSize(item.size) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #item.seeders="{ item }">
|
||||||
|
<div>{{ item.seeders }}</div>
|
||||||
|
</template>
|
||||||
|
<template #item.peers="{ item }">
|
||||||
|
<div>{{ item.peers }}</div>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<div class="me-n3">
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem variant="plain" @click="openTorrentDetail(item.page_url || '')">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-information" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>查看详情</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem
|
||||||
|
v-if="item.enclosure?.startsWith('http')"
|
||||||
|
variant="plain"
|
||||||
|
@click="downloadTorrentFile(item.enclosure)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #no-data> 没有数据 </template>
|
||||||
|
</VDataTable>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
<!-- 添加下载对话框 -->
|
||||||
|
<AddDownloadDialog
|
||||||
|
v-if="addDownloadDialog"
|
||||||
|
v-model="addDownloadDialog"
|
||||||
|
:torrent="torrent"
|
||||||
|
@done="addDownloadSuccess"
|
||||||
|
@error="addDownloadError"
|
||||||
|
@close="addDownloadDialog = false"
|
||||||
|
/>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.v-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Site, SiteUserData } from '@/api/types'
|
||||||
|
import api from '@/api'
|
||||||
|
import { useDisplay, useTheme } from 'vuetify'
|
||||||
|
import { VAvatar, VCardText, VIcon } from 'vuetify/lib/components/index.mjs'
|
||||||
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
site: Object as PropType<Site>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册事件
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const vuetifyTheme = useTheme()
|
||||||
|
|
||||||
|
const currentTheme = controlledComputed(
|
||||||
|
() => vuetifyTheme.name.value,
|
||||||
|
() => vuetifyTheme.current.value.colors,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 站点数据列表
|
||||||
|
const siteDatas = ref<SiteUserData[]>([])
|
||||||
|
|
||||||
|
// 最新一天的数据,按时间倒序排序后取第一条记录
|
||||||
|
const siteData = computed(() => siteDatas.value[0])
|
||||||
|
|
||||||
|
// 站点数据列表中的上传量、下载量数据生成图形使用的数据
|
||||||
|
const historySeries = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: '上传量',
|
||||||
|
data: siteDatas.value.map(item => Math.round((item.upload ?? 0) / 1024 / 1024 / 1024)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '下载量',
|
||||||
|
data: siteDatas.value.map(item => Math.round((item.download ?? 0) / 1024 / 1024 / 1024)),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图形选项
|
||||||
|
const historyChartOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
type: 'area',
|
||||||
|
parentHeightOffset: 0,
|
||||||
|
toolbar: { show: false },
|
||||||
|
animations: { enabled: true },
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
autoScaleYaxis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
tooltip: {
|
||||||
|
x: {
|
||||||
|
format: 'dd MMM yyyy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
xaxis: {
|
||||||
|
lines: { show: false },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: {
|
||||||
|
text: 'GB',
|
||||||
|
},
|
||||||
|
lines: { show: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: 3,
|
||||||
|
lineCap: 'butt',
|
||||||
|
curve: 'smooth',
|
||||||
|
},
|
||||||
|
colors: [currentTheme.value.success, currentTheme.value.warning],
|
||||||
|
markers: {
|
||||||
|
size: 0,
|
||||||
|
style: 'hollow',
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: 'category',
|
||||||
|
categories: siteDatas.value.map(item => item.updated_day),
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
formatter: function (val: string) {
|
||||||
|
return new Date(val).toLocaleDateString('zh-CN')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: {
|
||||||
|
text: 'GB',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
formatter: function (val: number) {
|
||||||
|
return val.toLocaleString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
shadeIntensity: 1,
|
||||||
|
opacityFrom: 0.5,
|
||||||
|
opacityTo: 0.7,
|
||||||
|
stops: [0, 100],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 做种分布列,seeding_info的格式为[[x, y], [x, y], ...],x为做种数,y为做种体积,做种体积需要转换为GB
|
||||||
|
const seedingSeries = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: '体积',
|
||||||
|
data: siteData.value?.seeding_info?.map(item => [item[0] ?? 0, Math.round((item[1] ?? 0) / 1024 / 1024 / 1024)]),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 做种分布图形选项
|
||||||
|
const seedingChartOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
type: 'scatter',
|
||||||
|
parentHeightOffset: 0,
|
||||||
|
toolbar: { show: false },
|
||||||
|
animations: { enabled: true },
|
||||||
|
zoom: {
|
||||||
|
autoScaleYaxis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
x: {
|
||||||
|
formatter: function (val: number) {
|
||||||
|
return '数量:' + val.toLocaleString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
xaxis: {
|
||||||
|
lines: { show: true },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
lines: { show: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: [currentTheme.value.primary],
|
||||||
|
xaxis: {
|
||||||
|
type: 'numeric',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
formatter: function (val: number) {
|
||||||
|
return Math.round(val).toLocaleString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: '数量',
|
||||||
|
},
|
||||||
|
tickAmount: 10,
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: {
|
||||||
|
text: 'GB',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
formatter: function (val: number) {
|
||||||
|
return val.toLocaleString() + ' GB'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据传入属性,计算列表数据中第一条与第二条的差值,如果没有第二条则差值为全部
|
||||||
|
const diffData: { [key: string]: any } = computed(() => {
|
||||||
|
if (siteDatas.value.length < 2) {
|
||||||
|
return siteData.value
|
||||||
|
}
|
||||||
|
const first = siteDatas.value[siteDatas.value.length - 1]
|
||||||
|
const second = siteDatas.value[siteDatas.value.length - 2]
|
||||||
|
return {
|
||||||
|
bonus: (first.bonus ?? 0) - (second.bonus ?? 0),
|
||||||
|
ratio: (first.ratio ?? 0) - (second.ratio ?? 0),
|
||||||
|
upload: (first.upload ?? 0) - (second.upload ?? 0),
|
||||||
|
download: (first.download ?? 0) - (second.download ?? 0),
|
||||||
|
seeding: (first.seeding ?? 0) - (second.seeding ?? 0),
|
||||||
|
seeding_size: (first.seeding_size ?? 0) - (second.seeding_size ?? 0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化差值
|
||||||
|
function getDiffString(diff: number | undefined, format: boolean = true) {
|
||||||
|
if (diff === undefined) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
if (format) {
|
||||||
|
return diff >= 0 ? `+${diff.toLocaleString()}` : diff.toLocaleString()
|
||||||
|
}
|
||||||
|
return diff >= 0 ? `+${diff}` : diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据差值的正负,返回不同的样式
|
||||||
|
function getDiffClass(diff: number | undefined) {
|
||||||
|
if (diff === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (diff == 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return diff > 0 ? 'text-success' : 'text-error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询站点用户数据
|
||||||
|
async function fetchSiteUserData() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(`site/userdata/${props.site?.id}`)
|
||||||
|
if (result.success) {
|
||||||
|
siteDatas.value = result.data.sort((a: { updated_day: any }, b: { updated_day: any }) =>
|
||||||
|
(a.updated_day || '').localeCompare(b.updated_day || ''),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await fetchSiteUserData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`数据 - ${props.site?.name}`" class="rounded-t">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText>
|
||||||
|
<VRow class="match-height">
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">用户等级</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ siteData?.user_level || '无' }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-account"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 积分 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">积分</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ siteData?.bonus?.toLocaleString() }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.bonus)">
|
||||||
|
({{ getDiffString(diffData?.bonus) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-scoreboard"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 分享率 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">分享率</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ siteData?.ratio }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.ratio)">
|
||||||
|
({{ getDiffString(diffData?.ratio) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-percent"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 总上传量 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">总上传量</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ formatFileSize(siteData?.upload || 0) }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.upload)">
|
||||||
|
({{ formatFileSize(diffData?.upload || 0, 2, true) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-upload"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 总下载量 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">总下载量</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ formatFileSize(siteData?.download || 0) }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.download)">
|
||||||
|
({{ formatFileSize(diffData?.download || 0, 2, true) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-download"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 总做种数 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">总做种数</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ siteData?.seeding?.toLocaleString() }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding)">
|
||||||
|
({{ getDiffString(diffData?.seeding) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-seed"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 总做种体积 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">总做种体积</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ formatFileSize(siteData?.seeding_size || 0) }}
|
||||||
|
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding_size)">
|
||||||
|
({{ formatFileSize(diffData?.seeding_size || 0, 2, true) }})
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-database"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
<!-- 加入时间 -->
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard>
|
||||||
|
<VCardText class="d-flex align-center">
|
||||||
|
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
||||||
|
<div class="d-flex flex-column gap-y-1">
|
||||||
|
<span class="text-base">加入时间</span>
|
||||||
|
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
||||||
|
{{ siteData?.join_at?.split(' ')[0] }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<VAvatar variant="tonal" size="42" rounded>
|
||||||
|
<VIcon icon="mdi-calendar"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol>
|
||||||
|
<VCard title="历史流量">
|
||||||
|
<VCardText>
|
||||||
|
<VueApexCharts type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol>
|
||||||
|
<VCard title="做种分布">
|
||||||
|
<VCardText>
|
||||||
|
<VueApexCharts type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -270,7 +270,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
<input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" />
|
<input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" />
|
||||||
|
|
||||||
|
|
||||||
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
|
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
|
||||||
<VIcon icon="mdi-refresh" />
|
<VIcon icon="mdi-refresh" />
|
||||||
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
|
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import api from '@/api'
|
|
||||||
import type { TorrentInfo } from '@/api/types'
|
|
||||||
import { formatFileSize } from '@core/utils/formatters'
|
|
||||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
site: Number,
|
|
||||||
width: String,
|
|
||||||
height: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 数据列表
|
|
||||||
const resourceDataList = ref<TorrentInfo[]>([])
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
const resourceSearch = ref('')
|
|
||||||
|
|
||||||
// 总条数
|
|
||||||
const resourceTotalItems = ref(0)
|
|
||||||
|
|
||||||
// 每页条数
|
|
||||||
const resourceItemsPerPage = ref(25)
|
|
||||||
|
|
||||||
// 加载状态
|
|
||||||
const resourceLoading = ref(false)
|
|
||||||
|
|
||||||
// 种子元数据
|
|
||||||
const torrent = ref<TorrentInfo>()
|
|
||||||
|
|
||||||
// 资源浏览表头
|
|
||||||
const resourceHeaders = [
|
|
||||||
{ title: '标题', key: 'title', sortable: false },
|
|
||||||
{ title: '时间', key: 'pubdate', sortable: true },
|
|
||||||
{ title: '大小', key: 'size', sortable: true },
|
|
||||||
{ title: '做种', key: 'seeders', sortable: true },
|
|
||||||
{ title: '下载', key: 'peers', sortable: true },
|
|
||||||
{ title: '', key: 'actions', sortable: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 打开种子详情页面
|
|
||||||
function openTorrentDetail(page_url: string) {
|
|
||||||
window.open(page_url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载种子文件
|
|
||||||
async function downloadTorrentFile(enclosure: string) {
|
|
||||||
window.open(enclosure, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API,查询站点资源
|
|
||||||
async function getResourceList() {
|
|
||||||
resourceLoading.value = true
|
|
||||||
try {
|
|
||||||
resourceDataList.value = await api.get(`site/resource/${props.site}`)
|
|
||||||
resourceLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 促销Chip类
|
|
||||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
|
||||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
|
||||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
|
||||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
|
||||||
else return 'text-white bg-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载
|
|
||||||
async function addDownload(_torrent: any) {
|
|
||||||
torrent.value = _torrent
|
|
||||||
addDownloadDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载对话框
|
|
||||||
const addDownloadDialog = ref(false)
|
|
||||||
|
|
||||||
// 添加下载成功
|
|
||||||
function addDownloadSuccess(url: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载失败
|
|
||||||
function addDownloadError(error: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 装载时查询站点图标
|
|
||||||
onMounted(() => {
|
|
||||||
getResourceList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDataTable
|
|
||||||
v-model:items-per-page="resourceItemsPerPage"
|
|
||||||
:headers="resourceHeaders"
|
|
||||||
:items="resourceDataList"
|
|
||||||
:items-length="resourceTotalItems"
|
|
||||||
:search="resourceSearch"
|
|
||||||
:loading="resourceLoading"
|
|
||||||
density="compact"
|
|
||||||
item-value="title"
|
|
||||||
return-object
|
|
||||||
fixed-header
|
|
||||||
hover
|
|
||||||
items-per-page-text="每页条数"
|
|
||||||
page-text="{0}-{1} 共 {2} 条"
|
|
||||||
loading-text="加载中..."
|
|
||||||
>
|
|
||||||
<template #item.title="{ item }">
|
|
||||||
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
|
||||||
<div class="text-high-emphasis pt-1">
|
|
||||||
{{ item.title }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm my-1">
|
|
||||||
{{ item.description }}
|
|
||||||
</div>
|
|
||||||
<VChip v-if="item.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
|
||||||
H&R
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="item.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
|
||||||
{{ item.freedate_diff }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-for="(label, index) in item.labels"
|
|
||||||
:key="index"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
|
|
||||||
:class="getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ item.volume_factor }}
|
|
||||||
</VChip>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<template #item.pubdate="{ item }">
|
|
||||||
<div>{{ item.date_elapsed }}</div>
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ item.pubdate }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item.size="{ item }">
|
|
||||||
<div class="text-nowrap whitespace-nowrap">
|
|
||||||
{{ formatFileSize(item.size) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item.seeders="{ item }">
|
|
||||||
<div>{{ item.seeders }}</div>
|
|
||||||
</template>
|
|
||||||
<template #item.peers="{ item }">
|
|
||||||
<div>{{ item.peers }}</div>
|
|
||||||
</template>
|
|
||||||
<template #item.actions="{ item }">
|
|
||||||
<div class="me-n3">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem variant="plain" @click="openTorrentDetail(item.page_url || '')">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-information" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>查看详情</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem
|
|
||||||
v-if="item.enclosure?.startsWith('http')"
|
|
||||||
variant="plain"
|
|
||||||
@click="downloadTorrentFile(item.enclosure)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #no-data> 没有数据 </template>
|
|
||||||
</VDataTable>
|
|
||||||
<AddDownloadDialog
|
|
||||||
v-if="addDownloadDialog"
|
|
||||||
v-model="addDownloadDialog"
|
|
||||||
:torrent="torrent"
|
|
||||||
@done="addDownloadSuccess"
|
|
||||||
@error="addDownloadError"
|
|
||||||
@close="addDownloadDialog = false"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -16,15 +16,18 @@ const appsMenu = ref(false)
|
|||||||
|
|
||||||
// SSE持续接收消息
|
// SSE持续接收消息
|
||||||
function startSSEMessager() {
|
function startSSEMessager() {
|
||||||
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`)
|
// 延迟 3 秒启动 SSE,避免相关认证信息尚未写入 Cookie 导致 403
|
||||||
eventSource.addEventListener('message', event => {
|
setTimeout(() => {
|
||||||
if (event.data) {
|
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message`)
|
||||||
const noti: SystemNotification = JSON.parse(event.data)
|
eventSource.addEventListener('message', event => {
|
||||||
notificationList.value.unshift(noti)
|
if (event.data) {
|
||||||
hasNewMessage.value = true
|
const noti: SystemNotification = JSON.parse(event.data)
|
||||||
// TODO 在顶部显示消息汽泡
|
notificationList.value.unshift(noti)
|
||||||
}
|
hasNewMessage.value = true
|
||||||
})
|
// TODO 在顶部显示消息汽泡
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载时,加载当前用户数据
|
// 页面加载时,加载当前用户数据
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import api from '@/api'
|
|||||||
import { DownloaderConf } from '@/api/types'
|
import { DownloaderConf } from '@/api/types'
|
||||||
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const activeTab = ref(route.query.tab)
|
const activeTab = ref(route.query.tab)
|
||||||
@@ -36,7 +37,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="enabledDownloaders.length > 0">
|
||||||
<VTabs v-model="activeTab">
|
<VTabs v-model="activeTab">
|
||||||
<VTab v-for="item in enabledDownloaders" :value="item.name" @to="jumpTab(item.name)">
|
<VTab v-for="item in enabledDownloaders" :value="item.name" @to="jumpTab(item.name)">
|
||||||
<span class="mx-5">{{ item.name }}</span>
|
<span class="mx-5">{{ item.name }}</span>
|
||||||
@@ -51,4 +52,10 @@ onMounted(() => {
|
|||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
</VWindow>
|
</VWindow>
|
||||||
</div>
|
</div>
|
||||||
|
<NoDataFound
|
||||||
|
v-else
|
||||||
|
error-code="404"
|
||||||
|
error-title="没有下载器"
|
||||||
|
error-description="请先在设置中正确配置并启用下载器。"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ const getPosterUrl: Ref<string> = computed(() => {
|
|||||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||||
// 如果地址中包含douban则使用中转代理
|
// 如果地址中包含douban则使用中转代理
|
||||||
if (url.includes('doubanio.com'))
|
if (url.includes('doubanio.com'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ async function saveStorages() {
|
|||||||
|
|
||||||
// 修改后生效
|
// 修改后生效
|
||||||
async function updatedStorage() {
|
async function updatedStorage() {
|
||||||
await saveStorages()
|
|
||||||
loadStorages()
|
loadStorages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +97,9 @@ async function saveDirectories() {
|
|||||||
|
|
||||||
// 添加媒体库目录
|
// 添加媒体库目录
|
||||||
function addDirectory() {
|
function addDirectory() {
|
||||||
let name = `目录${directories.value.length + 1}`;
|
let name = `目录${directories.value.length + 1}`
|
||||||
while (directories.value.some(item => item.name === name)) {
|
while (directories.value.some(item => item.name === name)) {
|
||||||
name = `目录${parseInt(name.split('目录')[1]) + 1}`;
|
name = `目录${parseInt(name.split('目录')[1]) + 1}`
|
||||||
}
|
}
|
||||||
directories.value.push({
|
directories.value.push({
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ const $toast = useToast()
|
|||||||
|
|
||||||
// 种子优先规则下拉框
|
// 种子优先规则下拉框
|
||||||
const TorrentPriorityItems = [
|
const TorrentPriorityItems = [
|
||||||
{ title: '站点排序优先', value: 'site' },
|
{ title: '资源优先级', value: 'torrent' },
|
||||||
{ title: '站点上传量优先', value: 'upload' },
|
{ title: '站点优先级', value: 'site' },
|
||||||
{ title: '资源做种数优先', value: 'seeder' },
|
{ title: '站点上传量', value: 'upload' },
|
||||||
|
{ title: '资源做种数', value: 'seeder' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 调用API查询自动分类配置
|
// 调用API查询自动分类配置
|
||||||
@@ -52,13 +53,13 @@ async function saveCustomRules() {
|
|||||||
|
|
||||||
// 添加自定义规则
|
// 添加自定义规则
|
||||||
function addCustomRule() {
|
function addCustomRule() {
|
||||||
let id = `RULE${customRules.value.length + 1}`;
|
let id = `RULE${customRules.value.length + 1}`
|
||||||
while (customRules.value.some(item => item.id === id)) {
|
while (customRules.value.some(item => item.id === id)) {
|
||||||
id = `RULE${parseInt(id.split('RULE')[1]) + 1}`;
|
id = `RULE${parseInt(id.split('RULE')[1]) + 1}`
|
||||||
}
|
}
|
||||||
let name = `规则${customRules.value.length + 1}`;
|
let name = `规则${customRules.value.length + 1}`
|
||||||
while (customRules.value.some(item => item.name === name)) {
|
while (customRules.value.some(item => item.name === name)) {
|
||||||
name = `规则${parseInt(name.split('规则')[1]) + 1}`;
|
name = `规则${parseInt(name.split('规则')[1]) + 1}`
|
||||||
}
|
}
|
||||||
customRules.value.push({
|
customRules.value.push({
|
||||||
id: id,
|
id: id,
|
||||||
@@ -97,9 +98,9 @@ async function saveFilterRuleGroups() {
|
|||||||
|
|
||||||
// 添加规则组
|
// 添加规则组
|
||||||
function addFilterRuleGroup() {
|
function addFilterRuleGroup() {
|
||||||
let name = `规则组${filterRuleGroups.value.length + 1}`;
|
let name = `规则组${filterRuleGroups.value.length + 1}`
|
||||||
while (filterRuleGroups.value.some(item => item.name === name)) {
|
while (filterRuleGroups.value.some(item => item.name === name)) {
|
||||||
name = `规则组${parseInt(name.split('规则组')[1]) + 1}`;
|
name = `规则组${parseInt(name.split('规则组')[1]) + 1}`
|
||||||
}
|
}
|
||||||
filterRuleGroups.value.push({
|
filterRuleGroups.value.push({
|
||||||
name: name,
|
name: name,
|
||||||
@@ -245,7 +246,7 @@ onMounted(() => {
|
|||||||
<VCard>
|
<VCard>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VCardTitle>下载规则</VCardTitle>
|
<VCardTitle>下载规则</VCardTitle>
|
||||||
<VCardSubtitle>按站点或做种数量优先下载。</VCardSubtitle>
|
<VCardSubtitle>同时命中多个资源时择优下载。</VCardSubtitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm>
|
<VForm>
|
||||||
@@ -254,8 +255,10 @@ onMounted(() => {
|
|||||||
<VSelect
|
<VSelect
|
||||||
v-model="selectedTorrentPriority"
|
v-model="selectedTorrentPriority"
|
||||||
:items="TorrentPriorityItems"
|
:items="TorrentPriorityItems"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
label="当前使用下载优先规则"
|
label="当前使用下载优先规则"
|
||||||
hint="同时命中多个站点的多个资源时下载的优先规则"
|
hint="排在前面的优先级越高,未选择的项不纳入排序"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const resetSitesText = ref('重置站点数据')
|
|||||||
// 站点重置按钮可用状态
|
// 站点重置按钮可用状态
|
||||||
const resetSitesDisabled = ref(false)
|
const resetSitesDisabled = ref(false)
|
||||||
|
|
||||||
|
const isPasswordVisible = ref(false)
|
||||||
|
|
||||||
// CookieCloud设置项
|
// CookieCloud设置项
|
||||||
const siteSetting = ref({
|
const siteSetting = ref({
|
||||||
COOKIECLOUD_HOST: '',
|
COOKIECLOUD_HOST: '',
|
||||||
@@ -155,7 +157,9 @@ onMounted(() => {
|
|||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteSetting.COOKIECLOUD_PASSWORD"
|
v-model="siteSetting.COOKIECLOUD_PASSWORD"
|
||||||
type="password"
|
:type="isPasswordVisible ? 'text' : 'password'"
|
||||||
|
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
|
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||||
label="端对端加密密码"
|
label="端对端加密密码"
|
||||||
hint="CookieCloud浏览器插件生成的端对端加密密码"
|
hint="CookieCloud浏览器插件生成的端对端加密密码"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site } from '@/api/types'
|
import type { Site, SiteUserData } from '@/api/types'
|
||||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
||||||
@@ -15,8 +15,11 @@ const appMode = computed(() => {
|
|||||||
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
|
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 数据列表
|
// 站点列表
|
||||||
const dataList = ref<Site[]>([])
|
const siteList = ref<Site[]>([])
|
||||||
|
|
||||||
|
// 站点数据列表
|
||||||
|
const userDataList = ref<SiteUserData[]>([])
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
@@ -31,7 +34,7 @@ const siteAddDialog = ref(false)
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
dataList.value = await api.get('site/')
|
siteList.value = await api.get('site/')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -39,10 +42,19 @@ async function fetchData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取站点最新数据
|
||||||
|
async function fetchUserData() {
|
||||||
|
try {
|
||||||
|
userDataList.value = await api.get('site/userdata/latest')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存站点排序
|
// 保存站点排序
|
||||||
async function savaSitesPriority() {
|
async function savaSitesPriority() {
|
||||||
// 重新排序
|
// 重新排序
|
||||||
const priorities = dataList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
const priorities = siteList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -53,12 +65,27 @@ async function savaSitesPriority() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据站点ID获取站点数据
|
||||||
|
function getUserData(domain: string) {
|
||||||
|
return userDataList.value.find(userData => userData.domain === domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新站点事件时
|
||||||
|
function onSiteSave() {
|
||||||
|
siteAddDialog.value = false
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(fetchData)
|
onBeforeMount(() => {
|
||||||
|
fetchData()
|
||||||
|
fetchUserData()
|
||||||
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
if (!loading.value) {
|
if (!loading.value) {
|
||||||
fetchData()
|
fetchData()
|
||||||
|
fetchUserData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -67,8 +94,8 @@ onActivated(() => {
|
|||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
<div>
|
<div>
|
||||||
<draggable
|
<draggable
|
||||||
v-if="dataList.length > 0"
|
v-if="siteList.length > 0"
|
||||||
v-model="dataList"
|
v-model="siteList"
|
||||||
@end="savaSitesPriority"
|
@end="savaSitesPriority"
|
||||||
handle=".cursor-move"
|
handle=".cursor-move"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
@@ -76,12 +103,12 @@ onActivated(() => {
|
|||||||
:component-data="{ 'class': 'grid gap-3 grid-site-card' }"
|
:component-data="{ 'class': 'grid gap-3 grid-site-card' }"
|
||||||
>
|
>
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
<SiteCard :site="element" @remove="fetchData" @update="fetchData" />
|
<SiteCard :site="element" :data="getUserData(element.domain)" @remove="fetchData" @update="fetchData" />
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="dataList.length === 0 && isRefreshed"
|
v-if="siteList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有站点"
|
error-title="没有站点"
|
||||||
error-description="已添加并支持的站点将会在这里显示。"
|
error-description="已添加并支持的站点将会在这里显示。"
|
||||||
@@ -102,12 +129,7 @@ onActivated(() => {
|
|||||||
v-if="siteAddDialog"
|
v-if="siteAddDialog"
|
||||||
v-model="siteAddDialog"
|
v-model="siteAddDialog"
|
||||||
oper="add"
|
oper="add"
|
||||||
@save="
|
@save="onSiteSave"
|
||||||
() => {
|
|
||||||
siteAddDialog = false
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
@close="siteAddDialog = false"
|
@close="siteAddDialog = false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -229,9 +229,12 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 监听 localStorage 中的用户头像变化
|
// 监听 localStorage 中的用户头像变化
|
||||||
watch(() => store.state.auth.avatar, () => {
|
watch(
|
||||||
currentAvatar.value = store.state.auth.avatar
|
() => store.state.auth.avatar,
|
||||||
})
|
() => {
|
||||||
|
currentAvatar.value = store.state.auth.avatar
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -271,7 +274,7 @@ watch(() => store.state.auth.avatar, () => {
|
|||||||
</VBtn>
|
</VBtn>
|
||||||
|
|
||||||
<VBtn
|
<VBtn
|
||||||
:color="accountInfo.is_otp ? 'error' : 'info'"
|
:color="accountInfo.is_otp ? 'warning' : 'success'"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
|
@click.stop="accountInfo.is_otp ? disableOtp() : getOtpUri()"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user