mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8f369ba0 | ||
|
|
824e2d72c7 | ||
|
|
143aa79797 | ||
|
|
0e1120f407 | ||
|
|
c8dbb9672a | ||
|
|
372b74776f | ||
|
|
abcc3c6411 | ||
|
|
ec4ab8762c | ||
|
|
bc93de8ff2 | ||
|
|
b426f3c6f2 | ||
|
|
99d9bb29ce | ||
|
|
5e109c666b | ||
|
|
0bed216735 | ||
|
|
d55bb8d336 | ||
|
|
7c32b3edf0 | ||
|
|
121cb7e442 | ||
|
|
dec3e1ea92 | ||
|
|
664b6610f3 | ||
|
|
44163f0fb2 | ||
|
|
d43865fcad | ||
|
|
fed92f3853 | ||
|
|
823d2a816e | ||
|
|
046c21edf6 | ||
|
|
8236d80b42 | ||
|
|
90e7eb1c79 | ||
|
|
ef09868af1 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.7.1-1",
|
||||
"version": "1.7.4-1",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -109,4 +109,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,26 @@ http {
|
||||
# 超时设置
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location /cookiecloud {
|
||||
# 后端cookiecloud地址
|
||||
proxy_pass http://backend_api;
|
||||
rewrite ^.+mock-server/?(.*)$ /$1 break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Nginx-Proxy true;
|
||||
|
||||
# 超时设置
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
upstream backend_api {
|
||||
|
||||
@@ -25,6 +25,16 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
// 配置代理中间件将CookieCloud请求转发给后端API
|
||||
app.use(
|
||||
'/cookiecloud',
|
||||
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
|
||||
// 路径加上 /cookiecloud 前缀
|
||||
proxyReqPathResolver: (req) => {
|
||||
return `/cookiecloud${req.url}`
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 处理根路径的请求
|
||||
app.get('/', (req, res) => {
|
||||
|
||||
@@ -147,3 +147,23 @@ export function formatEp(nums: number[]): string {
|
||||
|
||||
return formattedRanges.join('、')
|
||||
}
|
||||
|
||||
// 将yyyy-mm-dd hh:mm:ss转换为时间差,如:1小时前,1天前
|
||||
export function formatDateDifference(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
const currentDate = new Date()
|
||||
const timeDifference = currentDate.getTime() - date.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
const minutesDifference = Math.floor(secondsDifference / 60)
|
||||
const hoursDifference = Math.floor(minutesDifference / 60)
|
||||
const daysDifference = Math.floor(hoursDifference / 24)
|
||||
|
||||
if (daysDifference > 0)
|
||||
return `${daysDifference}天前`
|
||||
else if (hoursDifference > 0)
|
||||
return `${hoursDifference}小时前`
|
||||
else if (minutesDifference > 0)
|
||||
return `${minutesDifference}分钟前`
|
||||
else
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
133
src/api/types.ts
133
src/api/types.ts
@@ -81,7 +81,7 @@ export interface Subscribe {
|
||||
best_version: any
|
||||
|
||||
// 使用 imdbid 搜索
|
||||
search_imdbid?: boolean
|
||||
search_imdbid?: any
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
@@ -181,6 +181,9 @@ export interface MediaInfo {
|
||||
// 豆瓣ID
|
||||
douban_id?: string
|
||||
|
||||
// Bangumi ID
|
||||
bangumi_id?: string
|
||||
|
||||
// 媒体原语种
|
||||
original_language?: string
|
||||
|
||||
@@ -282,6 +285,9 @@ export interface MediaInfo {
|
||||
|
||||
// 下一集
|
||||
next_episode_to_air?: object
|
||||
|
||||
// 别名
|
||||
names?: string[]
|
||||
}
|
||||
|
||||
// TMDB季信息
|
||||
@@ -422,6 +428,28 @@ export interface DoubanPerson {
|
||||
|
||||
}
|
||||
|
||||
// Bangumi人物信息
|
||||
export interface BangumiPerson {
|
||||
// ID
|
||||
id?: number
|
||||
|
||||
// 名称
|
||||
name?: string
|
||||
|
||||
// 类型
|
||||
type?: number
|
||||
|
||||
// 角色
|
||||
career?: string[]
|
||||
|
||||
// images large/normal
|
||||
images?: { [key: string]: string }
|
||||
|
||||
// 关系
|
||||
relation?: string
|
||||
|
||||
}
|
||||
|
||||
// 站点
|
||||
export interface Site {
|
||||
|
||||
@@ -802,18 +830,34 @@ export interface Context {
|
||||
|
||||
// 用户信息
|
||||
export interface User {
|
||||
// 用户ID
|
||||
id: number
|
||||
|
||||
// 用户名称
|
||||
name: string
|
||||
|
||||
// 用户密码
|
||||
password: string
|
||||
|
||||
// 用户邮箱
|
||||
email: string
|
||||
|
||||
// 是否激活
|
||||
is_active: boolean
|
||||
|
||||
// 是否管理员
|
||||
is_superuser: boolean
|
||||
|
||||
// 头像
|
||||
avatar: string
|
||||
}
|
||||
|
||||
// 存储空间
|
||||
export interface Storage {
|
||||
// 总空间
|
||||
total_storage: number
|
||||
|
||||
// 已使用空间
|
||||
used_storage: number
|
||||
}
|
||||
|
||||
@@ -918,45 +962,132 @@ export interface Setting {
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
// 文件列表
|
||||
list: any
|
||||
|
||||
// 创建目录
|
||||
mkdir: any
|
||||
|
||||
// 删除文件
|
||||
delete: any
|
||||
|
||||
// 下载文件
|
||||
download: any
|
||||
|
||||
// 图片预览
|
||||
image: any
|
||||
|
||||
// 重命名
|
||||
rename: any
|
||||
}
|
||||
|
||||
// 文件浏览项目
|
||||
export interface FileItem {
|
||||
// 类型
|
||||
type: string
|
||||
|
||||
// 文件名
|
||||
name: string
|
||||
|
||||
// 文件名不含扩展名
|
||||
basename: string
|
||||
|
||||
// 文件路径
|
||||
path: string
|
||||
|
||||
// 文件扩展名
|
||||
extension: string
|
||||
|
||||
// 文件大小
|
||||
size: number
|
||||
|
||||
// 文件子元素
|
||||
children: FileItem[]
|
||||
|
||||
// 文件创建时间
|
||||
modify_time: number
|
||||
}
|
||||
|
||||
// 媒体服务器播放条目
|
||||
export interface MediaServerPlayItem {
|
||||
// ID
|
||||
id?: string | number
|
||||
|
||||
// 标题
|
||||
title: string
|
||||
|
||||
// 副标题
|
||||
subtitle?: string
|
||||
|
||||
// 类型
|
||||
type?: string
|
||||
|
||||
// 海报
|
||||
image?: string
|
||||
|
||||
// 链接
|
||||
link?: string
|
||||
|
||||
// 播放百分比
|
||||
percent?: number
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
export interface MediaServerLibrary {
|
||||
// 服务器名称
|
||||
server: string
|
||||
|
||||
// ID
|
||||
id?: string | number
|
||||
|
||||
// 名称
|
||||
name: string
|
||||
|
||||
// 路径
|
||||
path?: string
|
||||
|
||||
// 类型
|
||||
type?: string
|
||||
|
||||
// 图片
|
||||
image?: string
|
||||
|
||||
// 图片列表
|
||||
image_list?: string[]
|
||||
|
||||
// 链接
|
||||
link?: string
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
export interface Message {
|
||||
// 消息类型
|
||||
mtype?: string
|
||||
|
||||
// 消息标题
|
||||
title?: string
|
||||
|
||||
// 消息内容
|
||||
text?: string
|
||||
|
||||
// 消息链接
|
||||
link?: string
|
||||
|
||||
// 消息图片
|
||||
image?: string
|
||||
|
||||
// 消息时间
|
||||
date?: string
|
||||
|
||||
// 登记时间
|
||||
reg_time?: string
|
||||
|
||||
// 用户ID
|
||||
userid?: string
|
||||
|
||||
// 消息方向:0-接收,1-发送
|
||||
action?: number
|
||||
|
||||
// JSON
|
||||
note?: string
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import axios from 'axios'
|
||||
import List from './filebrowser/List.vue'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
@@ -70,12 +69,10 @@ const storagesArray = computed(() => {
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading) {
|
||||
if (loading)
|
||||
loading++
|
||||
}
|
||||
else if (loading > 0) {
|
||||
else if (loading > 0)
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
@@ -115,20 +112,6 @@ onMounted(() => {
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="path"
|
||||
|
||||
95
src/components/cards/BangumiPersonCard.vue
Normal file
95
src/components/cards/BangumiPersonCard.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import personIcon from '@images/misc/person-icon.png'
|
||||
import type { BangumiPerson } from '@/api/types'
|
||||
|
||||
const personProps = defineProps({
|
||||
person: Object as PropType<BangumiPerson>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 当前人物
|
||||
const personInfo = ref(personProps.person)
|
||||
|
||||
// 人物图片是否加载
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 人物图片地址
|
||||
function getPersonImage() {
|
||||
if (!personInfo.value?.images)
|
||||
return personIcon
|
||||
return personInfo.value?.images?.medium
|
||||
}
|
||||
|
||||
// 使用、拼装人物角色
|
||||
function getPersonCharacter() {
|
||||
if (!personInfo.value?.career)
|
||||
return ''
|
||||
return personInfo.value?.career.join('、')
|
||||
}
|
||||
|
||||
// 打开人物详情
|
||||
function goPersonDetail() {
|
||||
if (!personInfo.value?.id)
|
||||
return
|
||||
window.open(`https://bangumi.tv/person/${personInfo.value?.id}`, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="personProps">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="personProps.height"
|
||||
:width="personProps.width"
|
||||
class="rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105': hover.isHovering,
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div style="padding-bottom: 150%;">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
<VAvatar
|
||||
size="120"
|
||||
:class="{
|
||||
'ring-1 ring-gray-700': isImageLoaded,
|
||||
}"
|
||||
>
|
||||
<VImg
|
||||
v-img
|
||||
:src="getPersonImage()"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<div class="w-full truncate text-center font-bold">
|
||||
{{ personInfo?.name }}
|
||||
</div>
|
||||
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
|
||||
{{ getPersonCharacter() }}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.person-card {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
|
||||
}
|
||||
|
||||
.person-card:hover {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
|
||||
}
|
||||
</style>
|
||||
@@ -25,8 +25,8 @@ const isDownloading = ref(props.info?.state === 'downloading')
|
||||
|
||||
// 监听props.info?.state的变化
|
||||
watch(() => props.info?.state, (newValue) => {
|
||||
isDownloading.value = newValue === 'downloading';
|
||||
});
|
||||
isDownloading.value = newValue === 'downloading'
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
@@ -16,11 +16,6 @@ const props = defineProps({
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -57,6 +52,15 @@ const seasonInfos = ref<TmdbSeason[]>([])
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<TmdbSeason[]>([])
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
return props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: props.media?.douban_id
|
||||
? `douban:${props.media?.douban_id}`
|
||||
: `bangumi:${props.media?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 订阅弹窗选择的多季
|
||||
function subscribeSeasons() {
|
||||
subscribeSeasonDialog.value = false
|
||||
@@ -131,6 +135,7 @@ async function addSubscribe(season = 0) {
|
||||
year: props.media?.year,
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
doubanid: props.media?.douban_id,
|
||||
bangumiid: props.media?.bangumi_id,
|
||||
season,
|
||||
best_version,
|
||||
})
|
||||
@@ -151,9 +156,12 @@ async function addSubscribe(season = 0) {
|
||||
)
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
if (result.success && seasonsSelected.value.length <= 1) {
|
||||
const show_edit_dialog = await querySubscribeRules()
|
||||
if (show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -186,9 +194,7 @@ async function removeSubscribe() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
@@ -249,9 +255,7 @@ async function handleCheckExists() {
|
||||
// 调用API检查是否已订阅,电视剧需要指定季
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
@@ -314,11 +318,12 @@ async function querySubscribeRules() {
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
subscribeRules.value = result.data?.value
|
||||
return result.data.value.show_edit_dialog
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
@@ -362,11 +367,7 @@ function goMediaDetail() {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${
|
||||
props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
}`,
|
||||
mediaid: getMediaId(),
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
@@ -377,11 +378,7 @@ function handleSearch() {
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: `${
|
||||
props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
}`,
|
||||
keyword: getMediaId(),
|
||||
type: props.media?.type,
|
||||
area: 'title',
|
||||
},
|
||||
@@ -392,7 +389,6 @@ function handleSearch() {
|
||||
onBeforeMount(() => {
|
||||
handleCheckSubscribe()
|
||||
handleCheckExists()
|
||||
querySubscribeRules()
|
||||
})
|
||||
|
||||
// 计算图片地址
|
||||
|
||||
112
src/components/cards/MessageCard.vue
Normal file
112
src/components/cards/MessageCard.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Message } from '@/api/types'
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
message: Object as PropType<Message>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
}
|
||||
|
||||
// 链接打开新窗口
|
||||
function openLink() {
|
||||
if (props.message?.link)
|
||||
window.open(props.message.link, '_blank')
|
||||
}
|
||||
|
||||
// 将note转换为json
|
||||
function noteToJson() {
|
||||
if (props.message?.note) {
|
||||
try {
|
||||
return JSON.parse(props.message.note)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 将\n转换为html属性的换行符
|
||||
function replaceNewLine(value: string) {
|
||||
if (!value)
|
||||
return ''
|
||||
return value.replace(/\n/g, '<br/>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
variant="tonal"
|
||||
@click="openLink"
|
||||
>
|
||||
<div
|
||||
v-if="props.message?.image"
|
||||
class="relative text-center card-cover-blurred"
|
||||
>
|
||||
<VImg
|
||||
:src="props.message?.image"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</div>
|
||||
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
|
||||
{{ props.message?.title }}
|
||||
</VCardTitle>
|
||||
<VAlert
|
||||
v-if="props.message?.text && props.message?.action === 0"
|
||||
variant="tonal"
|
||||
type="success"
|
||||
>
|
||||
<template #prepend />
|
||||
{{ props.message?.text }}
|
||||
</VAlert>
|
||||
<VCardText
|
||||
v-if="props.message?.text && props.message?.action === 1"
|
||||
v-html="replaceNewLine(props.message?.text)"
|
||||
/>
|
||||
<VCardText v-if="props.message?.note">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(value, key) in noteToJson()"
|
||||
:key="key"
|
||||
two-line
|
||||
>
|
||||
<VListItemTitle v-if="value.title_year" class="font-bold">
|
||||
{{ key + 1 }}. {{ value.title_year }}
|
||||
</VListItemTitle>
|
||||
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
|
||||
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} ↑{{ value.seeders }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle v-if="value.type">
|
||||
类型:{{ value.type }} 评分:{{ value.vote_average }}
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle v-if="value.enclosure" class="whitespace-break-spaces">
|
||||
{{ value.description }}
|
||||
</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<div class="text-end">
|
||||
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
|
||||
<span class="text-sm italic me-2">{{ formatDateDifference(props.message?.reg_time || props.message?.date || '') }}</span>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -49,7 +49,7 @@ async function installPlugin() {
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} v${props?.plugin?.plugin_version} ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
@@ -163,15 +163,6 @@ const dropdownItems = ref([
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
>
|
||||
@@ -186,20 +177,22 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<VCardTitle>
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="pb-2">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
作者:<a
|
||||
<VCardText class="flex items-center justify-start pb-2">
|
||||
<VIcon icon="mdi-account" class="me-1" />
|
||||
<a
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a><br>
|
||||
版本:{{ props.plugin?.plugin_version }}
|
||||
</a>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 安装插件进度框 -->
|
||||
|
||||
@@ -41,12 +41,18 @@ const pluginConfigDialog = ref(false)
|
||||
// 插件配置表单数据
|
||||
const pluginConfigForm = ref({})
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在更新插件...')
|
||||
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
@@ -83,7 +89,12 @@ async function uninstallPlugin() {
|
||||
return
|
||||
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
|
||||
|
||||
@@ -221,6 +232,41 @@ async function resetPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新插件
|
||||
async function updatePlugin() {
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在更新 ${props.plugin?.plugin_name} ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
{
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
force: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 更新成功!`)
|
||||
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 更新失败:${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
@@ -254,8 +300,18 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
title: '更新',
|
||||
value: 3,
|
||||
show: props.plugin?.has_update,
|
||||
props: {
|
||||
prependIcon: 'mdi-arrow-up-circle-outline',
|
||||
color: 'success',
|
||||
click: updatePlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cancel',
|
||||
@@ -265,7 +321,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 4,
|
||||
value: 5,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
@@ -275,7 +331,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '查看日志',
|
||||
value: 5,
|
||||
value: 6,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-file-document-outline',
|
||||
@@ -286,7 +342,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 5,
|
||||
value: 7,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
@@ -294,6 +350,13 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// 监听插件状态变化
|
||||
watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
|
||||
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
|
||||
if (updateItemIndex !== -1)
|
||||
dropdownItems.value[updateItemIndex].show = newHasUpdate
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -313,6 +376,15 @@ const dropdownItems = ref([
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
@@ -355,7 +427,7 @@ const dropdownItems = ref([
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle class="flex items-center flex-row">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span>
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
@@ -430,6 +502,25 @@ const dropdownItems = ref([
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 更新插件进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, TorrentInfo } from '@/api/types'
|
||||
import type { Site } from '@/api/types'
|
||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||
|
||||
// 输入参数
|
||||
@@ -51,31 +51,6 @@ const progressDialog = ref(false)
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 资源浏览表头
|
||||
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 },
|
||||
]
|
||||
|
||||
// 数据列表
|
||||
const resourceDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 搜索
|
||||
const resourceSearch = ref('')
|
||||
|
||||
// 加载状态
|
||||
const resourceLoading = ref(false)
|
||||
|
||||
// 总条数
|
||||
const resourceTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 用户名密码表单
|
||||
const userPwForm = ref({
|
||||
username: '',
|
||||
@@ -83,16 +58,6 @@ const userPwForm = ref({
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile(enclosure: string) {
|
||||
window.open(enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
@@ -131,7 +96,6 @@ async function handleSiteUpdate() {
|
||||
// 打开资源浏览弹窗
|
||||
async function handleResourceBrowse() {
|
||||
resourceDialog.value = true
|
||||
getResourceList()
|
||||
}
|
||||
|
||||
// 调用API,更新站点Cookie UA
|
||||
@@ -171,30 +135,6 @@ async function updateSiteCookie() {
|
||||
}
|
||||
}
|
||||
|
||||
// 促销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'
|
||||
}
|
||||
|
||||
// 调用API,查询站点资源
|
||||
async function getResourceList() {
|
||||
resourceLoading.value = true
|
||||
try {
|
||||
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开站点页面
|
||||
function openSitePage() {
|
||||
window.open(cardProps.site?.url, '_blank')
|
||||
@@ -397,127 +337,13 @@ onMounted(() => {
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<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
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="text-high-emphasis pt-1">
|
||||
{{ item.raw.title }}
|
||||
</div>
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.raw?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.downloadvolumefactor !== 1 || item.raw?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(item.raw?.downloadvolumefactor, item.raw?.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.volume_factor }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div>{{ item.raw.date_elapsed }}</div>
|
||||
<div class="text-sm">
|
||||
{{ item.raw.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.raw.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.seeders="{ item }">
|
||||
<div>{{ item.raw.seeders }}</div>
|
||||
</template>
|
||||
<template #item.peers="{ item }">
|
||||
<div>{{ item.raw.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.raw.page_url)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.raw.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 输入参数
|
||||
const inProps = defineProps({
|
||||
@@ -73,6 +74,9 @@ const nameTestResult = ref<Context>()
|
||||
// 识别结果对话框
|
||||
const nameTestDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() =>
|
||||
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
|
||||
@@ -111,6 +115,7 @@ async function load() {
|
||||
}
|
||||
// 加载数据
|
||||
items.value = await axiosInstance.value.request(config) ?? []
|
||||
defer = useDefer(items.value.length)
|
||||
emit('loading', false)
|
||||
loading.value = false
|
||||
}
|
||||
@@ -382,6 +387,7 @@ onMounted(() => {
|
||||
>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-if="defer(index)"
|
||||
v-bind="hover.props"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
@@ -466,6 +472,7 @@ onMounted(() => {
|
||||
>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-if="defer(index)"
|
||||
v-bind="hover.props"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
icons: Object,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
refreshpending: Boolean,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged', 'loading', 'refreshed'])
|
||||
|
||||
// 变量
|
||||
const open = ref<string[]>([])
|
||||
// 活跃的文件夹
|
||||
const active = ref<string[]>([])
|
||||
// 内容
|
||||
const items = ref<FileItem[]>([])
|
||||
// 过滤
|
||||
const filter = ref('')
|
||||
|
||||
// 方法
|
||||
function init() {
|
||||
open.value = []
|
||||
items.value = [{
|
||||
type: 'dir',
|
||||
path: '/',
|
||||
basename: 'root',
|
||||
extension: '',
|
||||
name: 'root',
|
||||
children: [],
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
}]
|
||||
}
|
||||
|
||||
// 调用API读取文件夹
|
||||
async function readFolder(item: FileItem) {
|
||||
emit('loading', true)
|
||||
const url = props.endpoints?.list.url
|
||||
.replace(/{storage}/g, props.storage)
|
||||
.replace(/{path}/g, item.path)
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: props.endpoints?.list.method || 'get',
|
||||
}
|
||||
|
||||
const response: FileItem[] = await props.axios?.request(config) ?? []
|
||||
|
||||
item.children = response.map((item: FileItem) => {
|
||||
if (item.type === 'dir')
|
||||
item.children = []
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
emit('loading', false)
|
||||
}
|
||||
|
||||
// 选中变化
|
||||
function activeChanged(_active: string[]) {
|
||||
let path = ''
|
||||
if (active.value.length)
|
||||
path = active.value[0]
|
||||
|
||||
if (props.path !== path)
|
||||
emit('pathchanged', path)
|
||||
}
|
||||
|
||||
// 查找文件
|
||||
function findItem(path: string) {
|
||||
const stack: FileItem[] = []
|
||||
stack.push(items.value[0])
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()
|
||||
if (node?.path === path) {
|
||||
return node
|
||||
}
|
||||
else if (node?.children && node.children.length) {
|
||||
for (const element of node.children)
|
||||
stack.push(element)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 监听存储空间变量
|
||||
watch(() => props.storage, () => {
|
||||
init()
|
||||
})
|
||||
|
||||
// 监听路径变化
|
||||
watch(
|
||||
() => props.path,
|
||||
() => {
|
||||
if (props.path) {
|
||||
active.value = [props.path]
|
||||
if (!open.value.includes(props.path))
|
||||
open.value.push(props.path)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 refreshPending
|
||||
watch(
|
||||
() => props.refreshpending,
|
||||
async () => {
|
||||
if (props.refreshpending && props.path) {
|
||||
const item = findItem(props.path)
|
||||
if (item) {
|
||||
await readFolder(item)
|
||||
emit('refreshed')
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard flat width="250" min-height="500" class="d-flex flex-column folders-tree-card">
|
||||
<div class="grow scroll-x">
|
||||
<VTreeview
|
||||
:open="open"
|
||||
:active="active"
|
||||
:items="items"
|
||||
:search="filter"
|
||||
:load-children="readFolder"
|
||||
item-key="path"
|
||||
item-text="basename"
|
||||
dense
|
||||
activatable
|
||||
transition
|
||||
class="folders-tree"
|
||||
@update:active="activeChanged"
|
||||
>
|
||||
<template #prepend="{ item, open }">
|
||||
<VIcon
|
||||
v-if="item.type === 'dir'"
|
||||
>
|
||||
{{ open ? 'mdi-folder-open-outline' : 'mdi-folder-outline' }}
|
||||
</VIcon>
|
||||
<VIcon v-else-if="props.icons" :icon="props.icons[item.extension.toLowerCase()] || props.icons.other" />
|
||||
</template>
|
||||
<template #label="{ item }">
|
||||
{{ item.basename }}
|
||||
<VBtn
|
||||
v-if="item.type === 'dir'"
|
||||
icon
|
||||
class="ml-1"
|
||||
@click.stop="readFolder(item)"
|
||||
>
|
||||
<VIcon class="pa-0 mdi-18px" color="grey lighten-1">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTreeview>
|
||||
</div>
|
||||
<VDivider />
|
||||
<VToolbar
|
||||
density="compact"
|
||||
>
|
||||
<VBtn icon @click="init">
|
||||
<VIcon icon="mdi-collapse-all-outline" />
|
||||
</VBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folders-tree-card {
|
||||
height: 100%;
|
||||
|
||||
.scroll-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
::v-deep .folders-tree {
|
||||
width: fit-content;
|
||||
min-width: 250px;
|
||||
|
||||
.v-treeview-node {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
total_episode: 0,
|
||||
start_episode: 0,
|
||||
best_version: 0,
|
||||
search_imdbid: false,
|
||||
search_imdbid: 0,
|
||||
sites: [],
|
||||
type: '',
|
||||
name: '',
|
||||
@@ -100,6 +100,7 @@ async function getSubscribeInfo() {
|
||||
)
|
||||
subscribeForm.value = result
|
||||
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
||||
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
|
||||
244
src/components/table/SiteTorrentTable.vue
Normal file
244
src/components/table/SiteTorrentTable.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import api from '@/api'
|
||||
import type { TorrentInfo } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
site: Number,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 数据列表
|
||||
const resourceDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 搜索
|
||||
const resourceSearch = ref('')
|
||||
|
||||
// 总条数
|
||||
const resourceTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 加载状态
|
||||
const resourceLoading = ref(false)
|
||||
|
||||
// 资源浏览表头
|
||||
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) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_torrent.site_name}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/add', _torrent)
|
||||
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败:${result.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
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} 条"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<a href="javascript:void(0)" @click.stop="addDownload(item.raw)">
|
||||
<div class="text-high-emphasis pt-1">
|
||||
{{ item.raw.title }}
|
||||
</div>
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.raw?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.downloadvolumefactor !== 1 || item.raw?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(item.raw?.downloadvolumefactor, item.raw?.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.volume_factor }}
|
||||
</VChip>
|
||||
</a>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div>{{ item.raw.date_elapsed }}</div>
|
||||
<div class="text-sm">
|
||||
{{ item.raw.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.raw.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.seeders="{ item }">
|
||||
<div>{{ item.raw.seeders }}</div>
|
||||
</template>
|
||||
<template #item.peers="{ item }">
|
||||
<div>{{ item.raw.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.raw.page_url)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.raw.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
</template>
|
||||
@@ -44,7 +44,7 @@ const superUser = store.state.auth.superUser
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Shortcuts -->
|
||||
<ShortcutBar />
|
||||
<ShortcutBar v-if="superUser" />
|
||||
|
||||
<!-- 👉 Theme -->
|
||||
<NavbarThemeSwitcher class="me-2" />
|
||||
|
||||
@@ -4,7 +4,9 @@ import NetTestView from '@/views/system/NetTestView.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||
import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
||||
import MessageView from '@/views/system/MessageView.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false)
|
||||
@@ -24,11 +26,53 @@ const ruleTestDialog = ref(false)
|
||||
// 系统健康检查弹窗
|
||||
const systemTestDialog = ref(false)
|
||||
|
||||
// 消息中心弹窗
|
||||
const messageDialog = ref(false)
|
||||
|
||||
// 输入消息
|
||||
const user_message = ref('')
|
||||
|
||||
// 发送按钮是否可用
|
||||
const sendButtonDisabled = ref(false)
|
||||
|
||||
// 聊天容器
|
||||
const chatContainer = ref<HTMLDivElement>()
|
||||
|
||||
// 滚动到底部
|
||||
function scrollMessageToEnd() {
|
||||
nextTick(() => {
|
||||
if (chatContainer.value) {
|
||||
const scrollDiv = chatContainer.value.$el
|
||||
scrollDiv.scrollTop = scrollDiv.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拼接全部日志url
|
||||
function allLoggingUrl() {
|
||||
const token = store.state.auth.token
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
if (user_message.value) {
|
||||
try {
|
||||
sendButtonDisabled.value = true
|
||||
await api.post(`message/web?text=${user_message.value}`)
|
||||
user_message.value = ''
|
||||
sendButtonDisabled.value = false
|
||||
scrollMessageToEnd()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollMessageToEnd()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -124,7 +168,7 @@ function allLoggingUrl() {
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
日志
|
||||
</h6>
|
||||
<span class="text-sm">查看实时日志</span>
|
||||
<span class="text-sm">实时日志</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -145,7 +189,7 @@ function allLoggingUrl() {
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
网络
|
||||
</h6>
|
||||
<span class="text-sm">测试网速连通性</span>
|
||||
<span class="text-sm">网速连通性测试</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -168,7 +212,28 @@ function allLoggingUrl() {
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
系统
|
||||
</h6>
|
||||
<span class="text-sm">系统健康检查</span>
|
||||
<span class="text-sm">健康检查</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="messageDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-message-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
消息
|
||||
</h6>
|
||||
<span class="text-sm">消息中心</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -249,4 +314,41 @@ function allLoggingUrl() {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 消息中心弹窗 -->
|
||||
<VDialog
|
||||
v-model="messageDialog"
|
||||
max-width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="消息中心">
|
||||
<DialogCloseBtn @click="messageDialog = false" />
|
||||
<VCardText ref="chatContainer">
|
||||
<MessageView @scroll="scrollMessageToEnd" />
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<VTextField
|
||||
v-model="user_message"
|
||||
placeholder="输入消息或命令"
|
||||
outlined
|
||||
hide-details
|
||||
single-line
|
||||
clearable
|
||||
density="compact"
|
||||
:disabled="sendButtonDisabled"
|
||||
@keydown.enter="sendMessage"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:disabled="sendButtonDisabled"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
title="正在热映"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="bangumi/calendar"
|
||||
linkurl="/browse/bangumi/calendar?title=Bangumi每日放送"
|
||||
title="Bangumi每日放送"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="tmdb/movies"
|
||||
linkurl="/browse/tmdb/movies?title=热门电影"
|
||||
|
||||
@@ -75,7 +75,7 @@ async function fetchData() {
|
||||
else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
|
||||
dataList.value = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
|
||||
@@ -44,7 +44,7 @@ onMounted(fetchData)
|
||||
<template #content>
|
||||
<template
|
||||
v-for="data in dataList"
|
||||
:key="data.tmdb_id || data.douban_id"
|
||||
:key="data.tmdb_id || data.douban_id || data.bangumi_id"
|
||||
>
|
||||
<MediaCard
|
||||
:media="data"
|
||||
|
||||
@@ -51,6 +51,15 @@ const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
return mediaDetail.value?.tmdb_id
|
||||
? `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
: mediaDetail.value?.douban_id
|
||||
? `douban:${mediaDetail.value?.douban_id}`
|
||||
: `bangumi:${mediaDetail.value?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 调用API查询详情
|
||||
async function getMediaDetail() {
|
||||
if (mediaProps.mediaid && mediaProps.type) {
|
||||
@@ -60,7 +69,7 @@ async function getMediaDetail() {
|
||||
},
|
||||
})
|
||||
isRefreshed.value = true
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id)
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id)
|
||||
return
|
||||
|
||||
// 检查存在状态
|
||||
@@ -113,7 +122,7 @@ async function checkExists() {
|
||||
// 查询当前媒体是否已订阅
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
@@ -198,6 +207,7 @@ async function addSubscribe(season = 0) {
|
||||
year: mediaDetail.value?.year,
|
||||
tmdbid: mediaDetail.value?.tmdb_id,
|
||||
doubanid: mediaDetail.value?.douban_id,
|
||||
bangumiid: mediaDetail.value?.bangumi_id,
|
||||
season,
|
||||
best_version,
|
||||
})
|
||||
@@ -253,9 +263,7 @@ async function removeSubscribe(season: number) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = mediaDetail.value?.tmdb_id
|
||||
? `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
: `douban:${mediaDetail.value?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
@@ -330,6 +338,11 @@ function getTvdbLink() {
|
||||
return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}`
|
||||
}
|
||||
|
||||
// 拼装Bangumi地址
|
||||
function getBangumiLink() {
|
||||
return `https://bgm.tv/subject/${mediaDetail.value.bangumi_id}`
|
||||
}
|
||||
|
||||
// 拼装集图片地址
|
||||
function getEpisodeImage(stillPath: string) {
|
||||
if (!stillPath)
|
||||
@@ -405,7 +418,7 @@ function joinArray(arr: string[]) {
|
||||
|
||||
// 开始搜索
|
||||
function handleSearch(area: string) {
|
||||
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
const keyword = getMediaId()
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
@@ -453,7 +466,7 @@ onBeforeMount(() => {
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_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">
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
|
||||
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
|
||||
@@ -492,7 +505,7 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" variant="tonal" color="info" class="mb-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
@@ -518,7 +531,7 @@ onBeforeMount(() => {
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
@@ -580,6 +593,12 @@ onBeforeMount(() => {
|
||||
<span class="ms-1">TheTvDb</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">Bangumi</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
|
||||
季
|
||||
@@ -740,6 +759,33 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.bangumi_id" class="media-fact">
|
||||
<span>ID</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.bangumi_id }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.original_title" class="media-fact">
|
||||
<span>原始标题</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.release_date" class="media-fact border-b-0">
|
||||
<span>上映日期</span>
|
||||
<span class="media-fact-value">
|
||||
{{ mediaDetail.release_date }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<PersonCardSlideView
|
||||
@@ -757,6 +803,14 @@ onBeforeMount(() => {
|
||||
type="douban"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id">
|
||||
<PersonCardSlideView
|
||||
:apipath="`bangumi/credits/${mediaDetail.bangumi_id}`"
|
||||
:linkurl="`/credits/bangumi/credits/${mediaDetail.bangumi_id}?title=演员阵容&type=bangumi`"
|
||||
title="演员阵容"
|
||||
type="bangumi"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
@@ -771,6 +825,13 @@ onBeforeMount(() => {
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`bangumi/recommend/${mediaDetail.bangumi_id}`"
|
||||
:linkurl="`/browse/bangumi/recommend/${mediaDetail.bangumi_id}?title=推荐`"
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
@@ -781,7 +842,7 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
|
||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && !mediaDetail.bangumi_id && isRefreshed"
|
||||
error-code="500"
|
||||
error-title="出错啦!"
|
||||
error-description="未识别到媒体信息。"
|
||||
|
||||
@@ -3,6 +3,7 @@ import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||
import api from '@/api'
|
||||
import SlideView from '@/components/slide/SlideView.vue'
|
||||
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -59,6 +60,12 @@ onMounted(fetchData)
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
<BangumiPersonCard
|
||||
v-if="props.type === 'bangumi'"
|
||||
:person="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</SlideView>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
@@ -20,6 +21,9 @@ const isAppMarketLoaded = ref(false)
|
||||
// APP市场窗口
|
||||
const PluginAppDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 关闭插件市场窗口
|
||||
function pluginDialogClose() {
|
||||
PluginAppDialog.value = false
|
||||
@@ -55,17 +59,39 @@ async function fetchUninstalledPlugins() {
|
||||
state: 'market',
|
||||
},
|
||||
})
|
||||
// 设置APP市场加载完成
|
||||
isAppMarketLoaded.value = true
|
||||
// 设置更新状态
|
||||
for (const uninstalled of uninstalledList.value) {
|
||||
for (const data of dataList.value) {
|
||||
if (uninstalled.id === data.id) {
|
||||
data.has_update = true
|
||||
data.repo_url = uninstalled.repo_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
// 加载所有数据
|
||||
function refreshData() {
|
||||
fetchInstalledPlugins()
|
||||
fetchUninstalledPlugins()
|
||||
}
|
||||
|
||||
// 获取没有更新的插件
|
||||
const getUnupdatedPlugins = computed(() => {
|
||||
const list = uninstalledList.value.filter(item => !item.has_update)
|
||||
defer = useDefer(list.length)
|
||||
return list
|
||||
})
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
refreshData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -89,8 +115,8 @@ onBeforeMount(() => {
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:plugin="data"
|
||||
@remove="fetchInstalledPlugins"
|
||||
@save="fetchInstalledPlugins"
|
||||
@remove="refreshData"
|
||||
@save="refreshData"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
@@ -152,13 +178,18 @@ onBeforeMount(() => {
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||
<PluginAppCard
|
||||
v-for="data in uninstalledList"
|
||||
:key="data.id"
|
||||
:plugin="data"
|
||||
@install="pluginInstalled"
|
||||
/>
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card items-start">
|
||||
<div
|
||||
v-for="(data, index) in getUnupdatedPlugins"
|
||||
:key="index"
|
||||
>
|
||||
<PluginAppCard
|
||||
v-if="defer(index)"
|
||||
:key="data.id"
|
||||
:plugin="data"
|
||||
@install="pluginInstalled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
|
||||
@@ -173,7 +204,7 @@ onBeforeMount(() => {
|
||||
|
||||
<style lang="scss">
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -369,7 +369,7 @@ onMounted(() => {
|
||||
</td>
|
||||
<td>{{ user.is_superuser ? "是" : "否" }}</td>
|
||||
<td>
|
||||
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name != user.name">
|
||||
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name !== user.name">
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
@@ -409,11 +409,12 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<!-- =弹窗 -->
|
||||
<VDialog
|
||||
v-model="addUserDialog"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
z-index="1010"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="新增用户">
|
||||
|
||||
@@ -24,6 +24,7 @@ const cookieCloudSetting = ref({
|
||||
COOKIECLOUD_PASSWORD: '',
|
||||
COOKIECLOUD_INTERVAL: 0,
|
||||
USER_AGENT: '',
|
||||
COOKIECLOUD_ENABLE_LOCAL: '',
|
||||
})
|
||||
|
||||
// 种子优先规则下拉框
|
||||
@@ -108,6 +109,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_PASSWORD,
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
} = result.data
|
||||
cookieCloudSetting.value = {
|
||||
COOKIECLOUD_HOST,
|
||||
@@ -115,6 +117,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_PASSWORD,
|
||||
COOKIECLOUD_INTERVAL,
|
||||
USER_AGENT,
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,12 +158,18 @@ onMounted(() => {
|
||||
<VCardSubtitle> 从CookieCloud快速同步站点数据。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCheckbox v-model="cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL" label="启用本地CookieCloud服务器" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_HOST"
|
||||
label="CookieCloud服务器地址"
|
||||
label="远程CookieCloud服务器地址"
|
||||
placeholder="https://movie-pilot.org/cookiecloud"
|
||||
:disabled="cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Site[]>([])
|
||||
@@ -14,11 +15,15 @@ const isRefreshed = ref(false)
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 获取站点列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('site/')
|
||||
isRefreshed.value = true
|
||||
defer = useDefer(dataList.value.length)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -45,13 +50,18 @@ onBeforeMount(fetchData)
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-site-card"
|
||||
>
|
||||
<SiteCard
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:site="data"
|
||||
@remove="fetchData"
|
||||
@update="fetchData"
|
||||
/>
|
||||
<div
|
||||
v-for="(data, index) in dataList"
|
||||
:key="index"
|
||||
>
|
||||
<SiteCard
|
||||
v-if="defer(index)"
|
||||
:key="data.id"
|
||||
:site="data"
|
||||
@remove="fetchData"
|
||||
@update="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
|
||||
@@ -4,11 +4,14 @@ import store from '@/store'
|
||||
// 日志列表
|
||||
const logs = ref<string[]>([])
|
||||
|
||||
// SSE消息对象
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
|
||||
)
|
||||
|
||||
@@ -17,10 +20,6 @@ function startSSELogging() {
|
||||
if (message)
|
||||
logs.value.push(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +64,11 @@ const extractLogDetails = computed(() => {
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource)
|
||||
eventSource.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
156
src/views/system/MessageView.vue
Normal file
156
src/views/system/MessageView.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script lang="ts" setup>
|
||||
import store from '@/store'
|
||||
import type { Message } from '@/api/types'
|
||||
import MessageCard from '@/components/cards/MessageCard.vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['scroll'])
|
||||
|
||||
// 消息列表
|
||||
const messages = ref<Message[]>([])
|
||||
// 当前页数据
|
||||
const currData = ref<Message[]>([])
|
||||
|
||||
// 是否完成加载
|
||||
const isLoaded = ref(false)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 当前页码
|
||||
const page = ref(1)
|
||||
|
||||
// 存量消息最新时间
|
||||
const lastTime = ref('')
|
||||
|
||||
// SSE消息对象
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// SSE持续获取消息
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}&role=user`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
const object = JSON.parse(message)
|
||||
if (compareTime(object.date, lastTime.value) <= 0)
|
||||
return
|
||||
messages.value.push(object)
|
||||
emit('scroll')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API加载存量消息
|
||||
async function loadMessages({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
return
|
||||
}
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
try {
|
||||
currData.value = await api.get('message/web', {
|
||||
params: {
|
||||
page: page.value,
|
||||
size: 20,
|
||||
},
|
||||
})
|
||||
if (currData.value.length > 0) {
|
||||
// 取最后一条时间为存量消息最新时间
|
||||
lastTime.value = currData.value[currData.value.length - 1].reg_time ?? ''
|
||||
// 合并数据
|
||||
messages.value = [...currData.value, ...messages.value]
|
||||
// 加载完成
|
||||
done('ok')
|
||||
if (page.value === 1) {
|
||||
// 滚动到底部
|
||||
emit('scroll')
|
||||
// 监听SSE消息
|
||||
startSSEMessager()
|
||||
}
|
||||
// 页码+1
|
||||
page.value++
|
||||
}
|
||||
else {
|
||||
done('ok')
|
||||
// 监听SSE消息
|
||||
startSSEMessager()
|
||||
}
|
||||
loading.value = false
|
||||
isLoaded.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 比较yyyy-MM-dd HH:mm:ss时间大小
|
||||
function compareTime(time1: string, time2: string) {
|
||||
if (!time1)
|
||||
return -1
|
||||
if (!time2)
|
||||
return 1
|
||||
return new Date(time1).getTime() - new Date(time2).getTime()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (eventSource)
|
||||
eventSource.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="start"
|
||||
:items="messages"
|
||||
class="overflow-hidden"
|
||||
@load="loadMessages"
|
||||
>
|
||||
<template #loading>
|
||||
<VProgressCircular
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
size="48"
|
||||
class="mb-5"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<div>
|
||||
<VRow
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
:class="{
|
||||
'justify-end': msg.action === 0,
|
||||
'justify-start': msg.action === 1,
|
||||
}"
|
||||
>
|
||||
<VCol
|
||||
cols="10"
|
||||
lg="6"
|
||||
xl="4"
|
||||
style="position: relative;"
|
||||
>
|
||||
<MessageCard
|
||||
:message="msg"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
<div
|
||||
v-if="messages.length === 0 && isLoaded && !loading"
|
||||
class="w-full text-center flex flex-col items-center"
|
||||
>
|
||||
<span class="mb-3">当前没有消息</span>
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
Reference in New Issue
Block a user