Merge remote-tracking branch 'upstream/main'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
hao.dai
2024-04-24 15:43:56 +08:00
51 changed files with 770 additions and 528 deletions
+2
View File
@@ -10,9 +10,11 @@ declare module 'vue' {
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default'] DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default'] ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default'] ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default'] ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
} }
} }
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.8.1-3", "version": "1.8.3-1",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {
@@ -81,6 +81,7 @@
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"dayjs": "^1.11.10",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-typescript": "^3.5.1", "eslint-import-resolver-typescript": "^3.5.1",
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// 定义触发的自定义事件 // 定义触发的自定义事件
const emit = defineEmits(['click']) const emit = defineEmits(['click', 'update:modelValue'])
// 按钮点击 // 按钮点击
function onClick() { function onClick() {
emit('update:modelValue', false)
emit('click') emit('click')
} }
</script> </script>
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String
})
</script>
<template>
<div
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!props.text"
size="48"
indeterminate
color="primary"
/>
<VProgressCircular
v-if="props.progress"
class="mb-3"
color="primary"
:model-value="props.progress"
size="64"
/>
<span>{{ props.text }}</span>
</div>
</template>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts" setup>
interface Props {
color?: string
message?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
<VBadge :color="props.color" bordered>
<template #badge>
<VIcon icon="mdi-pulse"></VIcon>
</template>
</VBadge>
</div>
</template>
+27 -19
View File
@@ -1,5 +1,12 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import ZH_CN from 'dayjs/locale/zh-cn'
import { isToday } from './index' import { isToday } from './index'
dayjs.extend(relativeTime)
dayjs.locale(ZH_CN)
export function avatarText(value: string) { export function avatarText(value: string) {
if (!value) if (!value)
return '' return ''
@@ -19,7 +26,7 @@ export function kFormatter(num: number) {
* Format and return date in Humanize format * Format and return date in Humanize format
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format * Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
* Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat * Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
* @param {String} value date to format * @param {string} value date to format
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with * @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
*/ */
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) { export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
@@ -32,8 +39,8 @@ export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions
/** /**
* Return short human friendly month representation of date * Return short human friendly month representation of date
* Can also convert date to only time if date is of today (Better UX) * Can also convert date to only time if date is of today (Better UX)
* @param {String} value date to format * @param {string} value date to format
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current * @param {boolean} toTimeForCurrentDay Shall convert to time if day is today/current
*/ */
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) { export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
const date = new Date(value) const date = new Date(value)
@@ -107,7 +114,7 @@ export function formatBytes(bytes: number, decimals = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}` return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
} }
// 格式化剧集列表 // 格式化剧集列表
@@ -150,20 +157,21 @@ export function formatEp(nums: number[]): string {
// 将yyyy-mm-dd hh:mm:ss转换为时间差,如:1小时前,1天前 // 将yyyy-mm-dd hh:mm:ss转换为时间差,如:1小时前,1天前
export function formatDateDifference(dateString: string): string { export function formatDateDifference(dateString: string): string {
const date = new Date(dateString) // const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
const currentDate = new Date() // const secondsDifference = Math.floor(timeDifference / 1000)
const timeDifference = currentDate.getTime() - date.getTime() // const minutesDifference = Math.floor(secondsDifference / 60)
const secondsDifference = Math.floor(timeDifference / 1000) // const hoursDifference = Math.floor(minutesDifference / 60)
const minutesDifference = Math.floor(secondsDifference / 60) // const daysDifference = Math.floor(hoursDifference / 24)
const hoursDifference = Math.floor(minutesDifference / 60)
const daysDifference = Math.floor(hoursDifference / 24)
if (daysDifference > 0) // if (daysDifference > 0)
return `${daysDifference}天前` // return `${daysDifference}天前`
else if (hoursDifference > 0) // else if (hoursDifference > 0)
return `${hoursDifference}小时前` // return `${hoursDifference}小时前`
else if (minutesDifference > 0) // else if (minutesDifference > 0)
return `${minutesDifference}分钟前` // return `${minutesDifference}分钟前`
else // else
return '刚刚' // return '刚刚'
if (!dateString)
return ''
return dayjs(dateString).fromNow()
} }
+7 -3
View File
@@ -33,12 +33,16 @@ export function isToday(date: Date) {
) )
} }
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒 /**
* 计算时间差,返回xx天/xx小时/xx分钟/xx秒
*
* @deprecated 建议使用:@core/utils/formatters.ts formatDateDifference
*/
export function calculateTimeDifference(inputTime: string): string { export function calculateTimeDifference(inputTime: string): string {
if (!inputTime) if (!inputTime)
return '' return ''
const inputDate = new Date(inputTime) const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date() const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime() const timeDifference = currentDate.getTime() - inputDate.getTime()
@@ -70,7 +74,7 @@ export function calculateTimeDiff(inputTime: string): string {
return '' return ''
// 使用当前时区 // 使用当前时区
const inputDate = new Date(inputTime) const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date() const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime() const timeDifference = currentDate.getTime() - inputDate.getTime()
+21 -5
View File
@@ -1,13 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import api from '@/api'
import store from './store' import store from './store'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -41,10 +37,30 @@ function startSSEMessager() {
} }
} }
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem("MP_DASHBOARD")) {
localStorage.setItem("MP_DASHBOARD", data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem("MP_DASHBOARD")) {
return
}
await loadDashboardConfig()
}
// 页面加载时,加载当前用户数据 // 页面加载时,加载当前用户数据
onBeforeMount(async () => { onBeforeMount(async () => {
setTheme() setTheme()
startSSEMessager() startSSEMessager()
await tryLoadDashboardConfig()
}) })
</script> </script>
+30
View File
@@ -88,6 +88,9 @@ export interface Subscribe {
// 保存目录 // 保存目录
save_path: string save_path: string
// 时间
date: string
} }
// 历史记录 // 历史记录
@@ -505,6 +508,33 @@ export interface Site {
is_active: boolean is_active: boolean
} }
// 站点使用统计
export interface SiteStatistic {
// 站点主域名Key
domain?: string
// 成功次数
success?: number
// 失败次数
fail?: number
// 平均耗时
seconds?: number
// 最后一次访问状态 0-成功 1-失败
lst_state?: number
// 最后访问时间
lst_mod_date?: string
// 耗时记录 JSON
note?: string
}
// 正在下载 // 正在下载
export interface DownloadingInfo { export interface DownloadingInfo {
+4 -5
View File
@@ -1,9 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Axios } from 'axios' import type { Axios } from 'axios'
import axios from 'axios' import axios from 'axios'
import List from './filebrowser/List.vue' import FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import Toolbar from './filebrowser/Toolbar.vue'
import type { EndPoints } from '@/api/types' import type { EndPoints } from '@/api/types'
// 输入参数 // 输入参数
@@ -100,7 +99,7 @@ onMounted(() => {
<template> <template>
<VCard class="mx-auto" :loading="loading > 0 || !path"> <VCard class="mx-auto" :loading="loading > 0 || !path">
<div v-if="path"> <div v-if="path">
<Toolbar <FileToolbar
:path="path" :path="path"
:storages="storagesArray" :storages="storagesArray"
:storage="activeStorage" :storage="activeStorage"
@@ -111,7 +110,7 @@ onMounted(() => {
@foldercreated="refreshPending = true" @foldercreated="refreshPending = true"
@sortchanged="sortChanged" @sortchanged="sortChanged"
/> />
<List <FileList
:path="path" :path="path"
:storage="activeStorage" :storage="activeStorage"
:icons="fileIcons" :icons="fileIcons"
+1 -1
View File
@@ -25,7 +25,7 @@ function goPlay() {
// 计算图片地址 // 计算图片地址
const getImgUrl = computed(() => { const getImgUrl = computed(() => {
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
}) })
</script> </script>
@@ -1,50 +0,0 @@
<script setup lang="ts">
import { kFormatter } from '@core/utils/formatters'
interface Props {
title: string
color?: string
icon: string
stats: number
change: number
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
size="44"
rounded
:color="props.color"
variant="tonal"
class="me-4"
>
<VIcon
:icon="props.icon"
size="30"
/>
</VAvatar>
<div>
<span class="text-caption">{{ props.title }}</span>
<div class="d-flex align-center flex-wrap">
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
<div
v-if="props.change"
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
>
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>
@@ -1,56 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
color?: string
icon: string
stats: string
change: number
subtitle: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
v-if="props.icon"
size="38"
:color="props.color"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<VSpacer />
<MoreBtn class="me-n3 mt-n1" />
</VCardText>
<VCardText>
<h6 class="text-sm font-weight-semibold mb-2">
{{ props.title }}
</h6>
<div
v-if="props.change"
class="d-flex align-center mb-2"
>
<span class="font-weight-semibold text-h5 me-2">{{ props.stats }}</span>
<span
:class="isPositive ? 'text-success' : 'text-error'"
class="text-caption"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<span class="text-caption">{{ props.subtitle }}</span>
</VCardText>
</VCard>
</template>
@@ -1,65 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
subtitle: string
stats: string
change: number
image: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard class="overflow-visible">
<div class="d-flex position-relative">
<VCardText>
<h6 class="text-base font-weight-semibold mb-4">
{{ props.title }}
</h6>
<div class="d-flex align-center flex-wrap mb-4">
<h5 class="text-h5 font-weight-semibold me-2">
{{ props.stats }}
</h5>
<span
class="text-caption"
:class="isPositive ? 'text-success' : 'text-error'"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<VChip
v-if="props.subtitle"
size="small"
:color="props.color"
>
{{ props.subtitle }}
</VChip>
</VCardText>
<VSpacer />
<div class="illustrator-img">
<VImg
v-if="props.image"
:src="props.image"
:width="110"
/>
</div>
</div>
</VCard>
</template>
<style lang="scss">
.illustrator-img {
position: absolute;
inset-block-end: 0;
inset-inline-end: 5%;
}
</style>
+2 -2
View File
@@ -56,7 +56,7 @@ function getImgUrl(url: string) {
if (!url) if (!url)
return getDefaultImage() return getDefaultImage()
else else
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(url).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
} }
// 根据多张图片生成媒体库封面 // 根据多张图片生成媒体库封面
@@ -68,7 +68,7 @@ async function drawImages(imageList: string[]) {
// 为所有图片添加system/img前缀 // 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++) for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(IMAGES[i]).replace(/%2F/g, '/')}` IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
// canvas // canvas
const canvas = canvasRef.value const canvas = canvasRef.value
+26 -19
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType, Ref } from 'vue' import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue' import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -157,7 +157,7 @@ async function addSubscribe(season = 0) {
// 弹出订阅编辑弹窗 // 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) { if (result.success && seasonsSelected.value.length <= 1) {
const show_edit_dialog = await querySubscribeRules() const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) { if (show_edit_dialog) {
subscribeId.value = result.data.id subscribeId.value = result.data.id
subscribeEditDialog.value = true subscribeEditDialog.value = true
@@ -313,11 +313,16 @@ async function getMediaSeasons() {
} }
// 查询订阅弹窗规则 // 查询订阅弹窗规则
async function querySubscribeRules() { async function queryDefaultSubscribeConfig() {
try { try {
const result: { [key: string]: any } = await api.get( let subscribe_config_url = ''
'system/setting/DefaultFilterRules', if (props.media?.type === '电影')
) subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) if (result.data?.value)
return result.data.value.show_edit_dialog return result.data.value.show_edit_dialog
} }
@@ -364,14 +369,16 @@ function getExistText(season: number) {
} }
// 打开详情页 // 打开详情页
function goMediaDetail() { function goMediaDetail(isHovering = false) {
router.push({ if (isHovering) {
path: '/media', router.push({
query: { path: '/media',
mediaid: getMediaId(), query: {
type: props.media?.type, mediaid: getMediaId(),
}, type: props.media?.type,
}) },
})
}
} }
// 开始搜索 // 开始搜索
@@ -400,7 +407,7 @@ const getImgUrl: Ref<string> = computed(() => {
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
// 如果地址中包含douban则使用中转代理 // 如果地址中包含douban则使用中转代理
if (url.includes('doubanio.com')) if (url.includes('doubanio.com'))
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}` return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
return url return url
}) })
@@ -416,14 +423,14 @@ function getSeasonPoster(posterPath: string) {
function formatAirDate(airDate: string) { function formatAirDate(airDate: string) {
if (!airDate) if (!airDate)
return '' return ''
const date = new Date(airDate) const date = new Date(airDate.replaceAll(/-/g, '/'))
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}` return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
} }
// 从yyyy-mm-dd中提取年份 // 从yyyy-mm-dd中提取年份
function getYear(airDate: string) { function getYear(airDate: string) {
if (!airDate) if (!airDate)
return '' return ''
const date = new Date(airDate) const date = new Date(airDate.replaceAll(/-/g, '/'))
return date.getFullYear() return date.getFullYear()
} }
</script> </script>
@@ -440,7 +447,7 @@ function getYear(airDate: string) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }"
@click.stop="goMediaDetail" @click.stop="goMediaDetail(hover.isHovering)"
> >
<VImg <VImg
aspect-ratio="2/3" aspect-ratio="2/3"
@@ -592,7 +599,7 @@ function getYear(airDate: string) {
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-if="subscribeEditDialog" v-if="subscribeEditDialog"
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="subscribeId" :subid="subscribeId"
+1 -1
View File
@@ -91,7 +91,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage return noImage
// 如果是网络图片则使用代理后返回 // 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http')) if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}` return `./plugin_icon/${props.plugin?.plugin_icon}`
}) })
+3 -3
View File
@@ -223,7 +223,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage return noImage
// 如果是网络图片则使用代理后返回 // 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http')) if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}` return `./plugin_icon/${props.plugin?.plugin_icon}`
}) })
@@ -482,7 +482,7 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
:title="`${props.plugin?.plugin_name} - 配置`" :title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t" class="rounded-t"
> >
<DialogCloseBtn @click="pluginConfigDialog = false" /> <DialogCloseBtn v-model='pluginConfigDialog' />
<VCardText> <VCardText>
<FormRender <FormRender
v-for="(item, index) in pluginFormItems" v-for="(item, index) in pluginFormItems"
@@ -516,7 +516,7 @@ watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
:title="`${props.plugin?.plugin_name}`" :title="`${props.plugin?.plugin_name}`"
class="rounded-t" class="rounded-t"
> >
<DialogCloseBtn @click="pluginInfoDialog = false" /> <DialogCloseBtn v-model='pluginInfoDialog' />
<VCardText> <VCardText>
<PageRender <PageRender
v-for="(item, index) in pluginPageItems" v-for="(item, index) in pluginPageItems"
+4 -4
View File
@@ -31,12 +31,12 @@ const getImgUrl = computed(() => {
if (imageLoadError.value) if (imageLoadError.value)
return noImage return noImage
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
}) })
// 跳转播放 // 跳转播放
function goPlay() { function goPlay(isHovering = false) {
if (props.media?.link) if (props.media?.link && isHovering)
window.open(props.media?.link, '_blank') window.open(props.media?.link, '_blank')
} }
</script> </script>
@@ -53,7 +53,7 @@ function goPlay() {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded, 'ring-1': isImageLoaded,
}" }"
@click.stop="goPlay" @click.stop="goPlay(hover.isHovering)"
> >
<VImg <VImg
aspect-ratio="2/3" aspect-ratio="2/3"
+46 -5
View File
@@ -1,12 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue' import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
import SiteTorrentTable from '../table/SiteTorrentTable.vue' import SiteTorrentTable from '../table/SiteTorrentTable.vue'
import { requiredValidator } from '@/@validators' import { requiredValidator } from '@/@validators'
import api from '@/api' import api from '@/api'
import type { Site } from '@/api/types' import type { Site, SiteStatistic } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue' import ExistIcon from '@core/components/ExistIcon.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数 // 输入参数
const cardProps = defineProps({ const cardProps = defineProps({
@@ -58,6 +59,9 @@ const userPwForm = ref({
code: '', code: '',
}) })
// 站点使用统计
const siteStats = ref<SiteStatistic>({})
// 查询站点图标 // 查询站点图标
async function getSiteIcon() { async function getSiteIcon() {
try { try {
@@ -82,6 +86,18 @@ async function testSite() {
testButtonText.value = '测试' testButtonText.value = '测试'
testButtonDisable.value = false testButtonDisable.value = false
getSiteStats()
}
catch (error) {
console.error(error)
}
}
// 查询站点使用统计
async function getSiteStats() {
try {
siteStats.value = (await api.get(`site/statistic/${cardProps.site?.domain}`))
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -140,9 +156,33 @@ function openSitePage() {
window.open(cardProps.site?.url, '_blank') window.open(cardProps.site?.url, '_blank')
} }
// 根据站点状态显示不同的状态图标
const statColor = computed(() => {
if (isNullOrEmptyObject(siteStats.value)){
return 'secondary'
}
if (siteStats.value?.lst_state == 1){
return 'error'
}
else if (siteStats.value?.lst_state == 0){
if (!siteStats.value?.seconds)
return 'secondary'
if (siteStats.value?.seconds >= 5)
return 'warning'
return 'success'
}
})
// 监听resourceDialog,如果为false则重新查询站点使用统计
watch(resourceDialog, (value) => {
if (!value)
getSiteStats()
})
// 装载时查询站点图标 // 装载时查询站点图标
onMounted(() => { onMounted(() => {
getSiteIcon() getSiteIcon()
getSiteStats()
}) })
</script> </script>
@@ -163,16 +203,17 @@ onMounted(() => {
<VImg :src="siteIcon" /> <VImg :src="siteIcon" />
</VAvatar> </VAvatar>
</template> </template>
<VCardItem> <VCardItem>
<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>
<VCardSubtitle> <VCardSubtitle>
{{ cardProps.site?.url }} <span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
</VCardSubtitle> </VCardSubtitle>
</VCardItem> </VCardItem>
<ExistIcon v-if="cardProps.site?.is_active" /> <StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
<VCardText class="py-2"> <VCardText class="py-2">
<VTooltip <VTooltip
@@ -325,7 +366,7 @@ onMounted(() => {
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<SiteAddEditForm <SiteAddEditDialog
v-if="siteEditDialog" v-if="siteEditDialog"
v-model="siteEditDialog" v-model="siteEditDialog"
:siteid="cardProps.site?.id" :siteid="cardProps.site?.id"
+6 -8
View File
@@ -1,7 +1,7 @@
<script lang='ts' setup> <script lang='ts' setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue' import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { calculateTimeDifference } from '@/@core/utils' import { formatDateDifference } from '@/@core/utils/formatters'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
@@ -26,11 +26,9 @@ const subscribeEditDialog = ref(false)
// 上一次更新时间 // 上一次更新时间
const lastUpdateText = ref( const lastUpdateText = ref(
`${ props.media && props.media.last_update
props.media?.last_update ? formatDateDifference(props.media.last_update)
? `${calculateTimeDifference(props.media?.last_update || '')}` : '',
: ''
}`,
) )
// 图片加载完成响应 // 图片加载完成响应
@@ -284,7 +282,7 @@ const dropdownItems = ref([
/> />
</VCard> </VCard>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-if="subscribeEditDialog" v-if="subscribeEditDialog"
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="props.media?.id" :subid="props.media?.id"
+1 -1
View File
@@ -293,7 +293,7 @@ onMounted(() => {
<VExpandTransition> <VExpandTransition>
<div v-show="showMoreTorrents"> <div v-show="showMoreTorrents">
<VDivider /> <VDivider />
<VChipGroup class="p-3"> <VChipGroup class="p-3" column>
<VChip <VChip
v-for="(item, index) in props.more" v-for="(item, index) in props.more"
:key="index" :key="index"
@@ -0,0 +1,45 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VDialog
width="40rem"
scrollable
max-height="85vh"
>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue' import TmdbSelector from '../misc/TmdbSelector.vue'
import store from '@/store' import store from '@/store'
import api from '@/api' import api from '@/api'
import { numberValidator } from '@/@validators' import { numberValidator } from '@/@validators'
@@ -306,7 +306,7 @@ async function transfer() {
scrollable scrollable
max-height="85vh" max-height="85vh"
> >
<TmdbSelectorCard <TmdbSelector
v-model="transferForm.tmdbid" v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false" @close="tmdbSelectorDialog = false"
/> />
@@ -43,6 +43,8 @@ const subscribeForm = ref<Subscribe>({
username: '', username: '',
current_priority: 0, current_priority: 0,
save_path: '', save_path: '',
date: '',
show_edit_dialog: false
}) })
// //
@@ -270,7 +272,7 @@ onMounted(() => {
max-width="60rem" max-width="60rem"
> >
<VCard <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" class="rounded-t"
> >
<VCardText class="pt-2"> <VCardText class="pt-2">
@@ -412,6 +414,16 @@ onMounted(() => {
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持" hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
/> />
</VCol> </VCol>
<VCol v-if="props.default"
cols="12"
md="4"
>
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
/>
</VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -0,0 +1,239 @@
<script lang="ts" setup>
import api from '@/api';
import { Subscribe } from '@/api/types';
import { formatDateDifference } from '@core/utils/formatters'
// 输入参数
const props = defineProps({
type: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'save'])
// 订阅历史列表
const historyList = ref<Subscribe[]>([])
// 当前加载数据
const currData = ref<Subscribe[]>([])
// 当前页
const currentPage = ref(1)
// 每页数量
const pageSize = ref(30)
// 是否加载中
const loading = ref(false)
// 是否加载完成
const isRefreshed = ref(false)
// 进度框
const progressDialog = ref(false)
// 进度文字
const progressText = ref('正在重新订阅...')
// 调用API查询列表
async function loadHistory({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 调用API查询列表
try {
// 设置加载中
loading.value = true
currData.value = await api.get(`subscribe/history/${props.type}`, {
params: {
page: currentPage.value,
count: pageSize.value,
},
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
historyList.value = [...historyList.value, ...currData.value]
// 页码+1
currentPage.value++
// 返回加载成功
done('ok')
}
// 取消加载中
loading.value = false
} catch (e) {
console.error(e)
// 返回加载失败
done('error')
}
}
// 重新订阅
async function reSubscribe(item: Subscribe) {
if (item.type === '电影')
progressText.value = `正在重新订阅 ${item.name} ...`
else
progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: {[key: string]: any} = await api.post('subscribe', item)
if (result.success){
emit('save')
}
} catch (e) {
console.error(e)
}
progressDialog.value = false
}
// 删除记录
async function deleteHistory(item: Subscribe) {
try {
const result: {[key: string]: any} = await api.delete(`subscribe/history/${item.id}`)
if (result.success){
historyList.value = historyList.value.filter((i) => i.id !== item.id)
}
} catch (e) {
console.error(e)
}
}
// 弹出菜单
const dropdownItems = ref([
{
title: '重新订阅',
value: 1,
color: '',
props: {
prependIcon: 'mdi-redo',
click: reSubscribe,
},
},
{
title: '删除',
value: 2,
color: 'error',
props: {
prependIcon: 'mdi-delete',
click: deleteHistory,
},
}
])
</script>
<template>
<VDialog
scrollable
max-width="50rem"
max-height="90vh"
>
<VCard
class="mx-auto"
width="100%"
>
<VCardItem class="pb-0">
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<DialogCloseBtn @click="() => { emit('close') }" />
<VList
lines="two"
>
<VInfiniteScroll
mode="intersect"
side="end"
:items="historyList"
class="overflow-hidden"
@load="loadHistory"
>
<template #loading>
<LoadingBanner />
</template>
<template #empty />
<template v-for="(item, i) in historyList" :key="i">
<VListItem>
<template #prepend>
<VImg
height="75"
width="50"
:src="item.poster"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</template>
<VListItemTitle v-if="item.type == '电视剧'">
{{ item.name }} <span class="text-sm"> {{ item.season }} </span>
</VListItemTitle>
<VListItemTitle v-else>
{{ item.name }}
</VListItemTitle>
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>
</VInfiniteScroll>
</VList>
</VCard>
<!-- 进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</VDialog>
</template>
@@ -4,7 +4,7 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog' import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios' import axios from 'axios'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import ReorganizeForm from '../form/ReorganizeForm.vue' import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters' import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types' import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store' import store from '@/store'
@@ -535,7 +535,7 @@ onMounted(() => {
</VCard> </VCard>
</VDialog> </VDialog>
<!-- 文件整理弹窗 --> <!-- 文件整理弹窗 -->
<ReorganizeForm <ReorganizeDialog
v-if="transferPopper" v-if="transferPopper"
v-model="transferPopper" v-model="transferPopper"
:path="currentItem?.path" :path="currentItem?.path"
@@ -592,7 +592,7 @@ onMounted(() => {
@media (width <= 768px) { @media (width <= 768px) {
.virtual-scroll-div { .virtual-scroll-div {
block-size: calc(100vh - 16rem); block-size: calc(100vh - 17rem);
} }
} }
-39
View File
@@ -1,39 +0,0 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</template>
+11
View File
@@ -7,6 +7,7 @@ interface RenderProps {
text: string text: string
html: string html: string
content?: any content?: any
slots?: any
props?: any props?: any
} }
@@ -22,6 +23,7 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
html: '', html: '',
props: {}, props: {},
content: [], content: [],
slots: {},
}) })
</script> </script>
@@ -32,6 +34,15 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
v-bind="formItem.props" v-bind="formItem.props"
> >
{{ formItem.text }} {{ formItem.text }}
<template v-for="(content, name) in (formItem.slots || [])" :key="name" v-slot:[name]="{_props}">
<slot :name="name" v-bind="_props">
<PageRender
v-for="(slotItem, slotIndex) in (content || [])"
:key="slotIndex"
:config="slotItem"
/>
</slot>
</template>
<PageRender <PageRender
v-for="(innerItem, innerIndex) in (formItem.content || [])" v-for="(innerItem, innerIndex) in (formItem.content || [])"
:key="innerIndex" :key="innerIndex"
+7
View File
@@ -17,7 +17,13 @@ import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css' import 'vue-toast-notification/dist/theme-bootstrap.css'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'; import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
import 'vue3-perfect-scrollbar/style.css'; import 'vue3-perfect-scrollbar/style.css';
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts() loadFonts()
// 创建Vue实例 // 创建Vue实例
@@ -26,6 +32,7 @@ const app = createApp(App)
// 注册全局组件 // 注册全局组件
app.component('VAceEditor', VAceEditor) app.component('VAceEditor', VAceEditor)
.component('VApexChart', VueApexCharts) .component('VApexChart', VueApexCharts)
.component('VDialogCloseBtn', DialogCloseBtn)
// 注册插件 // 注册插件
app app
+13 -3
View File
@@ -9,6 +9,8 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue' import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue' import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue' import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import api from '@/api'
import { isNullOrEmptyObject } from '@/@core/utils'
// 仪表盘配置 // 仪表盘配置
const dashboard_names = { const dashboard_names = {
@@ -40,15 +42,23 @@ const default_config = {
playing: true, playing: true,
latest: true, latest: true,
} }
// 初始化默认值
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}')) const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
if (Object.keys(config.value).length === 0) { if (isNullOrEmptyObject(config.value)) {
config.value = default_config config.value = default_config
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
} }
// 设置项目 // 设置项目
function setDashboardConfig() { function setDashboardConfig() {
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value)) const data = JSON.stringify(config.value)
localStorage.setItem('MP_DASHBOARD', data)
// 保存到服务端
api.post('/user/config/Dashboard', data, {
headers: {
"Content-Type": "application/json"
}
})
dialog.value = false dialog.value = false
} }
</script> </script>
+29 -3
View File
@@ -66,6 +66,32 @@ async function fetchOTP() {
}) })
} }
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem("MP_DASHBOARD")) {
localStorage.setItem("MP_DASHBOARD", data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem("MP_DASHBOARD")) {
return
}
await loadDashboardConfig()
}
async function afterLogin() {
// 尝试加载用户监控面板配置(本地无配置时才加载)
await tryLoadDashboardConfig()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
}
// 登录获取token事件 // 登录获取token事件
function login() { function login() {
errorMessage.value = '' errorMessage.value = ''
@@ -102,9 +128,9 @@ function login() {
store.dispatch('auth/updateSuperUser', superuser) store.dispatch('auth/updateSuperUser', superuser)
store.dispatch('auth/updateUserName', username) store.dispatch('auth/updateUserName', username)
store.dispatch('auth/updateAvatar', avatar) store.dispatch('auth/updateAvatar', avatar)
// 跳转到首页或回原始页面 // 登录后处理
router.push(store.state.auth.originalPath ?? '/') afterLogin()
}) })
.catch((error: any) => { .catch((error: any) => {
// 登录失败,显示错误提示 // 登录失败,显示错误提示
+6 -5
View File
@@ -120,11 +120,12 @@ onMounted(() => {
</script> </script>
<template> <template>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"> <LoadingBanner
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" /> v-if="!isRefreshed"
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" /> class="mt-12"
<span>{{ progressText }}</span> :text="progressText"
</div> :progress="progressValue"
/>
<NoDataFound <NoDataFound
v-if="dataList.length === 0 && isRefreshed" v-if="dataList.length === 0 && isRefreshed"
:error-title="errorTitle" :error-title="errorTitle"
+20 -32
View File
@@ -52,70 +52,63 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回 // 如果正在加载中,直接返回
if (loading.value) { if (loading.value) {
done('ok') done('ok')
return return
} }
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错 // 加载到满屏或者加载出错
if (!hasScroll()) { if (!hasScroll()) {
// 加载多次 // 加载多次
while (!hasScroll()) { while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: getParams(), params: getParams(),
}) })
// 取消加载中
loading.value = false
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('ok') done('empty')
return return
} }
// 合并数据 // 合并数据
dataList.value = [...dataList.value, ...currData.value] dataList.value = [...dataList.value, ...currData.value]
// 页码+1 // 页码+1
page.value++ page.value++
// 返回加载成功
done('ok')
} }
} }
else { else {
// 加载一次 // 加载一次
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: getParams(), params: getParams(),
}) })
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
// 取消加载中 // 取消加载中
loading.value = false loading.value = false
// 返回加载成功
done('ok')
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
// 返回加载失败 // 返回加载失败
done('error') done('error')
} }
@@ -123,16 +116,10 @@ async function fetchData({ done }: { done: any }) {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll <VInfiniteScroll
mode="intersect" mode="intersect"
side="end" side="end"
@@ -141,6 +128,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData" @load="fetchData"
> >
<template #loading /> <template #loading />
<template #empty />
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-4 grid-media-card mx-3" class="grid gap-4 grid-media-card mx-3"
+33 -43
View File
@@ -8,7 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress' import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters' import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router' import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue' import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
// 输入参数 // 输入参数
const mediaProps = defineProps({ const mediaProps = defineProps({
@@ -46,11 +46,6 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号 // 订阅编号
const subscribeId = ref<number>() const subscribeId = ref<number>()
// 订阅规则
const subscribeRules = ref({
show_edit_dialog: false,
})
// 获得mediaid // 获得mediaid
function getMediaId() { function getMediaId() {
return mediaDetail.value?.tmdb_id return mediaDetail.value?.tmdb_id
@@ -230,9 +225,12 @@ async function addSubscribe(season = 0) {
) )
// 显示编辑弹窗 // 显示编辑弹窗
if (result.success && subscribeRules.value.show_edit_dialog) { if (result.success) {
subscribeId.value = result.data.id const show_edit_dialog = await queryDefaultSubscribeConfig()
subscribeEditDialog.value = true if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
} }
} }
catch (error) { catch (error) {
@@ -290,20 +288,6 @@ async function removeSubscribe(season: number) {
doneNProgress() doneNProgress()
} }
// 查询订阅弹窗规则
async function querySubscribeRules() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
subscribeRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 订阅按钮响应 // 订阅按钮响应
function handleSubscribe(season = 0) { function handleSubscribe(season = 0) {
if (isSubscribed.value) if (isSubscribed.value)
@@ -450,23 +434,35 @@ async function handlePlay() {
} }
} }
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (mediaProps.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
catch (error) {
console.log(error)
}
return false
}
onBeforeMount(() => { onBeforeMount(() => {
getMediaDetail() getMediaDetail()
querySubscribeRules()
}) })
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4"> <div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path"> <template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96"> <div class="vue-media-back absolute left-0 top-0 w-full h-96">
@@ -638,16 +634,10 @@ onBeforeMount(() => {
</VExpansionPanelTitle> </VExpansionPanelTitle>
<VExpansionPanelText> <VExpansionPanelText>
<template #default> <template #default>
<div <LoadingBanner
v-if="!seasonEpisodesInfo[season.season_number || 0]" v-if="!seasonEpisodesInfo[season.season_number || 0]"
class="mt-3 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-3"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<div class="flex flex-col justify-center divide-y divide-gray-700"> <div class="flex flex-col justify-center divide-y divide-gray-700">
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"> <div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
<div class="flex-1"> <div class="flex-1">
@@ -849,7 +839,7 @@ onBeforeMount(() => {
error-description="未识别到媒体信息" error-description="未识别到媒体信息"
/> />
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:subid="subscribeId" :subid="subscribeId"
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"
+26 -38
View File
@@ -42,74 +42,67 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回 // 如果正在加载中,直接返回
if (loading.value) { if (loading.value) {
done('ok') done('ok')
return return
} }
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错 // 加载到满屏或者加载出错
if (!hasScroll()) { if (!hasScroll()) {
// 加载多次 // 加载多次
while (!hasScroll()) { while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: { params: {
page: page.value, page: page.value,
}, },
}) })
// 取消加载中
loading.value = false
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
} }
else { else {
// 加载一次 // 加载一次
// 设置加载中
loading.value = true
// 请求API // 请求API
currData.value = await api.get(props.apipath, { currData.value = await api.get(props.apipath, {
params: { params: {
page: page.value, page: page.value,
}, },
}) })
// 标计为已请求完成 // 标计为已请求完成
isRefreshed.value = true isRefreshed.value = true
if (currData.value.length === 0) { if (currData.value.length === 0) {
// 如果没有数据,跳出 // 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok') done('ok')
return
} }
// 取消加载中
// 合并数据 loading.value = false
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
} }
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
// 返回加载失败 // 返回加载失败
done('error') done('error')
} }
@@ -117,16 +110,10 @@ async function fetchData({ done }: { done: any }) {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<VInfiniteScroll <VInfiniteScroll
mode="intersect" mode="intersect"
side="end" side="end"
@@ -135,6 +122,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData" @load="fetchData"
> >
<template #loading /> <template #loading />
<template #empty />
<div <div
v-if="dataList.length > 0 && props.type === 'tmdb'" v-if="dataList.length > 0 && props.type === 'tmdb'"
class="grid gap-4 grid-media-card mx-3" class="grid gap-4 grid-media-card mx-3"
+3 -9
View File
@@ -48,16 +48,10 @@ onBeforeMount(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="personDetail.id" class="max-w-8xl mx-auto px-4"> <div v-if="personDetail.id" class="max-w-8xl mx-auto px-4">
<div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row "> <div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row ">
<VAvatar <VAvatar
+7 -21
View File
@@ -117,7 +117,7 @@ function pluginIcon(item: Plugin) {
return noImage return noImage
// 如果是网络图片则使用代理后返回 // 如果是网络图片则使用代理后返回
if (item?.plugin_icon?.startsWith('http')) if (item?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(item?.plugin_icon).replace(/%2F/g, '/')}` return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}`
return `./plugin_icon/${item?.plugin_icon}` return `./plugin_icon/${item?.plugin_icon}`
} }
@@ -211,17 +211,10 @@ onBeforeMount(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-4 grid-plugin-card" class="grid gap-4 grid-plugin-card"
@@ -285,17 +278,10 @@ onBeforeMount(() => {
</VToolbar> </VToolbar>
</div> </div>
<VCardText> <VCardText>
<div <LoadingBanner
v-if="!isAppMarketLoaded" v-if="!isAppMarketLoaded"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isAppMarketLoaded"
size="48"
indeterminate
color="primary"
/>
</div>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card"> <div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
<PluginAppCard <PluginAppCard
v-for="data in sortedUninstalledList" v-for="data in sortedUninstalledList"
+3 -10
View File
@@ -67,17 +67,10 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<PullRefresh <PullRefresh
v-model="loading" v-model="loading"
@refresh="onRefresh" @refresh="onRefresh"
+7 -4
View File
@@ -3,7 +3,7 @@ import { ref, unref } from 'vue'
import { useToast } from 'vue-toast-notification' import { useToast } from 'vue-toast-notification'
import api from '@/api' import api from '@/api'
import type { TransferHistory } from '@/api/types' import type { TransferHistory } from '@/api/types'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue' import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
@@ -378,9 +378,12 @@ onMounted(fetchData)
<VIcon :icon="getIcon(item.type || '')" /> <VIcon :icon="getIcon(item.type || '')" />
</VAvatar> </VAvatar>
<div class="d-flex flex-column ms-1"> <div class="d-flex flex-column ms-1">
<span class="d-block text-high-emphasis min-w-20"> <span v-if="item.type === '电视剧'" class="d-block text-high-emphasis min-w-20">
{{ item?.title }} {{ item?.seasons }}{{ item?.episodes }} {{ item?.title }} {{ item?.seasons }}{{ item?.episodes }}
</span> </span>
<span v-else class="d-block text-high-emphasis min-w-20">
{{ item?.title }}
</span>
<small>{{ item?.category }}</small> <small>{{ item?.category }}</small>
</div> </div>
</div> </div>
@@ -479,7 +482,7 @@ onMounted(fetchData)
</VCard> </VCard>
</VBottomSheet> </VBottomSheet>
<!-- 文件整理弹窗 --> <!-- 文件整理弹窗 -->
<ReorganizeForm <ReorganizeDialog
v-if="redoDialog" v-if="redoDialog"
v-model="redoDialog" v-model="redoDialog"
:logids="redoIds" :logids="redoIds"
@@ -534,7 +537,7 @@ onMounted(fetchData)
@media (width <= 768px) { @media (width <= 768px) {
.data-table-div { .data-table-div {
block-size: calc(100vh - 16rem); block-size: calc(100vh - 17rem);
} }
} }
</style> </style>
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { calculateTimeDifference } from '@/@core/utils' import { formatDateDifference } from '@/@core/utils/formatters'
import api from '@/api' import api from '@/api'
// 系统环境变量 // 系统环境变量
@@ -62,7 +62,7 @@ async function queryAllRelease() {
// 计算发布时间 // 计算发布时间
function releaseTime(releaseDate: string) { function releaseTime(releaseDate: string) {
// 上一次更新时间 // 上一次更新时间
return `${calculateTimeDifference(releaseDate)}` return formatDateDifference(releaseDate)
} }
onMounted(() => { onMounted(() => {
+2 -2
View File
@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue' import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue' import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型 // 规则卡片类型
interface FilterCard { interface FilterCard {
@@ -420,7 +420,7 @@ onMounted(() => {
width="60rem" width="60rem"
scrollable scrollable
> >
<ImportCodeForm <ImportCodeDialog
v-model="importCodeString" v-model="importCodeString"
title="导入优先级规则" title="导入优先级规则"
@close="importCodeDialog = false" @close="importCodeDialog = false"
+3 -11
View File
@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue' import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types' import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator' import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue' import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型 // 规则卡片类型
interface FilterCard { interface FilterCard {
@@ -41,8 +41,7 @@ const defaultFilterRules = ref({
exclude: '', exclude: '',
movie_size: '', movie_size: '',
tv_size: '', tv_size: '',
min_seeders: 0, min_seeders: 0
show_edit_dialog: false,
}) })
// 订阅模式选择项 // 订阅模式选择项
@@ -622,13 +621,6 @@ onMounted(() => {
hint="小于该值的资源将被过滤掉,0表示不过滤" hint="小于该值的资源将被过滤掉,0表示不过滤"
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="defaultFilterRules.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后,添加订阅时将自动弹出订阅编辑框,要设置更多订阅选项"
/>
</VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>
@@ -648,7 +640,7 @@ onMounted(() => {
width="60rem" width="60rem"
scrollable scrollable
> >
<ImportCodeForm <ImportCodeDialog
v-model="importCodeString" v-model="importCodeString"
title="导入优先级规则" title="导入优先级规则"
@close="importCodeDialog = false" @close="importCodeDialog = false"
+5 -12
View File
@@ -3,7 +3,7 @@ import api from '@/api'
import type { Site } from '@/api/types' import type { Site } 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 SiteAddEditForm from '@/components/form/SiteAddEditForm.vue' import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import { useDefer } from '@/@core/utils/dom' import { useDefer } from '@/@core/utils/dom'
// 数据列表 // 数据列表
@@ -35,17 +35,10 @@ onBeforeMount(fetchData)
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<div <div
v-if="dataList.length > 0" v-if="dataList.length > 0"
class="grid gap-3 grid-site-card" class="grid gap-3 grid-site-card"
@@ -80,7 +73,7 @@ onBeforeMount(fetchData)
@click="siteAddDialog = true" @click="siteAddDialog = true"
/> />
<!-- 新增站点弹窗 --> <!-- 新增站点弹窗 -->
<SiteAddEditForm <SiteAddEditDialog
v-if="siteAddDialog" v-if="siteAddDialog"
v-model="siteAddDialog" v-model="siteAddDialog"
oper="add" oper="add"
+7 -7
View File
@@ -155,7 +155,7 @@ onMounted(() => {
</VCard> </VCard>
</div> </div>
<div class="md:hidden"> <div class="md:hidden">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`"> <VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<template #activator="{ props }"> <template #activator="{ props }">
<VImg <VImg
height="60" height="60"
@@ -384,8 +384,8 @@ onMounted(() => {
} }
.v-application .fc .fc-daygrid-day-number { .v-application .fc .fc-daygrid-day-number {
padding-block: 0rem; padding-block: 0;
padding-inline: 0rem; padding-inline: 0;
} }
.v-application .fc .fc-list-event-dot { .v-application .fc .fc-list-event-dot {
@@ -435,7 +435,7 @@ onMounted(() => {
margin-inline-end: 0.25rem; margin-inline-end: 0.25rem;
} }
@media (max-width: 1264px) { @media (width <= 1264px) {
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button { .v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button {
display: block !important; display: block !important;
} }
@@ -481,10 +481,10 @@ onMounted(() => {
} }
.v-application .fc .fc-button-primary { .v-application .fc .fc-button-primary {
background-color: transparent;
border: none; border: none;
outline: none; background-color: transparent;
color: var(--v-theme-on-surface); color: var(--v-theme-on-surface);
outline: none;
} }
.v-application .fc .fc-button-primary:hover { .v-application .fc .fc-button-primary:hover {
@@ -492,7 +492,7 @@ onMounted(() => {
color: rgb(var(--v-theme-primary)); color: rgb(var(--v-theme-primary));
} }
@media (max-width: 776px) { @media (width <= 776px) {
.fc-daygrid-event-harness { .fc-daygrid-event-harness {
display: flex; display: flex;
align-items: center; align-items: center;
+28 -12
View File
@@ -4,7 +4,8 @@ import api from '@/api'
import type { Subscribe } from '@/api/types' import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue' import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue' import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue' import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import store from '@/store' import store from '@/store'
// 输入参数 // 输入参数
@@ -21,6 +22,9 @@ const dataList = ref<Subscribe[]>([])
// 弹窗 // 弹窗
const subscribeEditDialog = ref(false) const subscribeEditDialog = ref(false)
// 历史记录弹窗
const historyDialog = ref(false)
// 获取订阅列表数据 // 获取订阅列表数据
async function fetchData() { async function fetchData() {
try { try {
@@ -58,17 +62,10 @@ const filteredDataList = computed(() => {
</script> </script>
<template> <template>
<div <LoadingBanner
v-if="!isRefreshed" v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center" class="mt-12"
> />
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
<PullRefresh <PullRefresh
v-model="loading" v-model="loading"
@refresh="onRefresh" @refresh="onRefresh"
@@ -102,8 +99,19 @@ const filteredDataList = computed(() => {
appear appear
@click="subscribeEditDialog = true" @click="subscribeEditDialog = true"
/> />
<VFab
icon="mdi-history"
color="info"
location="bottom end"
class="mb-2"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->
<SubscribeEditForm <SubscribeEditDialog
v-if="subscribeEditDialog" v-if="subscribeEditDialog"
v-model="subscribeEditDialog" v-model="subscribeEditDialog"
:default="true" :default="true"
@@ -111,6 +119,14 @@ const filteredDataList = computed(() => {
@save="subscribeEditDialog = false" @save="subscribeEditDialog = false"
@close="subscribeEditDialog = false" @close="subscribeEditDialog = false"
/> />
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"
v-model="historyDialog"
:type="props.type"
@close="historyDialog = false"
@save="() => {historyDialog = false; fetchData()}"
/>
</template> </template>
<style lang="scss"> <style lang="scss">
+18 -19
View File
@@ -55,38 +55,39 @@ async function loadMessages({ done }: { done: any }) {
done('ok') done('ok')
return return
} }
// 设置加载中
loading.value = true
try { try {
// 设置加载中
loading.value = true
currData.value = await api.get('message/web', { currData.value = await api.get('message/web', {
params: { params: {
page: page.value, page: page.value,
size: 20, size: 20,
}, },
}) })
// 已加载过
isLoaded.value = true
if (currData.value.length > 0) { if (currData.value.length > 0) {
// 取最后一条时间为存量消息最新时间 // 取最后一条时间为存量消息最新时间
lastTime.value = currData.value[currData.value.length - 1].reg_time ?? '' lastTime.value = currData.value[currData.value.length - 1].reg_time ?? ''
// 合并数据 // 合并数据
messages.value = [...currData.value, ...messages.value] messages.value = [...currData.value, ...messages.value]
// 加载完成
done('ok')
if (page.value === 1) { if (page.value === 1) {
// 滚动到底部 // 滚动到底部
emit('scroll') emit('scroll')
// 监听SSE消息
startSSEMessager()
} }
// 页码+1 // 页码+1
page.value++ page.value++
// 完成
done('ok')
} }
else { else {
done('ok') // 没有新数据
// 监听SSE消息 done('empty')
startSSEMessager()
} }
// 取消加载中
loading.value = false loading.value = false
isLoaded.value = true // 监听SSE消息
startSSEMessager()
} }
catch (error) { catch (error) {
console.error(error) console.error(error)
@@ -99,7 +100,7 @@ function compareTime(time1: string, time2: string) {
return -1 return -1
if (!time2) if (!time2)
return 1 return 1
return new Date(time1).getTime() - new Date(time2).getTime() return new Date(time1.replaceAll(/-/g, '/')).getTime() - new Date(time2.replaceAll(/-/g, '/')).getTime()
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -110,20 +111,18 @@ onBeforeUnmount(() => {
<template> <template>
<VInfiniteScroll <VInfiniteScroll
mode="intersect" :mode="!isLoaded ? 'intersect' : 'manual'"
side="start" side="start"
:items="messages" :items="messages"
class="overflow-hidden" class="overflow-hidden"
@load="loadMessages" @load="loadMessages"
load-more-text="加载更多 ..."
> >
<template #loading> <template #loading>
<VProgressCircular <LoadingBanner />
v-if="loading" </template>
indeterminate <template #empty>
size="48" 没有更多数据
class="mb-5"
color="primary"
/>
</template> </template>
<div> <div>
<VRow <VRow
+7
View File
@@ -3057,6 +3057,11 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0" es-errors "^1.3.0"
is-data-view "^1.0.1" is-data-view "^1.0.1"
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
de-indent@^1.0.2: de-indent@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -6482,6 +6487,7 @@ stop-iteration-iterator@^1.0.0:
internal-slot "^1.0.4" internal-slot "^1.0.4"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
name string-width-cjs
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6555,6 +6561,7 @@ stringify-object@^3.3.0:
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==