mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-07 00:29:56 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121cb7e442 | ||
|
|
44163f0fb2 | ||
|
|
d43865fcad | ||
|
|
fed92f3853 | ||
|
|
823d2a816e | ||
|
|
046c21edf6 | ||
|
|
8236d80b42 | ||
|
|
90e7eb1c79 | ||
|
|
ef09868af1 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.7.1-1",
|
"version": "1.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -109,4 +109,4 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"postcss": "8"
|
"postcss": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,3 +147,23 @@ export function formatEp(nums: number[]): string {
|
|||||||
|
|
||||||
return formattedRanges.join('、')
|
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 '刚刚'
|
||||||
|
}
|
||||||
|
|||||||
105
src/api/types.ts
105
src/api/types.ts
@@ -81,7 +81,7 @@ export interface Subscribe {
|
|||||||
best_version: any
|
best_version: any
|
||||||
|
|
||||||
// 使用 imdbid 搜索
|
// 使用 imdbid 搜索
|
||||||
search_imdbid?: boolean
|
search_imdbid?: any
|
||||||
|
|
||||||
// 当前优先级
|
// 当前优先级
|
||||||
current_priority: number
|
current_priority: number
|
||||||
@@ -802,18 +802,34 @@ export interface Context {
|
|||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
export interface User {
|
export interface User {
|
||||||
|
// 用户ID
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
|
// 用户名称
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
// 用户密码
|
||||||
password: string
|
password: string
|
||||||
|
|
||||||
|
// 用户邮箱
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
// 是否激活
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
|
|
||||||
|
// 是否管理员
|
||||||
is_superuser: boolean
|
is_superuser: boolean
|
||||||
|
|
||||||
|
// 头像
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储空间
|
// 存储空间
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
|
// 总空间
|
||||||
total_storage: number
|
total_storage: number
|
||||||
|
|
||||||
|
// 已使用空间
|
||||||
used_storage: number
|
used_storage: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,45 +934,132 @@ export interface Setting {
|
|||||||
|
|
||||||
// 文件浏览接口
|
// 文件浏览接口
|
||||||
export interface EndPoints {
|
export interface EndPoints {
|
||||||
|
// 文件列表
|
||||||
list: any
|
list: any
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
mkdir: any
|
mkdir: any
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
delete: any
|
delete: any
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
download: any
|
download: any
|
||||||
|
|
||||||
|
// 图片预览
|
||||||
image: any
|
image: any
|
||||||
|
|
||||||
|
// 重命名
|
||||||
rename: any
|
rename: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件浏览项目
|
// 文件浏览项目
|
||||||
export interface FileItem {
|
export interface FileItem {
|
||||||
|
// 类型
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
// 文件名
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
// 文件名不含扩展名
|
||||||
basename: string
|
basename: string
|
||||||
|
|
||||||
|
// 文件路径
|
||||||
path: string
|
path: string
|
||||||
|
|
||||||
|
// 文件扩展名
|
||||||
extension: string
|
extension: string
|
||||||
|
|
||||||
|
// 文件大小
|
||||||
size: number
|
size: number
|
||||||
|
|
||||||
|
// 文件子元素
|
||||||
children: FileItem[]
|
children: FileItem[]
|
||||||
|
|
||||||
|
// 文件创建时间
|
||||||
modify_time: number
|
modify_time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体服务器播放条目
|
// 媒体服务器播放条目
|
||||||
export interface MediaServerPlayItem {
|
export interface MediaServerPlayItem {
|
||||||
|
// ID
|
||||||
id?: string | number
|
id?: string | number
|
||||||
|
|
||||||
|
// 标题
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
|
// 副标题
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
|
|
||||||
|
// 类型
|
||||||
type?: string
|
type?: string
|
||||||
|
|
||||||
|
// 海报
|
||||||
image?: string
|
image?: string
|
||||||
|
|
||||||
|
// 链接
|
||||||
link?: string
|
link?: string
|
||||||
|
|
||||||
|
// 播放百分比
|
||||||
percent?: number
|
percent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体服务器媒体库
|
// 媒体服务器媒体库
|
||||||
export interface MediaServerLibrary {
|
export interface MediaServerLibrary {
|
||||||
|
// 服务器名称
|
||||||
server: string
|
server: string
|
||||||
|
|
||||||
|
// ID
|
||||||
id?: string | number
|
id?: string | number
|
||||||
|
|
||||||
|
// 名称
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
// 路径
|
||||||
path?: string
|
path?: string
|
||||||
|
|
||||||
|
// 类型
|
||||||
type?: string
|
type?: string
|
||||||
|
|
||||||
|
// 图片
|
||||||
image?: string
|
image?: string
|
||||||
|
|
||||||
|
// 图片列表
|
||||||
image_list?: string[]
|
image_list?: string[]
|
||||||
|
|
||||||
|
// 链接
|
||||||
link?: 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const isDownloading = ref(props.info?.state === 'downloading')
|
|||||||
|
|
||||||
// 监听props.info?.state的变化
|
// 监听props.info?.state的变化
|
||||||
watch(() => props.info?.state, (newValue) => {
|
watch(() => props.info?.state, (newValue) => {
|
||||||
isDownloading.value = newValue === 'downloading';
|
isDownloading.value = newValue === 'downloading'
|
||||||
});
|
})
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
|||||||
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 {
|
try {
|
||||||
// 显示等待提示框
|
// 显示等待提示框
|
||||||
progressDialog.value = true
|
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(
|
const result: { [key: string]: any } = await api.get(
|
||||||
`plugin/install/${props.plugin?.id}`,
|
`plugin/install/${props.plugin?.id}`,
|
||||||
@@ -163,15 +163,6 @@ const dropdownItems = ref([
|
|||||||
</VMenu>
|
</VMenu>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</div>
|
</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
|
<VAvatar
|
||||||
size="8rem"
|
size="8rem"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -41,12 +41,18 @@ const pluginConfigDialog = ref(false)
|
|||||||
// 插件配置表单数据
|
// 插件配置表单数据
|
||||||
const pluginConfigForm = ref({})
|
const pluginConfigForm = ref({})
|
||||||
|
|
||||||
|
// 进度框
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
// 插件表单配置项
|
// 插件表单配置项
|
||||||
let pluginFormItems = reactive([])
|
let pluginFormItems = reactive([])
|
||||||
|
|
||||||
// 插件数据页面
|
// 插件数据页面
|
||||||
const pluginInfoDialog = ref(false)
|
const pluginInfoDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度框文本
|
||||||
|
const progressText = ref('正在更新插件...')
|
||||||
|
|
||||||
// 插件数据页面配置项
|
// 插件数据页面配置项
|
||||||
let pluginPageItems = reactive([])
|
let pluginPageItems = reactive([])
|
||||||
|
|
||||||
@@ -83,7 +89,12 @@ async function uninstallPlugin() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 显示等待提示框
|
||||||
|
progressDialog.value = true
|
||||||
|
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
|
||||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||||
|
// 隐藏等待提示框
|
||||||
|
progressDialog.value = false
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
|
$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() {
|
function visitAuthorPage() {
|
||||||
window.open(props.plugin?.author_url, '_blank')
|
window.open(props.plugin?.author_url, '_blank')
|
||||||
@@ -254,8 +300,18 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '重置',
|
title: '更新',
|
||||||
value: 3,
|
value: 3,
|
||||||
|
show: props.plugin?.has_update,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-cancel',
|
||||||
|
color: 'success',
|
||||||
|
click: updatePlugin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '重置',
|
||||||
|
value: 4,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-cancel',
|
prependIcon: 'mdi-cancel',
|
||||||
@@ -265,7 +321,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '卸载',
|
title: '卸载',
|
||||||
value: 4,
|
value: 5,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-trash-can-outline',
|
prependIcon: 'mdi-trash-can-outline',
|
||||||
@@ -275,7 +331,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '查看日志',
|
title: '查看日志',
|
||||||
value: 5,
|
value: 6,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-file-document-outline',
|
prependIcon: 'mdi-file-document-outline',
|
||||||
@@ -286,7 +342,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '作者主页',
|
title: '作者主页',
|
||||||
value: 5,
|
value: 7,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-home-circle-outline',
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -313,6 +376,15 @@ const dropdownItems = ref([
|
|||||||
class="relative pa-4 text-center card-cover-blurred"
|
class="relative pa-4 text-center card-cover-blurred"
|
||||||
:style="{ background: `${backgroundColor}` }"
|
: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">
|
<div class="me-n3 absolute top-0 right-3">
|
||||||
<IconBtn>
|
<IconBtn>
|
||||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||||
@@ -430,6 +502,25 @@ const dropdownItems = ref([
|
|||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
total_episode: 0,
|
total_episode: 0,
|
||||||
start_episode: 0,
|
start_episode: 0,
|
||||||
best_version: 0,
|
best_version: 0,
|
||||||
search_imdbid: false,
|
search_imdbid: 0,
|
||||||
sites: [],
|
sites: [],
|
||||||
type: '',
|
type: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -100,6 +100,7 @@ async function getSubscribeInfo() {
|
|||||||
)
|
)
|
||||||
subscribeForm.value = result
|
subscribeForm.value = result
|
||||||
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
||||||
|
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const superUser = store.state.auth.superUser
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
|
|
||||||
<!-- 👉 Shortcuts -->
|
<!-- 👉 Shortcuts -->
|
||||||
<ShortcutBar />
|
<ShortcutBar v-if="superUser" />
|
||||||
|
|
||||||
<!-- 👉 Theme -->
|
<!-- 👉 Theme -->
|
||||||
<NavbarThemeSwitcher class="me-2" />
|
<NavbarThemeSwitcher class="me-2" />
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import NetTestView from '@/views/system/NetTestView.vue'
|
|||||||
import LoggingView from '@/views/system/LoggingView.vue'
|
import LoggingView from '@/views/system/LoggingView.vue'
|
||||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||||
import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
||||||
|
import MessageView from '@/views/system/MessageView.vue'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
// App捷径
|
// App捷径
|
||||||
const appsMenu = ref(false)
|
const appsMenu = ref(false)
|
||||||
@@ -24,11 +26,53 @@ const ruleTestDialog = ref(false)
|
|||||||
// 系统健康检查弹窗
|
// 系统健康检查弹窗
|
||||||
const systemTestDialog = 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
|
// 拼接全部日志url
|
||||||
function allLoggingUrl() {
|
function allLoggingUrl() {
|
||||||
const token = store.state.auth.token
|
const token = store.state.auth.token
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -124,7 +168,7 @@ function allLoggingUrl() {
|
|||||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||||
日志
|
日志
|
||||||
</h6>
|
</h6>
|
||||||
<span class="text-sm">查看实时日志</span>
|
<span class="text-sm">实时日志</span>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol
|
<VCol
|
||||||
@@ -145,7 +189,7 @@ function allLoggingUrl() {
|
|||||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||||
网络
|
网络
|
||||||
</h6>
|
</h6>
|
||||||
<span class="text-sm">测试网速连通性</span>
|
<span class="text-sm">网速连通性测试</span>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -168,7 +212,28 @@ function allLoggingUrl() {
|
|||||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||||
系统
|
系统
|
||||||
</h6>
|
</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>
|
</VListItem>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -249,4 +314,41 @@ function allLoggingUrl() {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</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>
|
</template>
|
||||||
|
|||||||
@@ -55,17 +55,37 @@ async function fetchUninstalledPlugins() {
|
|||||||
state: 'market',
|
state: 'market',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
// 设置APP市场加载完成
|
||||||
isAppMarketLoaded.value = true
|
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) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载所有数据
|
||||||
onBeforeMount(() => {
|
function refreshData() {
|
||||||
fetchInstalledPlugins()
|
fetchInstalledPlugins()
|
||||||
fetchUninstalledPlugins()
|
fetchUninstalledPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取没有更新的插件
|
||||||
|
const getUnupdatedPlugins = computed(() => {
|
||||||
|
return uninstalledList.value.filter(item => !item.has_update)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载时获取数据
|
||||||
|
onBeforeMount(() => {
|
||||||
|
refreshData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,8 +109,8 @@ onBeforeMount(() => {
|
|||||||
v-for="data in dataList"
|
v-for="data in dataList"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
:plugin="data"
|
:plugin="data"
|
||||||
@remove="fetchInstalledPlugins"
|
@remove="refreshData"
|
||||||
@save="fetchInstalledPlugins"
|
@save="refreshData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
@@ -154,7 +174,7 @@ onBeforeMount(() => {
|
|||||||
</div>
|
</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 uninstalledList"
|
v-for="data in getUnupdatedPlugins"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
:plugin="data"
|
:plugin="data"
|
||||||
@install="pluginInstalled"
|
@install="pluginInstalled"
|
||||||
|
|||||||
137
src/views/system/MessageView.vue
Normal file
137
src/views/system/MessageView.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
// SSE持续获取消息
|
||||||
|
function startSSEMessager() {
|
||||||
|
const token = store.state.auth.token
|
||||||
|
if (token) {
|
||||||
|
const eventSource = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}&role=user`,
|
||||||
|
)
|
||||||
|
|
||||||
|
eventSource.addEventListener('message', (event) => {
|
||||||
|
const message = event.data
|
||||||
|
if (message) {
|
||||||
|
const object = JSON.parse(message)
|
||||||
|
messages.value.push(object)
|
||||||
|
emit('scroll')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
eventSource.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用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) {
|
||||||
|
// 合并数据
|
||||||
|
messages.value = [...currData.value, ...messages.value]
|
||||||
|
// 加载完成
|
||||||
|
done('ok')
|
||||||
|
if (page.value === 1) {
|
||||||
|
// 滚动到底部
|
||||||
|
emit('scroll')
|
||||||
|
}
|
||||||
|
// 页码+1
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
done('ok')
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
isLoaded.value = true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听新消息
|
||||||
|
startSSEMessager()
|
||||||
|
})
|
||||||
|
</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