feat:新即时消息页面初步实现,ai伴学判定图标,fixbug修复

This commit is contained in:
小张 2025-09-06 02:18:07 +08:00
parent e1b32c2c3c
commit f4991929c6
7 changed files with 939 additions and 91 deletions

View File

@ -0,0 +1,656 @@
<template>
<div class="instant-message-container">
<!-- 对话列表 -->
<div class="conversation-list">
<div class="conversation-header">
<h3>对话</h3>
<div class="search-box">
<input type="text" placeholder="搜索对话" v-model="searchKeyword" />
<img src="/images/profile/message.png" alt="搜索" class="search-icon" />
</div>
</div>
<div class="conversation-items">
<div
v-for="conversation in filteredConversations"
:key="conversation.id"
:class="['conversation-item', { active: selectedConversation?.id === conversation.id }]"
@click="selectConversation(conversation)"
>
<div class="avatar-container">
<img :src="conversation.avatar" :alt="conversation.name" class="conversation-avatar" />
<div v-if="conversation.unreadCount > 0" class="unread-badge">{{ conversation.unreadCount }}</div>
</div>
<div class="conversation-info">
<div class="conversation-name">{{ conversation.name }}</div>
<div class="last-message">{{ conversation.lastMessage }}</div>
</div>
<div class="conversation-meta">
<div class="last-time">{{ conversation.lastTime }}</div>
<div v-if="conversation.isOnline" class="online-indicator"></div>
</div>
</div>
</div>
</div>
<!-- 聊天区域 -->
<div class="chat-area">
<div v-if="!selectedConversation" class="no-conversation">
<img src="/images/profile/message.png" alt="选择对话" class="placeholder-image" />
<p>选择一个对话开始聊天</p>
</div>
<div v-else class="chat-container">
<!-- 聊天头部 -->
<div class="chat-header">
<div class="chat-user-info">
<img :src="selectedConversation.avatar" :alt="selectedConversation.name" class="chat-avatar" />
<div class="chat-user-details">
<div class="chat-user-name">{{ selectedConversation.name }}</div>
<div class="chat-user-status">{{ selectedConversation.isOnline ? '在线' : '离线' }}</div>
</div>
</div>
<div class="chat-actions">
<button class="chat-action-btn">
<img src="/images/profile/message.png" alt="视频通话" />
</button>
<button class="chat-action-btn">
<img src="/images/profile/message.png" alt="语音通话" />
</button>
<button class="chat-action-btn">
<img src="/images/profile/message.png" alt="更多" />
</button>
</div>
</div>
<!-- 消息列表 -->
<div class="messages-container" ref="messagesContainer">
<div v-for="message in selectedConversation.messages" :key="message.id"
:class="['message-item', message.isSelf ? 'message-self' : 'message-other']">
<div v-if="!message.isSelf" class="message-avatar">
<img :src="selectedConversation.avatar" :alt="selectedConversation.name" />
</div>
<div class="message-content">
<div class="message-bubble">
<div v-if="message.type === 'text'" class="message-text">{{ message.content }}</div>
<div v-else-if="message.type === 'image'" class="message-image">
<img :src="message.content" alt="图片" />
</div>
<div v-else-if="message.type === 'file'" class="message-file">
<img src="/images/profile/message.png" alt="文件" />
<span>{{ message.fileName }}</span>
</div>
</div>
<div class="message-time">{{ message.time }}</div>
</div>
<div v-if="message.isSelf" class="message-avatar">
<img src="/images/profile/profile.png" alt="我" />
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="input-toolbar">
<button class="toolbar-btn" @click="showEmojiPicker = !showEmojiPicker">
<img src="/images/profile/message.png" alt="表情" />
</button>
<button class="toolbar-btn" @click="selectFile">
<img src="/images/profile/message.png" alt="附件" />
</button>
<button class="toolbar-btn">
<img src="/images/profile/message.png" alt="图片" />
</button>
</div>
<div class="input-container">
<textarea
v-model="messageInput"
placeholder="输入消息..."
@keydown.enter.prevent="sendMessage"
@input="adjustTextareaHeight"
ref="messageTextarea"
rows="1"
></textarea>
<button class="send-btn" @click="sendMessage" :disabled="!messageInput.trim()">
发送
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted } from 'vue'
//
interface Message {
id: number
type: 'text' | 'image' | 'file'
content: string
fileName?: string
time: string
isSelf: boolean
}
//
interface Conversation {
id: number
name: string
avatar: string
lastMessage: string
lastTime: string
unreadCount: number
isOnline: boolean
messages: Message[]
}
//
const searchKeyword = ref('')
const selectedConversation = ref<Conversation | null>(null)
const messageInput = ref('')
const showEmojiPicker = ref(false)
const messagesContainer = ref<HTMLElement>()
const messageTextarea = ref<HTMLTextAreaElement>()
//
const conversations = ref<Conversation[]>([
{
id: 1,
name: '张老师',
avatar: '/images/traings/traing1.png',
lastMessage: '好的,我知道了',
lastTime: '14:30',
unreadCount: 2,
isOnline: true,
messages: [
{ id: 1, type: 'text', content: '你好,有什么问题吗?', time: '14:25', isSelf: false },
{ id: 2, type: 'text', content: '我想问一下关于课程的问题', time: '14:26', isSelf: true },
{ id: 3, type: 'text', content: '好的,请说', time: '14:27', isSelf: false },
{ id: 4, type: 'text', content: '这个作业的截止时间是什么时候?', time: '14:28', isSelf: true },
{ id: 5, type: 'text', content: '下周五晚上12点前提交', time: '14:29', isSelf: false },
{ id: 6, type: 'text', content: '好的,我知道了', time: '14:30', isSelf: true }
]
},
{
id: 2,
name: '李同学',
avatar: '/images/traings/traing2.png',
lastMessage: '谢谢你的帮助',
lastTime: '昨天',
unreadCount: 0,
isOnline: false,
messages: [
{ id: 1, type: 'text', content: '能帮我看看这道题吗?', time: '昨天 16:20', isSelf: false },
{ id: 2, type: 'text', content: '当然可以,发过来看看', time: '昨天 16:21', isSelf: true },
{ id: 3, type: 'image', content: '/images/homework/question1.png', time: '昨天 16:22', isSelf: false },
{ id: 4, type: 'text', content: '这道题需要用到递归的思想...', time: '昨天 16:25', isSelf: true },
{ id: 5, type: 'text', content: '谢谢你的帮助', time: '昨天 16:30', isSelf: false }
]
},
{
id: 3,
name: '王助教',
avatar: '/images/traings/traing3.png',
lastMessage: '课程资料已发送',
lastTime: '2天前',
unreadCount: 1,
isOnline: true,
messages: [
{ id: 1, type: 'text', content: '你好,这是本周的课程资料', time: '2天前 10:00', isSelf: false },
{ id: 2, type: 'file', content: '/files/course-material.pdf', fileName: '第三章课程资料.pdf', time: '2天前 10:01', isSelf: false },
{ id: 3, type: 'text', content: '课程资料已发送', time: '2天前 10:02', isSelf: false }
]
}
])
//
const filteredConversations = computed(() => {
if (!searchKeyword.value) return conversations.value
return conversations.value.filter(conv =>
conv.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
//
const selectConversation = (conversation: Conversation) => {
selectedConversation.value = conversation
//
conversation.unreadCount = 0
//
nextTick(() => {
scrollToBottom()
})
}
//
const sendMessage = () => {
if (!messageInput.value.trim() || !selectedConversation.value) return
const newMessage: Message = {
id: Date.now(),
type: 'text',
content: messageInput.value.trim(),
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
isSelf: true
}
selectedConversation.value.messages.push(newMessage)
selectedConversation.value.lastMessage = newMessage.content
selectedConversation.value.lastTime = newMessage.time
messageInput.value = ''
//
nextTick(() => {
scrollToBottom()
resetTextareaHeight()
})
}
//
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
//
const adjustTextareaHeight = () => {
if (messageTextarea.value) {
messageTextarea.value.style.height = 'auto'
messageTextarea.value.style.height = Math.min(messageTextarea.value.scrollHeight, 120) + 'px'
}
}
//
const resetTextareaHeight = () => {
if (messageTextarea.value) {
messageTextarea.value.style.height = 'auto'
}
}
//
const selectFile = () => {
//
console.log('选择文件')
}
//
onMounted(() => {
if (conversations.value.length > 0) {
selectConversation(conversations.value[0])
}
})
</script>
<style scoped>
.instant-message-container {
display: flex;
height: 600px;
border: 1px solid #e6e6e6;
border-radius: 8px;
overflow: hidden;
}
/* 对话列表样式 */
.conversation-list {
width: 300px;
border-right: 1px solid #e6e6e6;
display: flex;
flex-direction: column;
}
.conversation-header {
padding: 16px;
border-bottom: 1px solid #e6e6e6;
}
.conversation-header h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
}
.search-box {
position: relative;
}
.search-box input {
width: 100%;
padding: 8px 32px 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
}
.search-icon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
}
.conversation-items {
flex: 1;
overflow-y: auto;
}
.conversation-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.conversation-item:hover {
background-color: #f5f5f5;
}
.conversation-item.active {
background-color: #e6f7ff;
}
.avatar-container {
position: relative;
margin-right: 12px;
}
.conversation-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.unread-badge {
position: absolute;
top: -4px;
right: -4px;
background: #ff4d4f;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
.conversation-info {
flex: 1;
min-width: 0;
}
.conversation-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.last-message {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.last-time {
font-size: 12px;
color: #999;
}
.online-indicator {
width: 8px;
height: 8px;
background: #52c41a;
border-radius: 50%;
}
/* 聊天区域样式 */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.no-conversation {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.placeholder-image {
width: 120px;
height: 120px;
margin-bottom: 16px;
opacity: 0.5;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e6e6e6;
}
.chat-user-info {
display: flex;
align-items: center;
gap: 12px;
}
.chat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.chat-user-name {
font-size: 16px;
font-weight: 500;
}
.chat-user-status {
font-size: 12px;
color: #52c41a;
}
.chat-actions {
display: flex;
gap: 8px;
}
.chat-action-btn {
width: 32px;
height: 32px;
border: none;
background: none;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.chat-action-btn:hover {
background-color: #f5f5f5;
}
.chat-action-btn img {
width: 20px;
height: 20px;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message-item {
display: flex;
gap: 8px;
}
.message-item.message-self {
flex-direction: row-reverse;
}
.message-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.message-content {
max-width: 60%;
}
.message-self .message-content {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message-bubble {
padding: 8px 12px;
border-radius: 12px;
word-wrap: break-word;
}
.message-other .message-bubble {
background-color: #f0f0f0;
border-bottom-left-radius: 4px;
}
.message-self .message-bubble {
background-color: #1890ff;
color: white;
border-bottom-right-radius: 4px;
}
.message-text {
line-height: 1.4;
}
.message-image img {
max-width: 200px;
border-radius: 8px;
}
.message-file {
display: flex;
align-items: center;
gap: 8px;
}
.message-file img {
width: 24px;
height: 24px;
}
.message-time {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.input-area {
border-top: 1px solid #e6e6e6;
padding: 16px;
}
.input-toolbar {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.toolbar-btn {
width: 32px;
height: 32px;
border: none;
background: none;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.toolbar-btn:hover {
background-color: #f5f5f5;
}
.toolbar-btn img {
width: 20px;
height: 20px;
}
.input-container {
display: flex;
gap: 8px;
align-items: flex-end;
}
.input-container textarea {
flex: 1;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
resize: none;
font-family: inherit;
font-size: 14px;
line-height: 1.4;
min-height: 36px;
max-height: 120px;
}
.send-btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
height: 36px;
}
.send-btn:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
.send-btn:not(:disabled):hover {
background-color: #40a9ff;
}
</style>

View File

@ -36,7 +36,6 @@ import PersonalCenter from '@/components/admin/PersonalCenter.vue'
import CourseManagement from '@/components/admin/CourseManagement.vue'
import MyResources from '@/components/admin/MyResources.vue'
import StudentManagement from '@/components/admin/StudentManagement.vue'
import MessageCenter from '@/views/teacher/message/MessageCenter.vue'
// 课程管理子组件
import CourseCategory from '@/components/admin/CourseComponents/CourseCategory.vue'
@ -83,7 +82,6 @@ import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue'
import ExamNoticeBeforeStart from '@/views/teacher/ExamPages/ExamNoticeBeforeStart.vue'
import ExamAnalysis from '@/views/teacher/ExamPages/ExamAnalysis.vue'
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
@ -311,12 +309,6 @@ const routes: RouteRecordRaw[] = [
component: MyResources,
meta: { title: '我的资源' }
},
{
path: 'message-center',
name: 'MessageCenter',
component: MessageCenter,
meta: { title: '消息中心' }
},
{
path: 'student-management',
name: 'StudentManagement',
@ -387,12 +379,6 @@ const routes: RouteRecordRaw[] = [
component: ExamLibrary,
meta: { title: '试卷管理' }
},
{
path: 'exam-analysis',
name: 'ExamAnalysis',
component: ExamAnalysis,
meta: { title: '试卷分析' }
},
{
path: 'marking-center',
name: 'MarkingCenter',

View File

@ -29,14 +29,14 @@
<div class="banner-content">
<div class="banner-text">
<span class="main-text">暑期名师领学,提高班级教学质量!高效冲分指南</span>
<div class="ai-companion-tag">
<div v-if="(course as any)?.izAi === 1" class="ai-companion-tag">
<img src="/images/aiCompanion/AI伴学标签@2x.png" alt="AI伴学" class="tag-image">
</div>
</div>
<div class="banner-button">
<!-- <div class="banner-button">
<img src="/images/aiCompanion/切换@2x.png" alt="切换" class="button-icon-image">
<span class="button-text">普通</span>
</div>
</div> -->
</div>
</div>

View File

@ -36,7 +36,7 @@
<div class="banner-content">
<div class="banner-text">
<span class="main-text">暑期名师领学,提高班级教学质量!高效冲分指南</span>
<div class="ai-companion-tag">
<div v-if="(course as any)?.izAi === 1" class="ai-companion-tag">
<img src="/images/aiCompanion/AI伴学标签@2x.png" alt="AI伴学" class="tag-image">
</div>
</div>

View File

@ -719,11 +719,21 @@
<!-- 消息筛选标签 -->
<div class="message-header">
<div class="message-tabs">
<span class="message-tab-item" :class="{ active: activeMessageTab === 'all' }"
@click="handleMessageTabChange('all')">
评论
<span class="message-tab-item" :class="{ active: activeMessageTab === 'instant' }"
@click="handleMessageTabChange('instant')">
即时消息
<span class="message-count">2</span>
</span>
<span class="message-tab-item" :class="{ active: activeMessageTab === 'comment' }"
@click="handleMessageTabChange('comment')">
评论和@
<span class="message-count">3</span>
</span>
<span class="message-tab-item" :class="{ active: activeMessageTab === 'like' }"
@click="handleMessageTabChange('like')">
点赞
<span class="message-count">5</span>
</span>
<span class="message-tab-item" :class="{ active: activeMessageTab === 'system' }"
@click="handleMessageTabChange('system')">
系统消息
@ -743,8 +753,13 @@
<!-- 消息列表 -->
<div class="message-list">
<!-- 普通消息 -->
<div v-for="msg in filteredMessages" :key="msg.id" class="message-item">
<!-- 即时消息 -->
<div v-if="activeMessageTab === 'instant'" class="instant-message-container">
<InstantMessage />
</div>
<!-- 评论和@消息 -->
<div v-else-if="activeMessageTab === 'comment'" v-for="msg in filteredMessages" :key="msg.id" class="message-item">
<!-- 未读标识 - 移到右上角 -->
<div v-if="!msg.isRead" class="unread-indicator-top-right"></div>
@ -801,8 +816,35 @@
</div>
</div>
<!-- 点赞消息 -->
<div v-else-if="activeMessageTab === 'like'" v-for="like in filteredLikeMessages" :key="'like-' + like.id" class="like-message-item">
<!-- 未读标识 -->
<div v-if="!like.isRead" class="unread-indicator-top-right"></div>
<!-- 点赞消息内容 -->
<div class="like-message-main">
<div class="like-message-user">
<img :src="like.userAvatar" class="image_22" />
<div class="like-info">
<div class="like-content">
<span class="message-text">{{ like.userName }}</span>
<span class="message-text">赞了我的{{ like.type === 'comment' ? '评论' : '课程' }}</span>
</div>
<div class="course-info-container" v-if="like.courseName">
<span class="course-label">在课程</span>
<span class="course-name">{{ like.courseName }}</span>
</div>
<div class="like-content-preview" v-if="like.content">
<span class="content-preview">{{ like.content }}</span>
</div>
</div>
<div class="message-time">{{ like.date }}</div>
</div>
</div>
</div>
<!-- 系统消息 -->
<div v-for="sysMsg in filteredSystemMessages" :key="'sys-' + sysMsg.id" class="system-message-item">
<div v-else-if="activeMessageTab === 'system'" v-for="sysMsg in filteredSystemMessages" :key="'sys-' + sysMsg.id" class="system-message-item">
<!-- 未读标识 - 移到右上角 -->
<!-- <div v-if="!sysMsg.isRead" class="unread-indicator-top-right"></div> -->
@ -1074,6 +1116,7 @@ import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import QuillEditor from '@/components/common/QuillEditor.vue'
import InstantMessage from '@/components/InstantMessage.vue'
import { useRouter, useRoute } from 'vue-router'
const { t, locale } = useI18n()
@ -1228,6 +1271,18 @@ interface SystemMessage {
isRead: boolean
}
//
interface LikeMessage {
id: number
userName: string
userAvatar: string
type: 'comment' | 'course'
courseName?: string
content?: string
date: string
isRead: boolean
}
const message = useMessage()
@ -1255,7 +1310,7 @@ const activeActivityTab = ref('all')
const activeFollowsTab = ref('all')
//
const activeMessageTab = ref('all')
const activeMessageTab = ref('instant')
const replyingMessageId = ref<number | null>(null)
const replyContent = ref('')
@ -2074,6 +2129,60 @@ const mockSystemMessages: SystemMessage[] = [
}
]
//
const mockLikeMessages: LikeMessage[] = [
{
id: 1,
userName: '张同学',
userAvatar: '/images/traings/traing1.png',
type: 'comment',
courseName: 'Python语言基础与应用',
content: '这个课程讲得很好,老师很专业',
date: '7月21日 14:30',
isRead: false
},
{
id: 2,
userName: '李老师',
userAvatar: '/images/traings/traing2.png',
type: 'course',
courseName: 'Java程序设计',
content: '',
date: '7月21日 10:15',
isRead: false
},
{
id: 3,
userName: '王同学',
userAvatar: '/images/traings/traing3.png',
type: 'comment',
courseName: 'C语言程序设计',
content: '感谢老师的耐心解答',
date: '7月20日 16:45',
isRead: true
},
{
id: 4,
userName: '刘同学',
userAvatar: '/images/traings/traing1.png',
type: 'comment',
courseName: 'JavaScript基础',
content: '这个例子很实用',
date: '7月20日 09:20',
isRead: true
},
{
id: 5,
userName: '陈老师',
userAvatar: '/images/traings/traing2.png',
type: 'course',
courseName: 'HTML5与CSS3',
content: '',
date: '7月19日 18:30',
isRead: true
}
]
//
// const mockPracticeDetails: { [key: number]: PracticeDetail } = {
// 2: {
@ -2593,14 +2702,10 @@ const sendReply = (messageId: number) => {
//
const filteredMessages = computed(() => {
if (activeMessageTab.value === 'system') {
return [] //
} else if (activeMessageTab.value === 'unread') {
return mockMessages.filter(msg => !msg.isRead)
} else if (activeMessageTab.value === 'read') {
return mockMessages.filter(msg => msg.isRead)
}
if (activeMessageTab.value === 'comment') {
return mockMessages
}
return []
})
//
@ -2611,6 +2716,14 @@ const filteredSystemMessages = computed(() => {
return []
})
//
const filteredLikeMessages = computed(() => {
if (activeMessageTab.value === 'like') {
return mockLikeMessages
}
return []
})
//
const getTabTitle = (tab: TabType) => {
const titles: Record<TabType, string> = {
@ -6389,6 +6502,55 @@ onActivated(() => {
/* 微调垂直位置 */
}
/* 点赞消息样式 */
.like-message-item {
position: relative;
background: white;
border: 1px solid #D8D8D8;
transition: border-color 0.3s ease;
}
.like-message-item:hover {
border-color: #1890ff;
}
.like-message-main {
padding: 1.04vh 1.04vw;
}
.like-message-user {
display: flex;
align-items: flex-start;
gap: 0.73vw;
margin-bottom: 0.73vh;
}
.like-info {
flex: 1;
min-width: 0;
}
.like-content {
display: flex;
align-items: center;
gap: 0.26vw;
margin-bottom: 0.52vh;
}
.like-content-preview {
margin-top: 0.52vh;
padding: 0.52vh 0.73vw;
background-color: #f5f5f5;
border-radius: 4px;
border-left: 3px solid #1890ff;
}
.content-preview {
font-size: 0.73vw;
color: #666;
font-style: italic;
}
/* 系统消息样式 */
.system-message-item {
position: relative;

View File

@ -548,6 +548,7 @@ const createNewQuestion = async (bankId: string) => {
};
//
const validateAnswers = (): boolean => {
console.log(questionForm);
@ -563,7 +564,7 @@ const validateAnswers = (): boolean => {
message.error('单选题至少需要2个选项');
return false;
}
if (questionForm.options.some((option: any) => !option.content.trim())) {
if (questionForm.options.some(option => !option.content.trim())) {
message.error('请填写所有选项的内容');
return false;
}
@ -578,7 +579,7 @@ const validateAnswers = (): boolean => {
message.error('多选题至少需要2个选项');
return false;
}
if (questionForm.options.some((option: any) => !option.content.trim())) {
if (questionForm.options.some(option => !option.content.trim())) {
message.error('请填写所有选项的内容');
return false;
}
@ -594,7 +595,7 @@ const validateAnswers = (): boolean => {
}
break;
case 'fill_blank':
if (questionForm.fillBlankAnswers.length === 0 || questionForm.fillBlankAnswers.every((answer: any) => !answer.content.trim())) {
if (questionForm.fillBlankAnswers.length === 0 || questionForm.fillBlankAnswers.every(answer => !answer.content.trim())) {
message.error('请设置填空题的参考答案');
return false;
}

View File

@ -3,9 +3,7 @@
<div class="breadcrumb-section">
<n-breadcrumb>
<n-breadcrumb-item @click="goToQuestionBank">
<n-icon>
<ChevronBackOutline />
</n-icon>
<n-icon><ChevronBackOutline /></n-icon>
题库管理
</n-breadcrumb-item>
<n-breadcrumb-item>{{ currentBankTitle }}</n-breadcrumb-item>
@ -18,27 +16,54 @@
<n-button type="primary" @click="addQuestion">添加试题</n-button>
<n-button ghost @click="importQuestions">导入</n-button>
<n-button ghost @click="exportQuestions">导出</n-button>
<n-button type="error" ghost @click="deleteSelected"
:disabled="selectedRowKeys.length === 0">删除</n-button>
<n-button type="error" ghost @click="deleteSelected" :disabled="selectedRowKeys.length === 0">删除</n-button>
<n-button @click="setCategoryForSelected" :disabled="selectedRowKeys.length === 0">分类设置</n-button>
<n-select v-model:value="filters.category" placeholder="分类"
:options="[{ label: '全部', value: '' }, ...allCategoryOptions]" style="width: 120px"
@update:value="handleFilterChange" />
<n-input v-model:value="filters.keyword" placeholder="请输入想要搜索的内容" style="width: 200px" clearable />
<n-select
v-model:value="filters.category"
placeholder="分类"
:options="[{ label: '全部', value: '' }, ...allCategoryOptions]"
style="width: 120px"
@update:value="handleFilterChange"
/>
<n-input
v-model:value="filters.keyword"
placeholder="请输入想要搜索的内容"
style="width: 200px"
clearable
/>
<n-button type="primary" @click="searchQuestions">搜索</n-button>
</n-space>
</div>
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :loading="loading"
:pagination="paginationConfig" :row-key="(row: any) => row.id" :checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck" class="question-table" :single-line="false" />
<n-data-table
ref="tableRef"
:columns="columns"
:data="questionList"
:loading="loading"
:pagination="paginationConfig"
:row-key="(row: Question) => row.id"
:checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck"
class="question-table"
:single-line="false"
/>
<!-- 导入弹窗 -->
<ImportModal v-model:show="showImportModal" template-name="question_template.xlsx" import-type="question"
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
<ImportModal
v-model:show="showImportModal"
template-name="question_template.xlsx"
import-type="question"
@success="handleImportSuccess"
@template-download="handleTemplateDownload"
/>
<!-- 分类设置弹窗 -->
<n-modal v-model:show="showCategoryModal" preset="dialog" title="分类设置" style="width: 500px;">
<n-modal
v-model:show="showCategoryModal"
preset="dialog"
title="分类设置"
style="width: 500px;"
>
<div class="category-modal-content">
<div class="selected-info">
<n-alert type="info" :show-icon="false" style="margin-bottom: 16px;">
@ -49,15 +74,23 @@
<div class="category-selection">
<div class="form-item">
<label>选择分类</label>
<n-select v-model:value="selectedCategory" :options="allCategoryOptions" placeholder="请选择分类"
style="width: 100%;" />
<n-select
v-model:value="selectedCategory"
:options="allCategoryOptions"
placeholder="请选择分类"
style="width: 100%;"
/>
</div>
<div v-if="showAddCategoryInput" class="form-item">
<label>新分类名称</label>
<n-space>
<n-input v-model:value="newCategoryName" placeholder="请输入新分类名称" style="width: 200px;"
@keyup.enter="addNewCategory" />
<n-input
v-model:value="newCategoryName"
placeholder="请输入新分类名称"
style="width: 200px;"
@keyup.enter="addNewCategory"
/>
<n-button type="primary" @click="addNewCategory" :disabled="!newCategoryName.trim()">
添加
</n-button>
@ -80,7 +113,12 @@
</n-modal>
<!-- 分类管理弹窗 -->
<n-modal v-model:show="showCategoryManageModal" preset="dialog" title="分类管理" style="width: 600px;">
<n-modal
v-model:show="showCategoryManageModal"
preset="dialog"
title="分类管理"
style="width: 600px;"
>
<div class="category-manage-content">
<div class="category-header">
<n-space>
@ -92,8 +130,12 @@
<div v-if="showAddCategoryInManage" class="add-category-section">
<n-space>
<n-input v-model:value="newCategoryInManage" placeholder="请输入分类名称" style="width: 200px;"
@keyup.enter="addCategoryInManage" />
<n-input
v-model:value="newCategoryInManage"
placeholder="请输入分类名称"
style="width: 200px;"
@keyup.enter="addCategoryInManage"
/>
<n-button type="primary" @click="addCategoryInManage" :disabled="!newCategoryInManage.trim()">
添加
</n-button>
@ -168,8 +210,8 @@ interface Question {
score: number;
creator: string;
createTime: string;
parentId?: string; // parentId
analysis?: string; // analysis
parentId?: string; // ID
analysis?: string; //
}
//
@ -293,7 +335,7 @@ const createColumns = ({
width: 100,
align: 'center' as const,
render(row: Question) {
const categoryInfo = allCategoryOptions.value.find((cat: any) => cat.value === row.category);
const categoryInfo = allCategoryOptions.value.find(cat => cat.value === row.category);
return categoryInfo ? categoryInfo.label : row.category;
}
},
@ -380,7 +422,7 @@ const generateMockData = (): Question[] => {
const mockData: Question[] = [];
const types = ['single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'short_answer'];
const difficulties = ['easy', 'medium', 'hard'];
const categories = customCategories.value.map((cat: any) => cat.value);
const categories = customCategories.value.map(cat => cat.value);
const creators = ['王建国', '李明', '张三', '刘老师'];
for (let i = 1; i <= 50; i++) {
@ -452,9 +494,10 @@ const loadQuestions = async () => {
let allData: Question[] = [];
// API
if (response.data && typeof response.data === 'object' && 'code' in response.data && (response.data.code === 200 || response.data.code === 0) && 'result' in response.data && response.data.result && Array.isArray(response.data.result)) {
const apiResponse = response.data as any;
if (apiResponse && (apiResponse.code === 200 || apiResponse.code === 0) && apiResponse.result) {
// API
allData = response.data.result.map((item: any, index: number) => ({
allData = apiResponse.result.map((item: any, index: number) => ({
id: item.id || `question_${index}`,
sequence: index + 1,
title: item.content || '题目内容',
@ -706,7 +749,7 @@ const addNewCategory = () => {
}
//
const exists = customCategories.value.some((cat: any) => cat.label === trimmedName);
const exists = customCategories.value.some(cat => cat.label === trimmedName);
if (exists) {
message.warning('该分类已存在');
return;
@ -735,8 +778,8 @@ const applyCategoryChange = async () => {
await new Promise(resolve => setTimeout(resolve, 300));
//
const selectedCategoryLabel = allCategoryOptions.value.find((cat: any) => cat.value === selectedCategory.value)?.label || selectedCategory.value;
questionList.value.forEach((question: any) => {
const selectedCategoryLabel = allCategoryOptions.value.find(cat => cat.value === selectedCategory.value)?.label || selectedCategory.value;
questionList.value.forEach(question => {
if (selectedRowKeys.value.includes(question.id)) {
question.category = selectedCategory.value;
}
@ -771,7 +814,7 @@ const addCategoryInManage = () => {
}
//
const exists = customCategories.value.some((cat: any) => cat.label === trimmedName);
const exists = customCategories.value.some(cat => cat.label === trimmedName);
if (exists) {
message.warning('该分类已存在');
return;
@ -801,13 +844,13 @@ const editCategory = (category: { label: string; value: string }) => {
const deleteCategory = (categoryValue: string) => {
// 使
const hasQuestions = questionList.value.some((q: any) => q.category === categoryValue);
const hasQuestions = questionList.value.some(q => q.category === categoryValue);
if (hasQuestions) {
message.warning('该分类下还有试题,不能删除');
return;
}
const index = customCategories.value.findIndex((cat: any) => cat.value === categoryValue);
const index = customCategories.value.findIndex(cat => cat.value === categoryValue);
if (index > -1) {
const categoryName = customCategories.value[index].label;
customCategories.value.splice(index, 1);