feat: 即时消息页面重构,新增表情,图片,文件功能,修复聊天区域布局问题,解决消息过多时输入框被挤下去的问题,调整系统消息页面

This commit is contained in:
QDKF 2025-09-18 22:35:37 +08:00
parent aa87b0e8e4
commit 935c68ac6d
7 changed files with 1539 additions and 178 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -727,10 +727,6 @@ const updateActiveNavItem = () => {
</script> </script>
<style scoped> <style scoped>
.admin-dashboard {
/* min-height: 100vh; */
}
.top-image-container { .top-image-container {
position: relative; position: relative;
width: 100%; width: 100%;
@ -1174,3 +1170,5 @@ const updateActiveNavItem = () => {
font-weight: normal; font-weight: normal;
} }
</style> </style>

View File

@ -217,7 +217,7 @@ const handleTabChange = (tabName: string) => {
} }
.tab-container { .tab-container {
margin-bottom: 20px; margin-bottom: 9px;
} }
.message-tabs { .message-tabs {

View File

@ -1,24 +1,73 @@
<template> <template>
<div class="message-input-container"> <div class="message-input-container">
<!-- 隐藏的文件输入框 -->
<input ref="fileInputRef" type="file" accept="image/*,.pdf,.doc,.docx,.txt,.zip,.rar" style="display: none"
@change="handleFileSelect" />
<!-- 图片预览区域 -->
<div v-if="selectedImages.length > 0" class="image-preview-container">
<div class="image-preview-list">
<div v-for="(image, index) in selectedImages" :key="index" class="image-preview-item">
<img :src="image.url" :alt="image.name" class="preview-image" />
<button class="remove-image-btn" @click="removeImage(index)">×</button>
</div>
</div>
</div>
<!-- 文件预览区域 -->
<div v-if="selectedFiles.length > 0" class="file-preview-container">
<div class="file-preview-list">
<div v-for="(file, index) in selectedFiles" :key="index" class="file-preview-item">
<div class="file-icon">
<img :src="getFileIcon(file.type)" :alt="file.name" class="file-preview-image" />
</div>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ formatFileSize(file.size) }}</div>
</div>
<button class="remove-file-btn" @click="removeFile(index)">×</button>
</div>
</div>
</div>
<div class="input-wrapper"> <div class="input-wrapper">
<n-input <n-input v-model:value="messageText" type="textarea" :placeholder="placeholder"
v-model:value="messageText" :autosize="{ minRows: 3, maxRows: 6 }" :bordered="false" class="message-input" @keydown="handleKeyDown"
type="textarea" @input="handleInput" />
:placeholder="placeholder" </div>
:autosize="{ minRows: 1, maxRows: 4 }"
:bordered="false"
class="message-input" <div class="input-footer">
@keydown="handleKeyDown" <div class="action-buttons">
@input="handleInput" <div class="emoji-button-wrapper">
/> <button class="action-btn" :class="{ active: showEmojiPicker }" @click="toggleEmojiPicker">
<div class="input-actions"> <img src="/images/teacher/expression.png" alt="表情" class="action-icon" />
<n-button </button>
type="primary" <!-- 表情选择器从表情按钮上方弹出 -->
size="medium" <div v-if="showEmojiPicker" class="emoji-picker">
:disabled="!canSend" <div class="emoji-picker-header">
class="send-button" <span class="emoji-title">选择表情</span>
@click="handleSend" <button class="emoji-close" @click="closeEmojiPicker">×</button>
> </div>
<div class="emoji-grid">
<button v-for="emoji in emojiList" :key="emoji" class="emoji-item" @click="insertEmoji(emoji)">
{{ emoji }}
</button>
</div>
</div>
</div>
<button class="action-btn" @click="handleImage">
<img src="/images/teacher/Image.png" alt="图片" class="action-icon" />
</button>
<button class="action-btn" @click="handleFile">
<img src="/images/teacher/file.png" alt="文件" class="action-icon" />
</button>
</div>
<div class="footer-right">
<span class="char-count">{{ messageText.length }}/{{ maxLength }}</span>
<n-button type="primary" size="medium" :disabled="!canSend" class="send-button" @click="handleSend">
发送 发送
</n-button> </n-button>
</div> </div>
@ -47,21 +96,186 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{ const emit = defineEmits<{
send: [message: string] send: [message: string]
input: [value: string] input: [value: string]
emoji: []
image: [data: { file: File; url: string; name: string; size: number }]
file: [data: { file: File; name: string; size: number; type: string }]
}>() }>()
// //
const messageText = ref('') const messageText = ref('')
const showEmojiPicker = ref(false)
const fileInputRef = ref<HTMLInputElement | null>(null)
const selectedImages = ref<Array<{ file: File; url: string; name: string; size: number }>>([])
const selectedFiles = ref<Array<{ file: File; name: string; size: number; type: string }>>([])
//
const emojiList = ref([
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
'🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚',
'😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩',
'🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣',
'😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬',
'🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗',
'🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😬', '🙄', '😯',
'😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐',
'🥴', '🤢', '🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '😈',
'👿', '👹', '👺', '🤡', '💩', '👻', '💀', '☠️', '👽', '👾',
'🤖', '🎃', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿',
'😾', '👶', '🧒', '👦', '👧', '🧑', '👨', '👩', '🧓', '👴',
'👵', '👱', '🧔', '👲', '🧕', '👮', '👷', '💂', '🕵️', '👩‍⚕️',
'👨‍⚕️', '👩‍🌾', '👨‍🌾', '👩‍🍳', '👨‍🍳', '👩‍🎓', '👨‍🎓', '👩‍🎤', '👨‍🎤', '👩‍🏫',
'👨‍🏫', '👩‍🏭', '👨‍🏭', '👩‍💻', '👨‍💻', '👩‍💼', '👨‍💼', '👩‍🔧', '👨‍🔧', '👩‍🔬',
'👨‍🔬', '👩‍🎨', '👨‍🎨', '👩‍🚒', '👨‍🚒', '👩‍✈️', '👨‍✈️', '👩‍🚀', '👨‍🚀', '👩‍⚖️',
'👨‍⚖️', '👰', '🤵', '👸', '🤴', '🦸', '🦹', '🤶', '🎅', '🧙',
'🧚', '🧛', '🧜', '🧝', '🧞', '🧟', '💆', '💇', '🚶', '🏃',
'💃', '🕺', '👯', '🧘', '🛀', '🛌', '👭', '👫', '👬', '💏',
'💑', '👪', '🗣️', '👤', '👥', '🫂', '👋', '🤚', '🖐️', '✋',
'🖖', '👌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉',
'👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜',
'👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪',
'🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🦷', '🦴',
'👀', '👁️', '👅', '👄', '💋', '🩸', '💎', '👑', '🥽', '🕶️',
'👓', '🦺', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', '👗',
'👘', '🥻', '🩱', '🩲', '🩳', '👙', '👚', '👛', '👜', '👝',
'🎒', '👞', '👟', '🥾', '🥿', '👠', '👡', '🩰', '👢', '👑',
'💍', '💎', '💄', '💅', '💇', '💆', '🦯', '🦽', '🦼', '🩹',
'🩺', '💊', '💉', '🧬', '🦠', '🩸', '💧', '💦', '💨', '🕳️',
'💣', '💥', '💢', '💫', '💦', '💨', '🕳️', '💣', '💥', '💢',
'💫', '💦', '💨', '🕳️', '💣', '💥', '💢', '💫', '💦', '💨'
])
// //
const canSend = computed(() => { const canSend = computed(() => {
return messageText.value.trim().length > 0 && !props.disabled return (messageText.value.trim().length > 0 || selectedImages.value.length > 0 || selectedFiles.value.length > 0) && !props.disabled
}) })
// //
//
const handleImage = () => {
fileInputRef.value?.click()
}
const handleFile = () => {
fileInputRef.value?.click()
}
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
const file = files[0]
//
if (file.type.startsWith('image/')) {
handleImageUpload(file)
} else {
handleFileUpload(file)
}
}
// input
target.value = ''
}
const handleImageUpload = (file: File) => {
// (5MB)
if (file.size > 5 * 1024 * 1024) {
alert('图片大小不能超过5MB')
return
}
//
const reader = new FileReader()
reader.onload = (e) => {
try {
const imageUrl = e.target?.result as string
if (imageUrl) {
//
const imageData = {
file,
url: imageUrl,
name: file.name,
size: file.size
}
selectedImages.value.push(imageData)
}
} catch (error) {
console.error('处理图片时出错:', error)
}
}
reader.onerror = () => {
console.error('读取图片文件失败')
}
reader.readAsDataURL(file)
}
const handleFileUpload = (file: File) => {
// (10MB)
if (file.size > 10 * 1024 * 1024) {
alert('文件大小不能超过10MB')
return
}
//
selectedFiles.value.push({
file,
name: file.name,
size: file.size,
type: file.type
})
}
const removeImage = (index: number) => {
selectedImages.value.splice(index, 1)
}
const removeFile = (index: number) => {
selectedFiles.value.splice(index, 1)
}
//
const getFileIcon = (type: string) => {
if (type.includes('pdf')) return '/images/profile/pdf.png'
if (type.includes('word') || type.includes('document')) return '/images/profile/word.png'
if (type.includes('excel') || type.includes('spreadsheet')) return '/images/profile/xls.png'
if (type.includes('powerpoint') || type.includes('presentation')) return '/images/profile/ppt.png'
if (type.includes('zip') || type.includes('rar')) return '/images/profile/zip.png'
if (type.includes('text')) return '/images/profile/doc.png'
return '/images/profile/file.png'
}
//
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const handleSend = () => { const handleSend = () => {
if (canSend.value) { if (canSend.value) {
emit('send', messageText.value.trim()) //
if (messageText.value.trim()) {
emit('send', messageText.value.trim())
}
//
if (selectedImages.value.length > 0) {
selectedImages.value.forEach((image: { file: File; url: string; name: string; size: number }) => {
emit('image', image)
})
}
//
if (selectedFiles.value.length > 0) {
selectedFiles.value.forEach((file: { file: File; name: string; size: number; type: string }) => {
emit('file', file)
})
}
//
messageText.value = '' messageText.value = ''
selectedImages.value = []
selectedFiles.value = []
} }
} }
@ -77,6 +291,24 @@ const handleKeyDown = (event: KeyboardEvent) => {
} }
} }
//
const toggleEmojiPicker = () => {
showEmojiPicker.value = !showEmojiPicker.value
}
const closeEmojiPicker = () => {
showEmojiPicker.value = false
}
const insertEmoji = (emoji: string) => {
messageText.value += emoji
closeEmojiPicker()
//
emit('input', messageText.value)
}
//
// //
defineExpose({ defineExpose({
focus: () => { focus: () => {
@ -95,58 +327,344 @@ defineExpose({
left: 0; left: 0;
right: 0; right: 0;
background: #ffffff; background: #ffffff;
border-top: 1px solid #e8e8e8; border: 1px solid #E0E0E0;
padding: 16px 20px; padding: 16px 20px;
z-index: 100; z-index: 100;
position: relative;
} }
.input-wrapper { /* 图片预览样式 */
.image-preview-container {
padding: 12px;
border-bottom: 1px solid #F0F0F0;
background: #FAFAFA;
margin: -16px -20px 16px -20px;
}
.image-preview-list {
display: flex; display: flex;
align-items: center; flex-wrap: wrap;
gap: 12px;
max-width: 1200px;
margin: 0 auto;
}
.message-input {
flex: 1;
background: #f8f9fa;
border-radius: 12px;
padding: 12px 16px;
min-height: 44px;
font-size: 14px;
line-height: 1.5;
transition: all 0.2s ease;
}
.message-input:hover {
background: #f0f2f5;
}
.message-input:focus-within {
background: #ffffff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.input-actions {
display: flex;
align-items: center;
gap: 8px; gap: 8px;
} }
.send-button { .image-preview-item {
height: 44px; position: relative;
padding: 0 24px; width: 80px;
border-radius: 12px; height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #E0E0E0;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.remove-image-btn {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #ff4757;
color: white;
border: none;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.remove-image-btn:hover {
background: #ff3742;
transform: scale(1.1);
}
/* 文件预览样式 */
.file-preview-container {
padding: 12px;
border-bottom: 1px solid #F0F0F0;
background: #FAFAFA;
margin: -16px -20px 16px -20px;
}
.file-preview-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.file-preview-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #ffffff;
border: 1px solid #E0E0E0;
border-radius: 8px;
position: relative;
}
.file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.file-preview-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.file-type-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #F5F5F5;
border-radius: 4px;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 14px;
font-weight: 500; font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.file-size {
font-size: 12px;
color: #666;
}
.remove-file-btn {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #ff4757;
color: white;
border: none;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.remove-file-btn:hover {
background: #ff3742;
transform: scale(1.1);
}
.input-wrapper {
margin-bottom: 12px;
}
.message-input {
width: 100%;
background: #ffffff;
border-top: none;
border-radius: 0 0 3px 3px;
padding: 5px 12px 12px 0;
font-size: 14px;
line-height: 1.6;
transition: all 0.2s ease;
resize: none;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-buttons {
display: flex;
gap: 8px;
}
.emoji-button-wrapper {
position: relative;
}
.action-btn {
width: 32px;
height: 32px;
border: 1.5px solid #D8D8D8;
background: transparent;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
background: #E3F2FD;
border-color: #D8D8D8;
}
.action-btn.active {
background: #E3F2FD;
border-color: #0288D1;
}
.action-icon {
width: 17px;
height: 17px;
object-fit: contain;
}
.footer-right {
display: flex;
align-items: center;
gap: 12px;
}
.char-count {
font-size: 10px;
color: #3F3F44;
}
.send-button {
width: 70px;
height: 35px;
padding: 0;
border-radius: 3px;
font-weight: 400;
font-size: 14px; font-size: 14px;
white-space: nowrap; white-space: nowrap;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.send-button:not(:disabled):hover { /* 表情选择器样式 */
transform: translateY(-1px); .emoji-picker {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3); position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 12px;
background: #ffffff;
border: 1px solid #E8E8E8;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
z-index: 1000;
max-height: 200px;
overflow: hidden;
min-width: 320px;
backdrop-filter: blur(8px);
}
.emoji-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #F0F0F0;
background: #FAFAFA;
border-radius: 12px 12px 0 0;
}
.emoji-title {
font-size: 13px;
color: #333;
font-weight: 600;
letter-spacing: 0.3px;
}
.emoji-close {
width: 24px;
height: 24px;
border: none;
background: none;
font-size: 18px;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
font-weight: 300;
}
.emoji-close:hover {
background: #F0F0F0;
color: #666;
transform: scale(1.05);
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
padding: 16px;
max-height: 180px;
overflow-y: auto;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
.emoji-grid::-webkit-scrollbar {
display: none;
/* Chrome, Safari, Opera */
}
.emoji-item {
width: 36px;
height: 36px;
border: none;
background: none;
font-size: 20px;
cursor: pointer;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.emoji-item:hover {
background: #F5F5F5;
transform: scale(1.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.emoji-item:active {
transform: scale(1.05);
background: #E8F4FD;
} }
/* 自定义Naive UI样式 */ /* 自定义Naive UI样式 */
@ -155,12 +673,20 @@ defineExpose({
border: none !important; border: none !important;
padding: 0 !important; padding: 0 !important;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.6;
resize: none; resize: none;
} }
:deep(.n-input-wrapper) {
padding-left: 0 !important;
}
:deep(.n-input) {
padding-left: 0 !important;
}
:deep(.n-input .n-input__textarea::placeholder) { :deep(.n-input .n-input__textarea::placeholder) {
color: #999; color: #C2C2C2;
font-size: 14px; font-size: 14px;
} }
@ -170,17 +696,21 @@ defineExpose({
} }
:deep(.n-button.n-button--primary-type) { :deep(.n-button.n-button--primary-type) {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); background: #0088D1;
border: none; border: 1px solid #0088D1;
color: #FFF;
} }
:deep(.n-button.n-button--primary-type:not(.n-button--disabled):hover) { :deep(.n-button.n-button--primary-type:not(.n-button--disabled):hover) {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%); background: #0077C2;
border-color: #0077C2;
} }
:deep(.n-button.n-button--primary-type.n-button--disabled) { :deep(.n-button.n-button--primary-type.n-button--disabled) {
background: #d9d9d9; background: #0088D1;
color: #fff; border-color: #0088D1;
color: #FFF;
opacity: 0.5;
} }
/* 响应式设计 */ /* 响应式设计 */
@ -188,20 +718,25 @@ defineExpose({
.message-input-container { .message-input-container {
padding: 12px 16px; padding: 12px 16px;
} }
.input-wrapper { .action-btn {
gap: 8px; width: 28px;
height: 28px;
} }
.action-icon {
width: 14px;
height: 14px;
}
.send-button { .send-button {
height: 40px; height: 32px;
padding: 0 16px; padding: 0 12px;
font-size: 13px; font-size: 13px;
} }
.message-input { .message-input {
padding: 10px 14px; padding: 10px;
min-height: 40px;
font-size: 13px; font-size: 13px;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,7 @@
<!-- 系统图标 --> <!-- 系统图标 -->
<div class="avatar-container"> <div class="avatar-container">
<div class="system-avatar"> <div class="system-avatar">
<n-icon size="24" color="#1890ff"> <img src="/images/teacher/消息.png" alt="系统消息" class="system-icon" />
<NotificationsOutline />
</n-icon>
</div> </div>
</div> </div>
@ -28,13 +26,8 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<div class="message-text"> <div class="message-text">
{{ message.content }} {{ message.content }}
<n-button type="info" text icon-placement="right" style="margin-left: 6px;"> <n-button type="info" text class="detail-btn">
查看详情 查看详情>
<template #icon>
<NIcon>
<ChevronForward />
</NIcon>
</template>
</n-button> </n-button>
</div> </div>
</div> </div>
@ -51,8 +44,8 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination" v-if="messages.length > 0"> <div class="pagination" v-if="messages.length > 0">
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(1)">首页</button> <button class="page-btn nav-btn" :disabled="currentPage === 1" @click="goToPage(1)">首页</button>
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button> <button class="page-btn nav-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
<button v-for="page in visiblePages" :key="page" class="page-btn" :class="{ active: page === currentPage }" <button v-for="page in visiblePages" :key="page" class="page-btn" :class="{ active: page === currentPage }"
@click="goToPage(page)"> @click="goToPage(page)">
@ -62,8 +55,9 @@
<span v-if="showEllipsis" class="ellipsis">...</span> <span v-if="showEllipsis" class="ellipsis">...</span>
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button> <button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button> <button class="page-btn nav-btn" :disabled="currentPage === totalPages"
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button> @click="goToPage(currentPage + 1)">下一页</button>
<button class="page-btn nav-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
</div> </div>
</div> </div>
</template> </template>
@ -71,7 +65,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
import { NotificationsOutline, NotificationsOffOutline, ChevronForward } from '@vicons/ionicons5' import { NotificationsOffOutline } from '@vicons/ionicons5'
// //
interface SystemMessage { interface SystemMessage {
@ -181,13 +175,17 @@ const goToPage = (page: number) => {
.message-list { .message-list {
padding: 0; padding: 0;
display: flex;
flex-direction: column;
gap: 20px;
} }
.message-item { .message-item {
position: relative; position: relative;
display: flex; display: flex;
align-items: center;
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #f0f0f0; border: 1.5px solid #D8D8D8;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@ -208,14 +206,21 @@ const goToPage = (page: number) => {
} }
.system-avatar { .system-avatar {
width: 40px; width: 45px;
height: 40px; height: 45px;
border-radius: 50%; border-radius: 50%;
background-color: #e6f7ff; background-color: #0288D1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid #91d5ff; border: 1px solid #0288D1;
}
.system-icon {
width: 22px;
height: 22px;
transform: rotate(180deg);
object-fit: contain;
} }
.message-content { .message-content {
@ -227,7 +232,7 @@ const goToPage = (page: number) => {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 2px;
gap: 12px; gap: 12px;
} }
@ -266,13 +271,27 @@ const goToPage = (page: number) => {
} }
.message-text { .message-text {
color: #666; color: #999;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
margin-bottom: 12px;
word-break: break-word; word-break: break-word;
} }
.detail-btn {
margin-left: 10px;
font-size: 14px !important;
color: #0288D1 !important;
}
.detail-btn :deep(.n-button__content) {
color: #0288D1 !important;
font-size: 14px !important;
}
.detail-btn:hover :deep(.n-button__content) {
color: #0288D1 !important;
}
.message-actions { .message-actions {
display: flex; display: flex;
gap: 16px; gap: 16px;
@ -347,8 +366,8 @@ const goToPage = (page: number) => {
} }
.page-btn.active { .page-btn.active {
background: #1890ff; background: #0088D1;
border-color: #1890ff; border-color: #0088D1;
color: #fff; color: #fff;
} }
@ -359,6 +378,22 @@ const goToPage = (page: number) => {
background: #fafafa; background: #fafafa;
} }
.nav-btn {
border: none !important;
background: transparent !important;
}
.nav-btn:hover:not(:disabled) {
border: none !important;
background: #f5f5f5 !important;
color: #0088D1 !important;
}
.nav-btn:disabled {
border: none !important;
background: transparent !important;
color: #bfbfbf !important;
}
.ellipsis { .ellipsis {
color: #bfbfbf; color: #bfbfbf;
font-size: 14px; font-size: 14px;