mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-12 02:21:06 +08:00
227 lines
5.5 KiB
Vue
227 lines
5.5 KiB
Vue
<script lang="ts" setup>
|
|
import MarkdownIt from 'markdown-it'
|
|
import mdLinkAttributes from 'markdown-it-link-attributes'
|
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
|
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 emit = defineEmits(['imageload'])
|
|
|
|
// 图片是否加载完成
|
|
const isImageLoaded = ref(false)
|
|
|
|
// 图片是否加载失败
|
|
const imageLoadError = ref(false)
|
|
|
|
// 初始化 markdown-it
|
|
const md = new MarkdownIt({
|
|
html: true,
|
|
breaks: true,
|
|
linkify: true,
|
|
typographer: true,
|
|
})
|
|
|
|
// 插件:链接在新窗口打开
|
|
md.use(mdLinkAttributes, {
|
|
attrs: {
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
},
|
|
})
|
|
|
|
// 图片加载完成
|
|
async function imageLoaded() {
|
|
isImageLoaded.value = true
|
|
emit('imageload')
|
|
}
|
|
|
|
// 链接打开新窗口
|
|
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) {
|
|
return props.message.note
|
|
}
|
|
}
|
|
return {}
|
|
}
|
|
|
|
// 渲染 Markdown
|
|
function renderMarkdown(value: string) {
|
|
if (!value) return ''
|
|
return md.render(value)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink" max-width="23rem">
|
|
<div v-if="props.message?.image" class="relative text-center card-cover-blurred">
|
|
<VImg
|
|
:src="props.message?.image"
|
|
aspect-ratio="3/2"
|
|
cover
|
|
position="top"
|
|
@load="imageLoaded"
|
|
@error="imageLoadError = true"
|
|
min-height="10rem"
|
|
>
|
|
<template #placeholder>
|
|
<div class="w-full h-full">
|
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
</div>
|
|
</template>
|
|
</VImg>
|
|
</div>
|
|
<div
|
|
v-if="
|
|
props.message?.title &&
|
|
!props.message?.text &&
|
|
!props.message?.image &&
|
|
isNullOrEmptyObject(props.message?.note) &&
|
|
props.message?.action === 0
|
|
"
|
|
class="rounded-md text-body-1 py-2 px-4 elevation-2 bg-primary text-white chat-right mb-1"
|
|
>
|
|
<p class="mb-0">{{ props.message?.title }}</p>
|
|
</div>
|
|
<VCardTitle v-else-if="props.message?.title" class="break-words whitespace-break-spaces">
|
|
{{ props.message?.title }}
|
|
</VCardTitle>
|
|
<div
|
|
v-if="props.message?.text && props.message?.action === 0"
|
|
class="rounded-md text-body-1 py-1 px-4 elevation-2 bg-primary text-white chat-right"
|
|
>
|
|
<div class="markdown-body" v-html="renderMarkdown(props.message?.text)" />
|
|
</div>
|
|
<VCardText
|
|
v-if="props.message?.text && props.message?.action === 1"
|
|
class="markdown-body"
|
|
v-html="renderMarkdown(props.message?.text)"
|
|
/>
|
|
<VCardText v-if="!isNullOrEmptyObject(props.message?.note)">
|
|
<VList>
|
|
<VListItem v-for="(value, key) in noteToJson()" :key="key" two-line>
|
|
<VListItemTitle v-if="value.title_year" class="font-bold break-words whitespace-break-spaces">
|
|
{{ Number(key) + 1 }}. {{ value.title_year }}
|
|
</VListItemTitle>
|
|
<VListItemTitle v-if="value.enclosure" class="font-bold break-words whitespace-break-spaces">
|
|
{{ Number(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>
|
|
</VCard>
|
|
<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>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.markdown-body {
|
|
word-break: break-all;
|
|
|
|
p {
|
|
margin-block-end: 0.5rem;
|
|
}
|
|
|
|
p:last-child {
|
|
margin-block-end: 0;
|
|
}
|
|
|
|
a {
|
|
color: inherit;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
ul {
|
|
list-style-type: disc;
|
|
margin-block-end: 0.5rem;
|
|
padding-inline-start: 1.5rem;
|
|
}
|
|
|
|
ol {
|
|
list-style-type: decimal;
|
|
margin-block-end: 0.5rem;
|
|
padding-inline-start: 1.5rem;
|
|
}
|
|
|
|
li {
|
|
display: list-item;
|
|
margin-block-end: 0.25rem;
|
|
}
|
|
|
|
code {
|
|
border-radius: 4px;
|
|
background-color: rgba(var(--v-border-color), 0.1);
|
|
font-family: monospace;
|
|
padding-block: 0.2rem;
|
|
padding-inline: 0.4rem;
|
|
}
|
|
|
|
pre {
|
|
overflow: auto;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
background-color: rgba(var(--v-border-color), 0.1);
|
|
margin-block-end: 0.5rem;
|
|
|
|
code {
|
|
padding: 0;
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
|
|
blockquote {
|
|
border-inline-start: 4px solid rgba(var(--v-border-color), 0.2);
|
|
font-style: italic;
|
|
margin-block-end: 0.5rem;
|
|
padding-inline-start: 1rem;
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
inline-size: 100%;
|
|
margin-block-end: 1rem;
|
|
|
|
th,
|
|
td {
|
|
padding: 0.5rem;
|
|
border: 1px solid rgba(var(--v-border-color), 0.1);
|
|
text-align: start;
|
|
}
|
|
|
|
th {
|
|
background-color: rgba(var(--v-border-color), 0.05);
|
|
}
|
|
}
|
|
|
|
img {
|
|
block-size: auto;
|
|
max-inline-size: 100%;
|
|
}
|
|
}
|
|
</style>
|