feat:新即时消息页面初步实现,ai伴学判定图标,fixbug修复
This commit is contained in:
parent
e1b32c2c3c
commit
f4991929c6
656
src/components/InstantMessage.vue
Normal file
656
src/components/InstantMessage.vue
Normal 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>
|
@ -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',
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user