mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
fix message ui
This commit is contained in:
@@ -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 '刚刚'
|
||||||
|
}
|
||||||
|
|||||||
+6
-3
@@ -1051,12 +1051,15 @@ export interface Message {
|
|||||||
// 消息时间
|
// 消息时间
|
||||||
date?: string
|
date?: string
|
||||||
|
|
||||||
|
// 登记时间
|
||||||
|
reg_time?: string
|
||||||
|
|
||||||
// 用户ID
|
// 用户ID
|
||||||
userid?: string
|
userid?: string
|
||||||
|
|
||||||
// 用户名称
|
|
||||||
username?: string
|
|
||||||
|
|
||||||
// 消息方向:0-接收,1-发送
|
// 消息方向:0-接收,1-发送
|
||||||
action?: number
|
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)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Message } from '@/api/types'
|
import type { Message } from '@/api/types'
|
||||||
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -24,12 +25,33 @@ function openLink() {
|
|||||||
if (props.message?.link)
|
if (props.message?.link)
|
||||||
window.open(props.message.link, '_blank')
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard
|
<VCard
|
||||||
:width="props.width"
|
:width="props.width"
|
||||||
:height="props.height"
|
:height="props.height"
|
||||||
|
variant="tonal"
|
||||||
@click="openLink"
|
@click="openLink"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -45,7 +67,7 @@ function openLink() {
|
|||||||
@error="imageLoadError = true"
|
@error="imageLoadError = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<VCardTitle v-if="props.message?.title">
|
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
|
||||||
{{ props.message?.title }}
|
{{ props.message?.title }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<VAlert
|
<VAlert
|
||||||
@@ -56,8 +78,35 @@ function openLink() {
|
|||||||
<template #prepend />
|
<template #prepend />
|
||||||
{{ props.message?.text }}
|
{{ props.message?.text }}
|
||||||
</VAlert>
|
</VAlert>
|
||||||
<VCardText v-if="props.message?.text && props.message?.action === 1">
|
<VCardText
|
||||||
{{ props.message?.text }}
|
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>
|
</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>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 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)
|
||||||
@@ -28,11 +29,48 @@ const systemTestDialog = ref(false)
|
|||||||
// 消息中心弹窗
|
// 消息中心弹窗
|
||||||
const messageDialog = ref(false)
|
const messageDialog = ref(false)
|
||||||
|
|
||||||
|
// 输入消息
|
||||||
|
const user_message = ref('')
|
||||||
|
|
||||||
|
// 发送按钮是否可用
|
||||||
|
const sendButtonDisabled = ref(false)
|
||||||
|
|
||||||
|
// 聊天容器
|
||||||
|
const chatContainer = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
function scrollMessageToEnd() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (chatContainer.value)
|
||||||
|
chatContainer.value.scrollTop = chatContainer.value.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>
|
||||||
@@ -282,9 +320,32 @@ function allLoggingUrl() {
|
|||||||
>
|
>
|
||||||
<VCard title="消息中心">
|
<VCard title="消息中心">
|
||||||
<DialogCloseBtn @click="messageDialog = false" />
|
<DialogCloseBtn @click="messageDialog = false" />
|
||||||
<VCardText>
|
<VCardText ref="chatContainer">
|
||||||
<MessageView />
|
<MessageView @scroll="scrollMessageToEnd" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardItem>
|
||||||
|
<VTextField
|
||||||
|
v-model="user_message"
|
||||||
|
placeholder="输入消息或命令"
|
||||||
|
outlined
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
@keydown.enter="sendMessage"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
:disabled="sendButtonDisabled"
|
||||||
|
@click="sendMessage"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</VBtn>
|
||||||
|
</template>
|
||||||
|
</VTextField>
|
||||||
|
</VCardItem>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,24 +4,15 @@ import MessageCard from '@/components/cards/MessageCard.vue'
|
|||||||
import type { Message } from '@/api/types'
|
import type { Message } from '@/api/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['scroll'])
|
||||||
|
|
||||||
// 消息列表
|
// 消息列表
|
||||||
const messages = ref<Message[]>([])
|
const messages = ref<Message[]>([])
|
||||||
|
|
||||||
// 是否完成加载
|
// 是否完成加载
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
// 输入消息
|
|
||||||
const content = ref('')
|
|
||||||
|
|
||||||
// 聊天容器
|
|
||||||
const chatContainer = ref<HTMLDivElement>()
|
|
||||||
|
|
||||||
// 滚动到底部
|
|
||||||
function scrollToEnd() {
|
|
||||||
if (chatContainer.value)
|
|
||||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSE持续获取消息
|
// SSE持续获取消息
|
||||||
function startSSEMessager() {
|
function startSSEMessager() {
|
||||||
const token = store.state.auth.token
|
const token = store.state.auth.token
|
||||||
@@ -35,7 +26,7 @@ function startSSEMessager() {
|
|||||||
if (message) {
|
if (message) {
|
||||||
const object = JSON.parse(message)
|
const object = JSON.parse(message)
|
||||||
messages.value.push(object)
|
messages.value.push(object)
|
||||||
scrollToEnd()
|
emit('scroll')
|
||||||
}
|
}
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
})
|
})
|
||||||
@@ -52,7 +43,7 @@ async function loadMessages() {
|
|||||||
messages.value = await api.get('message/web')
|
messages.value = await api.get('message/web')
|
||||||
if (messages.value.length > 0) {
|
if (messages.value.length > 0) {
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
scrollToEnd()
|
emit('scroll')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -86,7 +77,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<span class="mb-3">当前没有消息</span>
|
<span class="mb-3">当前没有消息</span>
|
||||||
</div>
|
</div>
|
||||||
<VContainer ref="chatContainer" fluid style="padding: 0;">
|
<div>
|
||||||
<VRow
|
<VRow
|
||||||
v-for="(msg, index) in messages"
|
v-for="(msg, index) in messages"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -96,7 +87,7 @@ onMounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<VCol
|
<VCol
|
||||||
sm="8"
|
cols="10"
|
||||||
lg="6"
|
lg="6"
|
||||||
xl="4"
|
xl="4"
|
||||||
style="position: relative;"
|
style="position: relative;"
|
||||||
@@ -106,5 +97,5 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VContainer>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user