refactor: remove ShortcutMessageDialog and MessageView components; update ShortcutBar and UserNotification for improved notification handling

This commit is contained in:
jxxghp
2026-06-17 16:32:00 +08:00
parent c055740926
commit c66ee881b1
9 changed files with 454 additions and 962 deletions

View File

@@ -1,226 +0,0 @@
<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>

View File

@@ -1,139 +0,0 @@
<script setup lang="ts">
import api from '@/api'
import { clearUnreadMessages } from '@/utils/badge'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const MessageView = defineAsyncComponent(() => import('@/views/system/MessageView.vue'))
type MessageViewExpose = {
pauseSSE?: () => void
resumeSSE?: () => void
refreshLatestMessages?: () => Promise<void> | void
forceScrollToEnd?: () => void
}
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
modelValue: {
type: Boolean,
default: true,
},
})
// 定义触发的自定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 弹窗显示状态
const visible = computed({
get: () => props.modelValue,
set: value => {
emit('update:modelValue', value)
if (!value) emit('close')
},
})
// 输入消息
const user_message = ref('')
// 发送按钮是否可用
const sendButtonDisabled = ref(false)
// 消息视图引用
const messageViewRef = ref<MessageViewExpose | null>(null)
/** 发送 Web 消息。 */
async function sendMessage() {
const messageText = user_message.value.trim()
if (!messageText) {
return
}
try {
sendButtonDisabled.value = true
await api.post(`message/web?text=${encodeURIComponent(messageText)}`)
user_message.value = ''
messageViewRef.value?.forceScrollToEnd?.()
} catch (error) {
console.error(error)
} finally {
sendButtonDisabled.value = false
}
}
/** 清除未读消息计数和桌面角标。 */
function clearUnreadMessageState() {
window.setTimeout(() => {
void clearUnreadMessages()
}, 500)
}
watch(visible, async newValue => {
if (newValue) {
await nextTick()
messageViewRef.value?.resumeSSE?.()
clearUnreadMessageState()
return
}
messageViewRef.value?.pauseSSE?.()
})
onMounted(async () => {
await nextTick()
clearUnreadMessageState()
})
onUnmounted(() => {
messageViewRef.value?.pauseSSE?.()
})
</script>
<template>
<VDialog v-if="visible" v-model="visible" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-message" class="me-2" />
{{ t('shortcut.message.subtitle') }}
</VCardTitle>
<VDialogCloseBtn v-model="visible" />
</VCardItem>
<VDivider />
<VCardText>
<MessageView ref="messageViewRef" />
</VCardText>
<VDivider />
<VCardActions class="pa-4">
<div class="d-flex w-100 gap-2">
<VTextField
v-model="user_message"
variant="outlined"
hide-details
density="compact"
:placeholder="t('common.inputMessage')"
@keyup.enter="sendMessage"
/>
<VBtn
variant="elevated"
:disabled="sendButtonDisabled"
@click="sendMessage"
:loading="sendButtonDisabled"
color="primary"
prepend-icon="mdi-send"
>{{ t('common.send') }}
</VBtn>
</div>
</VCardActions>
</VCard>
</VDialog>
</template>