feat: 即时消息页面重构,新增表情,图片,文件功能,修复聊天区域布局问题,解决消息过多时输入框被挤下去的问题,调整系统消息页面
This commit is contained in:
parent
aa87b0e8e4
commit
935c68ac6d
BIN
public/images/teacher/file.png
Normal file
BIN
public/images/teacher/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/teacher/消息.png
Normal file
BIN
public/images/teacher/消息.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
@ -727,10 +727,6 @@ const updateActiveNavItem = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard {
|
||||
/* min-height: 100vh; */
|
||||
}
|
||||
|
||||
.top-image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -1174,3 +1170,5 @@ const updateActiveNavItem = () => {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
@ -217,7 +217,7 @@ const handleTabChange = (tabName: string) => {
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.message-tabs {
|
||||
|
@ -1,24 +1,73 @@
|
||||
<template>
|
||||
<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">
|
||||
<n-input
|
||||
v-model:value="messageText"
|
||||
type="textarea"
|
||||
:placeholder="placeholder"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||
:bordered="false"
|
||||
class="message-input"
|
||||
@keydown="handleKeyDown"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<div class="input-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="medium"
|
||||
:disabled="!canSend"
|
||||
class="send-button"
|
||||
@click="handleSend"
|
||||
>
|
||||
<n-input v-model:value="messageText" type="textarea" :placeholder="placeholder"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }" :bordered="false" class="message-input" @keydown="handleKeyDown"
|
||||
@input="handleInput" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-footer">
|
||||
<div class="action-buttons">
|
||||
<div class="emoji-button-wrapper">
|
||||
<button class="action-btn" :class="{ active: showEmojiPicker }" @click="toggleEmojiPicker">
|
||||
<img src="/images/teacher/expression.png" alt="表情" class="action-icon" />
|
||||
</button>
|
||||
<!-- 表情选择器从表情按钮上方弹出 -->
|
||||
<div v-if="showEmojiPicker" class="emoji-picker">
|
||||
<div class="emoji-picker-header">
|
||||
<span class="emoji-title">选择表情</span>
|
||||
<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>
|
||||
</div>
|
||||
@ -47,21 +96,186 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
const emit = defineEmits<{
|
||||
send: [message: 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 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(() => {
|
||||
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 = () => {
|
||||
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 = ''
|
||||
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({
|
||||
focus: () => {
|
||||
@ -95,58 +327,344 @@ defineExpose({
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
border: 1px solid #E0E0E0;
|
||||
padding: 16px 20px;
|
||||
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;
|
||||
align-items: center;
|
||||
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;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
height: 44px;
|
||||
padding: 0 24px;
|
||||
border-radius: 12px;
|
||||
.image-preview-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
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;
|
||||
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;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.send-button:not(:disabled):hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
/* 表情选择器样式 */
|
||||
.emoji-picker {
|
||||
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样式 */
|
||||
@ -155,12 +673,20 @@ defineExpose({
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
:deep(.n-input-wrapper) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.n-input .n-input__textarea::placeholder) {
|
||||
color: #999;
|
||||
color: #C2C2C2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -170,17 +696,21 @@ defineExpose({
|
||||
}
|
||||
|
||||
:deep(.n-button.n-button--primary-type) {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
border: none;
|
||||
background: #0088D1;
|
||||
border: 1px solid #0088D1;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
: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) {
|
||||
background: #d9d9d9;
|
||||
color: #fff;
|
||||
background: #0088D1;
|
||||
border-color: #0088D1;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@ -189,19 +719,24 @@ defineExpose({
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
gap: 8px;
|
||||
.action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
padding: 10px 14px;
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,9 +9,7 @@
|
||||
<!-- 系统图标 -->
|
||||
<div class="avatar-container">
|
||||
<div class="system-avatar">
|
||||
<n-icon size="24" color="#1890ff">
|
||||
<NotificationsOutline />
|
||||
</n-icon>
|
||||
<img src="/images/teacher/消息.png" alt="系统消息" class="system-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,13 +26,8 @@
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-text">
|
||||
{{ message.content }}
|
||||
<n-button type="info" text icon-placement="right" style="margin-left: 6px;">
|
||||
查看详情
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<ChevronForward />
|
||||
</NIcon>
|
||||
</template>
|
||||
<n-button type="info" text class="detail-btn">
|
||||
查看详情>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,8 +44,8 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" v-if="messages.length > 0">
|
||||
<button class="page-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(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 }"
|
||||
@click="goToPage(page)">
|
||||
@ -62,8 +55,9 @@
|
||||
<span v-if="showEllipsis" class="ellipsis">...</span>
|
||||
<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" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||
<button class="page-btn nav-btn" :disabled="currentPage === totalPages"
|
||||
@click="goToPage(currentPage + 1)">下一页</button>
|
||||
<button class="page-btn nav-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -71,7 +65,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { NotificationsOutline, NotificationsOffOutline, ChevronForward } from '@vicons/ionicons5'
|
||||
import { NotificationsOffOutline } from '@vicons/ionicons5'
|
||||
|
||||
// 系统消息类型定义
|
||||
interface SystemMessage {
|
||||
@ -181,13 +175,17 @@ const goToPage = (page: number) => {
|
||||
|
||||
.message-list {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
@ -208,14 +206,21 @@ const goToPage = (page: number) => {
|
||||
}
|
||||
|
||||
.system-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
background-color: #e6f7ff;
|
||||
background-color: #0288D1;
|
||||
display: flex;
|
||||
align-items: 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 {
|
||||
@ -227,7 +232,7 @@ const goToPage = (page: number) => {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 2px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@ -266,13 +271,27 @@ const goToPage = (page: number) => {
|
||||
}
|
||||
|
||||
.message-text {
|
||||
color: #666;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
@ -347,8 +366,8 @@ const goToPage = (page: number) => {
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
background: #0088D1;
|
||||
border-color: #0088D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@ -359,6 +378,22 @@ const goToPage = (page: number) => {
|
||||
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 {
|
||||
color: #bfbfbf;
|
||||
font-size: 14px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user