feat: 对接我的会话,查询会话消息,查询群聊成员列表,查询课程评论接口;添加会话消息空状态样式,添加证书中心内容样式,修复班级管理组件删除学员功能
This commit is contained in:
parent
ad2ec33c6a
commit
ef49c7b6d3
@ -12,6 +12,7 @@ export { default as OrderApi } from './modules/order'
|
||||
export { default as UploadApi } from './modules/upload'
|
||||
export { default as StatisticsApi } from './modules/statistics'
|
||||
export { default as ExamApi } from './modules/exam'
|
||||
export { ChatApi } from './modules/chat'
|
||||
|
||||
// API 基础配置
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
|
||||
@ -221,6 +222,17 @@ export const API_ENDPOINTS = {
|
||||
PROGRESS: '/learning-progress',
|
||||
MY_COURSES: '/my-courses',
|
||||
},
|
||||
|
||||
// 聊天相关
|
||||
CHAT: {
|
||||
MY_CHATS: '/aiol/aiolChat/my_chats',
|
||||
MESSAGES: '/aiol/aiolChat/:chatId/messages',
|
||||
MEMBERS: '/aiol/aiolChat/:chatId/members',
|
||||
SEND: '/aiol/aiolChat/send',
|
||||
MARK_READ: '/aiol/aiolChat/mark-read',
|
||||
UNREAD_COUNT: '/aiol/aiolChat/unread-count',
|
||||
FOLLOW: '/aiol/aiolUserFollow/follow',
|
||||
},
|
||||
|
||||
// 资源相关
|
||||
RESOURCES: {
|
||||
|
177
src/api/modules/chat.ts
Normal file
177
src/api/modules/chat.ts
Normal file
@ -0,0 +1,177 @@
|
||||
// 聊天相关API接口
|
||||
import { ApiRequest } from '../request'
|
||||
import type { ApiResponse } from '../types'
|
||||
|
||||
// 聊天会话接口类型定义
|
||||
export interface ChatSession {
|
||||
id: string
|
||||
name: string
|
||||
type: number // 0=私聊,1=群聊
|
||||
avatar?: string
|
||||
lastMessage?: string
|
||||
lastMessageTime?: string
|
||||
unreadCount?: number
|
||||
isOnline?: boolean
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
// 根据实际API响应添加的字段
|
||||
refId?: string // 关联ID
|
||||
createBy?: string
|
||||
updateBy?: string
|
||||
izAllMuted?: boolean
|
||||
showLabel?: string
|
||||
memberCount?: number // 群成员数量
|
||||
status?: number // 会话状态
|
||||
description?: string // 会话描述
|
||||
}
|
||||
|
||||
// 聊天消息接口类型定义
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
chatId: string
|
||||
senderId: string
|
||||
senderName: string
|
||||
senderAvatar?: string
|
||||
content: string
|
||||
messageType: 'text' | 'image' | 'file' | 'system'
|
||||
timestamp: string
|
||||
isRead: boolean
|
||||
replyTo?: string
|
||||
// 根据接口文档优化的字段
|
||||
status?: number // 消息状态
|
||||
fileUrl?: string // 文件URL
|
||||
fileSize?: number // 文件大小
|
||||
fileType?: string // 文件类型
|
||||
}
|
||||
|
||||
// 群聊成员接口类型定义
|
||||
export interface ChatMember {
|
||||
id: string
|
||||
chatId: string
|
||||
userId: string
|
||||
userName: string
|
||||
userAvatar?: string
|
||||
role: 'admin' | 'member'
|
||||
joinTime: string
|
||||
isOnline?: boolean
|
||||
// 根据接口文档优化的字段
|
||||
status?: number // 成员状态
|
||||
lastActiveTime?: string // 最后活跃时间
|
||||
}
|
||||
|
||||
// 我的会话列表响应类型
|
||||
export interface MyChatsResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: ChatSession[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 会话消息列表响应类型
|
||||
export interface ChatMessagesResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: ChatMessage[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 群聊成员列表响应类型
|
||||
export interface ChatMembersResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: ChatMember[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 分页查询参数
|
||||
export interface ChatQueryParams {
|
||||
page?: number
|
||||
size?: number
|
||||
keyword?: string
|
||||
type?: 'private' | 'group'
|
||||
status?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天相关API
|
||||
*/
|
||||
export const ChatApi = {
|
||||
/**
|
||||
* 获取我的会话列表
|
||||
* GET /aiol/aiolChat/my_chats
|
||||
* 支持分页和筛选参数
|
||||
*/
|
||||
getMyChats: (params?: ChatQueryParams): Promise<ApiResponse<MyChatsResponse>> => {
|
||||
return ApiRequest.get('/aiol/aiolChat/my_chats', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询会话消息
|
||||
* GET /aiol/aiolChat/{chatId}/messages
|
||||
* 支持分页加载历史消息
|
||||
*/
|
||||
getChatMessages: (chatId: string, params?: ChatQueryParams): Promise<ApiResponse<ChatMessagesResponse>> => {
|
||||
console.log('🔍 调用getChatMessages接口,chatId:', chatId, 'params:', params)
|
||||
return ApiRequest.get(`/aiol/aiolChat/${chatId}/messages`, { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询群聊成员列表
|
||||
* GET /aiol/aiolChat/{chatId}/members
|
||||
*/
|
||||
getChatMembers: (chatId: string): Promise<ApiResponse<ChatMembersResponse>> => {
|
||||
return ApiRequest.get(`/aiol/aiolChat/${chatId}/members`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 关注/取关教师
|
||||
* POST /aiol/aiolUserFollow/follow
|
||||
*/
|
||||
followTeacher: (followedId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post('/aiol/aiolUserFollow/follow', {
|
||||
followedId
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* POST /aiol/aiolChat/send
|
||||
* 根据接口文档添加发送消息功能
|
||||
*/
|
||||
sendMessage: (data: {
|
||||
chatId: string
|
||||
content: string
|
||||
messageType: 'text' | 'image' | 'file'
|
||||
replyTo?: string
|
||||
fileUrl?: string
|
||||
fileSize?: number
|
||||
fileType?: string
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post('/aiol/aiolChat/send', data)
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 标记消息为已读
|
||||
* PUT /aiol/aiolChat/mark-read
|
||||
* 根据接口文档添加标记已读功能
|
||||
*/
|
||||
markAsRead: (chatId: string, messageIds?: string[]): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.put('/aiol/aiolChat/mark-read', {
|
||||
chatId,
|
||||
messageIds
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
* GET /aiol/aiolChat/unread-count
|
||||
* 根据接口文档添加未读消息统计
|
||||
*/
|
||||
getUnreadCount: (): Promise<ApiResponse<{ total: number; chats: { chatId: string; count: number }[] }>> => {
|
||||
return ApiRequest.get('/aiol/aiolChat/unread-count')
|
||||
}
|
||||
}
|
@ -489,7 +489,7 @@ export class ClassApi {
|
||||
* 删除班级内学生
|
||||
*/
|
||||
static async removeStudent(classId: string, studentId: string): Promise<ApiResponse<any>> {
|
||||
return ApiRequest.delete(`/aiol/aiolClass/${classId}/remove_student/${studentId}`);
|
||||
return ApiRequest.delete(`/aiol/aiolClass/${classId}/remove_student/${studentId}`, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,7 +94,7 @@
|
||||
<!-- 题目列表 -->
|
||||
<div class="question-list-section">
|
||||
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :pagination="false"
|
||||
:loading="loading" :row-key="(row) => row.id" :checked-row-keys="selectedRowKeys"
|
||||
:loading="loading" :row-key="(row: any) => row.id" :checked-row-keys="selectedRowKeys"
|
||||
@update:checked-row-keys="handleCheck" striped>
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
@ -124,8 +124,8 @@
|
||||
<div v-if="paginationItemCount > 0" class="pagination-wrapper">
|
||||
<n-pagination v-model:page="paginationPage" v-model:page-size="paginationPageSize"
|
||||
:item-count="paginationItemCount" :page-sizes="[10, 20, 50]" show-size-picker show-quick-jumper
|
||||
:prefix="({ itemCount }) => `共${itemCount}题`" @update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange" />
|
||||
:prefix="({ itemCount }: { itemCount: number }) => `共${itemCount}题`"
|
||||
@update:page="handlePageChange" @update:page-size="handlePageSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,21 +3,11 @@
|
||||
<div class="toolbar" v-if="props.type === 'course'">
|
||||
<NSpace>
|
||||
<n-select v-model:value="selectedDepartment" :options="departmentOptions" placeholder="班级名称"
|
||||
style="width: 200px" />
|
||||
<n-button
|
||||
type="info"
|
||||
ghost
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handleBatchTransfer"
|
||||
>
|
||||
style="width: 200px" />
|
||||
<n-button type="info" ghost :disabled="selectedRowKeys.length === 0" @click="handleBatchTransfer">
|
||||
批量调班({{ selectedRowKeys.length }})
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
ghost
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<n-button type="error" ghost :disabled="selectedRowKeys.length === 0" @click="handleBatchDelete">
|
||||
批量移除({{ selectedRowKeys.length }})
|
||||
</n-button>
|
||||
</NSpace>
|
||||
@ -38,7 +28,8 @@
|
||||
</template>
|
||||
管理班级
|
||||
</n-button>
|
||||
<n-button type="primary" ghost @click="openInviteModal(selectedDepartment || props.classId?.toString() || '1')">
|
||||
<n-button type="primary" ghost
|
||||
@click="openInviteModal(selectedDepartment || String(props.classId || '1'))">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<QrCode />
|
||||
@ -57,7 +48,8 @@
|
||||
全部学员
|
||||
</div>
|
||||
<NSpace>
|
||||
<n-dropdown trigger="hover" :options="addStudentOptions" @select="handleAddStudentSelect" v-if="props.type === 'course'" >
|
||||
<n-dropdown trigger="hover" :options="addStudentOptions" @select="handleAddStudentSelect"
|
||||
v-if="props.type === 'course'">
|
||||
<n-button type="primary">
|
||||
添加学员
|
||||
</n-button>
|
||||
@ -82,23 +74,16 @@
|
||||
</div>
|
||||
<n-divider v-if="props.type === 'student'" />
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-key="(row: StudentItem) => row.id"
|
||||
v-model:checked-row-keys="selectedRowKeys"
|
||||
striped
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
<n-data-table :columns="columns" :data="data" :pagination="pagination" :loading="loading"
|
||||
:row-key="(row: StudentItem) => row.id" v-model:checked-row-keys="selectedRowKeys" striped bordered
|
||||
size="small" />
|
||||
|
||||
<!-- 添加班级弹窗 -->
|
||||
<n-modal v-model:show="showAddClassModal" :title="isRenameMode ? '重命名' : '添加班级'">
|
||||
<n-card style="width: 500px" :title="isRenameMode ? '重命名' : '添加班级'" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
||||
<n-form ref="classFormRef" :model="classFormData" :rules="classRules" label-placement="left" label-width="80px"
|
||||
require-mark-placement="right-hanging">
|
||||
<n-card style="width: 500px" :title="isRenameMode ? '重命名' : '添加班级'" :bordered="false" size="huge"
|
||||
role="dialog" aria-modal="true">
|
||||
<n-form ref="classFormRef" :model="classFormData" :rules="classRules" label-placement="left"
|
||||
label-width="80px" require-mark-placement="right-hanging">
|
||||
<n-form-item label="班级名称" path="className">
|
||||
<n-input v-model:value="classFormData.className" placeholder="请输入班级名称" clearable />
|
||||
</n-form-item>
|
||||
@ -136,8 +121,10 @@
|
||||
<div class="row-item creator-col">{{ classItem.creator }}</div>
|
||||
<div class="row-item time-col">{{ classItem.createTime }}</div>
|
||||
<div class="row-item action-col">
|
||||
<n-button size="small" type="info" ghost @click="handleRenameClass(classItem)">重命名</n-button>
|
||||
<n-button size="small" type="error" ghost @click="handleDeleteClass(classItem)">删除</n-button>
|
||||
<n-button size="small" type="info" ghost
|
||||
@click="handleRenameClass(classItem)">重命名</n-button>
|
||||
<n-button size="small" type="error" ghost
|
||||
@click="handleDeleteClass(classItem)">删除</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,11 +139,7 @@
|
||||
<p>将选中的 <strong>{{ selectedRowKeys.length }}</strong> 名学员调至以下班级:</p>
|
||||
<div class="selected-students">
|
||||
<div class="student-list">
|
||||
<div
|
||||
v-for="student in selectedStudents"
|
||||
:key="student.id"
|
||||
class="student-item"
|
||||
>
|
||||
<div v-for="student in selectedStudents" :key="student.id" class="student-item">
|
||||
<span class="student-name">{{ student.studentName }}</span>
|
||||
<span class="student-account">({{ student.accountNumber }})</span>
|
||||
</div>
|
||||
@ -166,17 +149,11 @@
|
||||
<div class="class-selection">
|
||||
<div class="selection-title">选择目标班级:</div>
|
||||
<div class="class-list">
|
||||
<div
|
||||
v-for="option in classOptions"
|
||||
:key="option.value"
|
||||
class="class-item"
|
||||
<div v-for="option in classOptions" :key="option.value" class="class-item"
|
||||
:class="{ 'selected': selectedTargetClass === option.value }"
|
||||
@click="selectedTargetClass = option.value"
|
||||
>
|
||||
<n-checkbox
|
||||
:checked="selectedTargetClass === option.value"
|
||||
@update:checked="() => selectedTargetClass = option.value"
|
||||
/>
|
||||
@click="selectedTargetClass = option.value">
|
||||
<n-checkbox :checked="selectedTargetClass === option.value"
|
||||
@update:checked="() => selectedTargetClass = option.value" />
|
||||
<span class="class-name">{{ option.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -193,7 +170,8 @@
|
||||
|
||||
<!-- 添加/编辑学员弹窗 -->
|
||||
<n-modal v-model:show="showAddModal" :title="isEditMode ? '编辑学员' : '添加学员'">
|
||||
<n-card style="width: 600px" :title="isEditMode ? '编辑学员' : '添加学员'" :bordered="false" size="huge" role="dialog" aria-modal="true">
|
||||
<n-card style="width: 600px" :title="isEditMode ? '编辑学员' : '添加学员'" :bordered="false" size="huge"
|
||||
role="dialog" aria-modal="true">
|
||||
<n-form ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="80px"
|
||||
require-mark-placement="right-hanging">
|
||||
<n-form-item label="姓名" path="studentName">
|
||||
@ -203,30 +181,16 @@
|
||||
<n-input v-model:value="formData.studentId" placeholder="请输入学员学号" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="登录密码" path="loginPassword">
|
||||
<n-input
|
||||
v-model:value="formData.loginPassword"
|
||||
type="password"
|
||||
:placeholder="isEditMode ? '不填写则不修改密码' : '请输入登录密码'"
|
||||
show-password-on="click"
|
||||
clearable
|
||||
/>
|
||||
<n-input v-model:value="formData.loginPassword" type="password"
|
||||
:placeholder="isEditMode ? '不填写则不修改密码' : '请输入登录密码'" show-password-on="click" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="所在学校" path="college">
|
||||
<n-select
|
||||
v-model:value="formData.college"
|
||||
:options="collegeOptions"
|
||||
placeholder="请选择学校"
|
||||
clearable
|
||||
/>
|
||||
<n-select v-model:value="formData.college" :options="collegeOptions" placeholder="请选择学校"
|
||||
clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="所在班级" path="className">
|
||||
<n-select
|
||||
v-model:value="formData.className"
|
||||
:options="classSelectOptions"
|
||||
placeholder="请选择班级"
|
||||
multiple
|
||||
clearable
|
||||
/>
|
||||
<n-select v-model:value="formData.className" :options="classSelectOptions" placeholder="请选择班级"
|
||||
multiple clearable />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
@ -251,22 +215,13 @@
|
||||
<div class="class-selection">
|
||||
<div class="selection-title">选择目标班级:</div>
|
||||
<div class="class-list">
|
||||
<div
|
||||
v-for="option in classOptions"
|
||||
:key="option.value"
|
||||
class="class-item"
|
||||
<div v-for="option in classOptions" :key="option.value" class="class-item"
|
||||
:class="{ 'selected': selectedTargetClass === option.value }"
|
||||
@click="selectedTargetClass = option.value"
|
||||
>
|
||||
<n-checkbox
|
||||
:checked="selectedTargetClass === option.value"
|
||||
@update:checked="() => selectedTargetClass = option.value"
|
||||
/>
|
||||
@click="selectedTargetClass = option.value">
|
||||
<n-checkbox :checked="selectedTargetClass === option.value"
|
||||
@update:checked="() => selectedTargetClass = option.value" />
|
||||
<span class="class-name">{{ option.label }}</span>
|
||||
<span
|
||||
v-if="isCurrentClass(option.value)"
|
||||
class="class-desc"
|
||||
>当前</span>
|
||||
<span v-if="isCurrentClass(option.value)" class="class-desc">当前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -287,7 +242,7 @@
|
||||
<div class="invite-code-display">
|
||||
<div class="invite-title">班级邀请码</div>
|
||||
<div class="invite-note" v-if="currentInviteClassId">
|
||||
班级:{{ masterClassList.find(item => item.id === currentInviteClassId)?.className || '未知班级' }}
|
||||
班级:{{masterClassList.find(item => item.id === currentInviteClassId)?.className || '未知班级'}}
|
||||
</div>
|
||||
<div class="invite-code">{{ inviteCode }}</div>
|
||||
<n-button ghost type="primary" @click="copyInviteCode">复制</n-button>
|
||||
@ -296,19 +251,11 @@
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- 导入学员弹窗 -->
|
||||
<ImportModal
|
||||
v-model:show="showImportModal"
|
||||
title="导入学员"
|
||||
:show-radio-options="true"
|
||||
radio-label="导入重复学员信息"
|
||||
:radio-options="importRadioOptions"
|
||||
radio-field="updateMode"
|
||||
import-type="student"
|
||||
template-name="student_import_template.xlsx"
|
||||
@success="handleImportSuccess"
|
||||
@template-download="handleTemplateDownload"
|
||||
/>
|
||||
<!-- 导入学员弹窗 -->
|
||||
<ImportModal v-model:show="showImportModal" title="导入学员" :show-radio-options="true" radio-label="导入重复学员信息"
|
||||
:radio-options="importRadioOptions" radio-field="updateMode" import-type="student"
|
||||
template-name="student_import_template.xlsx" @success="handleImportSuccess"
|
||||
@template-download="handleTemplateDownload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -342,7 +289,7 @@ import ImportModal from '@/components/common/ImportModal.vue'
|
||||
// 定义 props 类型
|
||||
interface Props {
|
||||
type: 'course' | 'student'
|
||||
classId?: number | null // 新增班级ID参数
|
||||
classId?: string | number | null // 新增班级ID参数,支持字符串和数字
|
||||
}
|
||||
|
||||
// 接收 props
|
||||
@ -447,12 +394,12 @@ const rules: FormRules = {
|
||||
{ required: true, message: '请选择所在学院', trigger: 'blur' }
|
||||
],
|
||||
className: [
|
||||
{
|
||||
required: true,
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: '请选择至少一个班级',
|
||||
trigger: 'blur'
|
||||
message: '请选择至少一个班级',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -482,7 +429,7 @@ const addStudentOptions = [
|
||||
key: 'manual'
|
||||
},
|
||||
{
|
||||
label: '学员库添加',
|
||||
label: '学员库添加',
|
||||
key: 'library'
|
||||
}
|
||||
]
|
||||
@ -504,7 +451,7 @@ const departmentOptions = computed(() => [
|
||||
])
|
||||
|
||||
// 班级选项(用于调班弹窗)
|
||||
const classOptions = computed(() =>
|
||||
const classOptions = computed(() =>
|
||||
masterClassList.value.map(item => ({
|
||||
label: item.className,
|
||||
value: item.id
|
||||
@ -512,7 +459,7 @@ const classOptions = computed(() =>
|
||||
)
|
||||
|
||||
// 班级选择器选项(用于表单中的班级选择)
|
||||
const classSelectOptions = computed(() =>
|
||||
const classSelectOptions = computed(() =>
|
||||
masterClassList.value.map(item => ({
|
||||
label: item.className,
|
||||
value: item.id
|
||||
@ -550,8 +497,8 @@ const columns: DataTableColumns<StudentItem> = [
|
||||
// 渲染班级名称,支持多行显示
|
||||
return h('div', {
|
||||
class: 'class-cell'
|
||||
}, classNames.map((name, index) =>
|
||||
h('div', {
|
||||
}, classNames.map((name, index) =>
|
||||
h('div', {
|
||||
key: index,
|
||||
class: 'class-cell-item'
|
||||
}, name)
|
||||
@ -687,7 +634,7 @@ const handleTransfer = (row: StudentItem) => {
|
||||
currentTransferStudent.value = row
|
||||
selectedTargetClass.value = ''
|
||||
showTransferModal.value = true
|
||||
|
||||
|
||||
console.log('打开调班弹窗:', {
|
||||
学员信息: row,
|
||||
可选班级: classOptions.value
|
||||
@ -714,10 +661,10 @@ const handleBatchDelete = () => {
|
||||
message.warning('请先选择要移除的学员')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const selectedStudentsList = selectedStudents.value
|
||||
const studentNames = selectedStudentsList.map(s => s.studentName).join('、')
|
||||
|
||||
|
||||
dialog.info({
|
||||
title: '批量移除确认',
|
||||
content: `确定要移除选中的 ${selectedRowKeys.value.length} 名学员吗?\n\n学员名单:${studentNames}\n\n移除后这些学员将无法访问班级资源!`,
|
||||
@ -727,17 +674,17 @@ const handleBatchDelete = () => {
|
||||
try {
|
||||
// 这里模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
|
||||
const removedCount = selectedRowKeys.value.length
|
||||
|
||||
|
||||
// 从数据中移除选中的学员
|
||||
data.value = data.value.filter(student => !selectedRowKeys.value.includes(student.id))
|
||||
|
||||
|
||||
// 清空选中状态
|
||||
selectedRowKeys.value = []
|
||||
|
||||
|
||||
message.success(`成功移除 ${removedCount} 名学员`)
|
||||
|
||||
|
||||
// 重新加载数据
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
@ -772,17 +719,17 @@ const confirmBatchTransfer = async () => {
|
||||
try {
|
||||
// 这里模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
|
||||
const transferCount = selectedRowKeys.value.length
|
||||
const targetClassName = masterClassList.value.find(item => item.id === selectedTargetClass.value)?.className
|
||||
|
||||
|
||||
message.success(`已将 ${transferCount} 名学员调至 ${targetClassName}`)
|
||||
|
||||
|
||||
// 关闭弹窗并重置状态
|
||||
showBatchTransferModal.value = false
|
||||
selectedTargetClass.value = ''
|
||||
selectedRowKeys.value = []
|
||||
|
||||
|
||||
// 重新加载数据
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
@ -829,13 +776,13 @@ const handleDeleteStudent = (row: StudentItem) => {
|
||||
message.error('班级ID不存在,无法删除学员')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用删除班级学员API
|
||||
await ClassApi.removeStudent(props.classId.toString(), row.id)
|
||||
|
||||
|
||||
// 调用删除班级学员API - 使用学号而不是主键ID
|
||||
await ClassApi.removeStudent(String(props.classId), row.accountNumber)
|
||||
|
||||
const studentName = row.studentName
|
||||
message.success(`已删除学员:${studentName}`)
|
||||
|
||||
|
||||
// 重新加载数据以确保数据同步
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
@ -866,15 +813,15 @@ const handleConfirmTransfer = async () => {
|
||||
try {
|
||||
// 这里模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
|
||||
const targetClassName = masterClassList.value.find(item => item.id === selectedTargetClass.value)?.className
|
||||
message.success(`已将学员 ${currentTransferStudent.value.studentName} 调至 ${targetClassName}`)
|
||||
|
||||
|
||||
// 关闭弹窗并重置状态
|
||||
showTransferModal.value = false
|
||||
currentTransferStudent.value = null
|
||||
selectedTargetClass.value = ''
|
||||
|
||||
|
||||
// 重新加载数据
|
||||
loadData(props.classId)
|
||||
} catch (error) {
|
||||
@ -888,11 +835,11 @@ const isCurrentClass = (classValue: string) => {
|
||||
console.log('调班判断: 未选中学员')
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// 根据学员的className匹配班级value
|
||||
const studentClassName = currentTransferStudent.value.className
|
||||
const classOption = masterClassList.value.find(item => item.className === studentClassName)
|
||||
|
||||
|
||||
console.log('调班判断:', {
|
||||
学员姓名: currentTransferStudent.value.studentName,
|
||||
学员班级: studentClassName,
|
||||
@ -900,7 +847,7 @@ const isCurrentClass = (classValue: string) => {
|
||||
找到的班级: classOption,
|
||||
是否匹配: classOption?.id === classValue
|
||||
})
|
||||
|
||||
|
||||
return classOption?.id === classValue
|
||||
}
|
||||
|
||||
@ -916,9 +863,9 @@ const getClassNameById = (classId: string): string => {
|
||||
|
||||
// 处理多班级显示的辅助函数
|
||||
const formatClassNames = (classInfo: string): string[] => {
|
||||
|
||||
|
||||
if (!classInfo) return ['未分配班级']
|
||||
|
||||
|
||||
if (classInfo.includes(',')) {
|
||||
// 多个班级,用逗号分隔
|
||||
return classInfo.split(',').map(id => id.trim()).map(getClassNameById)
|
||||
@ -941,7 +888,7 @@ const openInviteModal = (classId: string) => {
|
||||
currentInviteClassId.value = classId
|
||||
inviteCode.value = generateInviteCode(classId)
|
||||
showInviteModal.value = true
|
||||
|
||||
|
||||
console.log('打开邀请码弹窗:', {
|
||||
班级ID: classId,
|
||||
邀请码: inviteCode.value
|
||||
@ -959,7 +906,7 @@ const copyInviteCode = () => {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式暂不实现
|
||||
message.info('编辑功能暂未实现,敬请期待')
|
||||
@ -969,13 +916,13 @@ const handleSubmit = async () => {
|
||||
console.log('🚀 开始新增学员...')
|
||||
console.log('表单数据:', formData.value)
|
||||
console.log('当前班级ID:', props.classId)
|
||||
|
||||
|
||||
// 验证必要参数
|
||||
if (!formData.value.className || formData.value.className.length === 0) {
|
||||
message.error('请选择班级')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 构建API请求参数,根据接口文档字段映射
|
||||
const payload = {
|
||||
realName: formData.value.studentName,
|
||||
@ -984,14 +931,14 @@ const handleSubmit = async () => {
|
||||
school: formData.value.college,
|
||||
classId: formData.value.className.join(',')
|
||||
}
|
||||
|
||||
|
||||
console.log('📝 API请求参数:', payload)
|
||||
|
||||
|
||||
// 调用创建学员API
|
||||
const response = await ClassApi.createdStudents(payload)
|
||||
|
||||
|
||||
console.log('✅ 创建学员响应:', response)
|
||||
|
||||
|
||||
if (response.data && (response.data.success || response.data.code === 200)) {
|
||||
message.success(`已成功添加学员 ${formData.value.studentName}`)
|
||||
} else {
|
||||
@ -999,11 +946,11 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 关闭弹窗并重置表单
|
||||
showAddModal.value = false
|
||||
resetForm()
|
||||
|
||||
|
||||
// 重新加载数据
|
||||
loadData(props.classId)
|
||||
} catch (error: any) {
|
||||
@ -1085,7 +1032,7 @@ const handleAddClass = async () => {
|
||||
// 通知父组件更新列表
|
||||
emit('class-changed')
|
||||
}
|
||||
|
||||
|
||||
// 重新加载班级列表数据
|
||||
await loadClassList()
|
||||
closeAddClassModal()
|
||||
@ -1119,7 +1066,7 @@ const handleDeleteClass = (classItem: any) => {
|
||||
try {
|
||||
await ClassApi.deleteClass(classItem.id)
|
||||
message.success(`已删除班级:${classItem.className}`)
|
||||
|
||||
|
||||
// 重新加载班级列表数据
|
||||
await loadClassList()
|
||||
// 通知父组件更新列表
|
||||
@ -1136,7 +1083,7 @@ const loadClassList = async () => {
|
||||
try {
|
||||
console.log('🚀 开始加载班级列表数据...')
|
||||
const response = await ClassApi.queryClassList({ course_id: null })
|
||||
|
||||
|
||||
// 转换API响应数据为组件需要的格式
|
||||
const classListData = response.data.result || []
|
||||
const transformedClassData: ClassItem[] = classListData.map((classItem: any) => ({
|
||||
@ -1158,7 +1105,7 @@ const loadClassList = async () => {
|
||||
minute: '2-digit'
|
||||
}).replace(/\//g, '.').replace(',', '')
|
||||
}))
|
||||
|
||||
|
||||
masterClassList.value = transformedClassData
|
||||
console.log(`✅ 成功加载班级列表,共 ${transformedClassData.length} 个班级`)
|
||||
} catch (error) {
|
||||
@ -1172,33 +1119,28 @@ const loadClassList = async () => {
|
||||
let loadDataTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 数据加载函数,对接班级学生列表API
|
||||
const loadData = async (classId?: number | null) => {
|
||||
const loadData = async (classId?: string | number | null) => {
|
||||
// 防抖处理:如果在很短时间内多次调用,只执行最后一次
|
||||
if (loadDataTimer) {
|
||||
clearTimeout(loadDataTimer)
|
||||
}
|
||||
|
||||
|
||||
loadDataTimer = setTimeout(async () => {
|
||||
console.log(`🚀 开始加载班级数据 - classId: ${classId}, 调用栈:`, new Error().stack?.split('\n')[2]?.trim())
|
||||
|
||||
// 如果正在加载中,避免重复请求
|
||||
if (loading.value) {
|
||||
console.log('⚠️ 数据正在加载中,跳过重复请求')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
if (classId === null || classId === undefined) {
|
||||
// 未选择班级时显示空数据
|
||||
data.value = []
|
||||
totalStudents.value = 0
|
||||
console.log('📝 未选择班级,显示空数据')
|
||||
} else {
|
||||
// 调用班级学生列表API
|
||||
console.log(`📡 正在获取班级 ${classId} 的学生数据...`)
|
||||
const response = await ClassApi.getClassStudents(classId.toString())
|
||||
|
||||
const response = await ClassApi.getClassStudents(String(classId))
|
||||
|
||||
// 转换API响应数据为组件需要的格式
|
||||
const studentsData = response.data.result || []
|
||||
const transformedData: StudentItem[] = studentsData.map((student: any) => ({
|
||||
@ -1216,14 +1158,12 @@ const loadData = async (classId?: number | null) => {
|
||||
minute: '2-digit'
|
||||
}).replace(/\//g, '.').replace(',', '') : '未知时间'
|
||||
}))
|
||||
|
||||
|
||||
data.value = transformedData
|
||||
totalStudents.value = transformedData.length
|
||||
|
||||
console.log(`✅ 成功加载班级 ${classId} 的数据,共 ${transformedData.length} 名学员`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载班级学生数据失败:', error)
|
||||
console.error('加载班级学生数据失败:', error)
|
||||
message.error('加载学生数据失败,请重试')
|
||||
data.value = []
|
||||
totalStudents.value = 0
|
||||
@ -1238,7 +1178,7 @@ const loadData = async (classId?: number | null) => {
|
||||
const handleImportSuccess = (result: any) => {
|
||||
console.log('导入成功:', result)
|
||||
message.success(`导入完成!成功:${result.details?.success || 0} 条,失败:${result.details?.failed || 0} 条`)
|
||||
|
||||
|
||||
// 重新加载数据
|
||||
loadData(props.classId)
|
||||
}
|
||||
@ -1254,10 +1194,9 @@ const handleTemplateDownload = (type?: string) => {
|
||||
watch(
|
||||
() => props.classId,
|
||||
(newClassId, oldClassId) => {
|
||||
console.log(`班级ID从 ${oldClassId} 变更为 ${newClassId}`)
|
||||
if (newClassId !== oldClassId) {
|
||||
// 同步更新选择器的状态(不触发选择器的watch)
|
||||
selectedDepartment.value = newClassId ? newClassId.toString() : ''
|
||||
selectedDepartment.value = newClassId ? String(newClassId) : ''
|
||||
loadData(newClassId)
|
||||
}
|
||||
},
|
||||
@ -1269,12 +1208,11 @@ watch(
|
||||
watch(
|
||||
() => selectedDepartment.value,
|
||||
(newDepartmentId, oldDepartmentId) => {
|
||||
console.log(`选择的班级从 ${oldDepartmentId} 变更为 ${newDepartmentId}`)
|
||||
// 只有当不是props.classId驱动的变化时才加载数据
|
||||
// 如果当前props.classId存在且与选择器值一致,说明是props驱动的变化,不需要重复加载
|
||||
const currentPropsClassId = props.classId?.toString()
|
||||
const currentPropsClassId = props.classId ? String(props.classId) : ''
|
||||
if (newDepartmentId !== oldDepartmentId && newDepartmentId !== currentPropsClassId) {
|
||||
const targetClassId = newDepartmentId ? Number(newDepartmentId) : null
|
||||
const targetClassId = newDepartmentId || null
|
||||
loadData(targetClassId)
|
||||
}
|
||||
},
|
||||
@ -1283,7 +1221,6 @@ watch(
|
||||
|
||||
const loadSchoolList = () => {
|
||||
TeachCourseApi.getSchoolList().then(res => {
|
||||
console.log('获取学校列表:', res)
|
||||
collegeOptions.value = res.data.result.map((school: any) => ({
|
||||
label: school,
|
||||
value: school
|
||||
@ -1297,15 +1234,13 @@ onMounted(async () => {
|
||||
// 首先加载班级列表数据
|
||||
await loadClassList()
|
||||
loadSchoolList()
|
||||
|
||||
|
||||
// 初始加载时,优先使用使用传入的classId,其次使用选择器的值
|
||||
const initialClassId = props.classId ? props.classId : Number(selectedDepartment.value)
|
||||
const initialClassId = props.classId ? props.classId : selectedDepartment.value
|
||||
loadData(initialClassId)
|
||||
|
||||
// 获取课程id 只有课程管理下有课程id
|
||||
if(route.path.includes('/teacher/course-editor')){
|
||||
console.log('当前路由路径:', route.path)
|
||||
console.log('课程ID:', router.currentRoute.value.params.id)
|
||||
if (route.path.includes('/teacher/course-editor')) {
|
||||
courseId.value = router.currentRoute.value.params.id.toString()
|
||||
}
|
||||
})
|
||||
|
@ -138,7 +138,7 @@
|
||||
<div class="question-component-wrapper">
|
||||
<!-- 单选题 -->
|
||||
<SingleChoiceQuestion v-if="subQuestion.type === 'single_choice'"
|
||||
v-model="subQuestion.options!" :correctAnswer="subQuestion.correctAnswer"
|
||||
v-model="subQuestion.options!" :correctAnswer="subQuestion.correctAnswer || null"
|
||||
@update:correctAnswer="(val: number | null) => subQuestion.correctAnswer = val"
|
||||
v-model:title="subQuestion.title" v-model:explanation="subQuestion.explanation" />
|
||||
|
||||
@ -719,7 +719,7 @@ const ensureSubQuestionFields = (subQuestion: SubQuestion): void => {
|
||||
if (!subQuestion.correctAnswers) subQuestion.correctAnswers = [];
|
||||
if (!subQuestion.fillBlanks) subQuestion.fillBlanks = [];
|
||||
if (!subQuestion.subQuestions) subQuestion.subQuestions = [];
|
||||
if (subQuestion.correctAnswer === undefined || subQuestion.correctAnswer === '' || subQuestion.correctAnswer === null) subQuestion.correctAnswer = null;
|
||||
if (subQuestion.correctAnswer === undefined || subQuestion.correctAnswer === null) subQuestion.correctAnswer = null;
|
||||
if (subQuestion.trueFalseAnswer === undefined) subQuestion.trueFalseAnswer = null;
|
||||
if (!subQuestion.textAnswer) subQuestion.textAnswer = '';
|
||||
if (!subQuestion.explanation) subQuestion.explanation = '';
|
||||
|
@ -162,7 +162,12 @@
|
||||
<div class="section">
|
||||
<h3 class="section-title">学员信息</h3>
|
||||
<div class="info-fields">
|
||||
<div class="field">学员姓名</div>
|
||||
<div class="field editable-field" @mouseenter="startEdit('studentName')"
|
||||
@mouseleave="stopEdit('studentName')">
|
||||
<span v-if="!editingField.studentName">{{ studentName }}</span>
|
||||
<input v-else v-model="studentName" @blur="stopEdit('studentName')"
|
||||
@keyup.enter="stopEdit('studentName')" class="field-input" />
|
||||
</div>
|
||||
<div class="field">学员帐号</div>
|
||||
<div class="field">班级名称</div>
|
||||
<div class="field">自定义文字</div>
|
||||
@ -173,11 +178,37 @@
|
||||
<div class="section">
|
||||
<h3 class="section-title">考试信息</h3>
|
||||
<div class="info-fields">
|
||||
<div class="field">考试名称</div>
|
||||
<div class="field">考试分数</div>
|
||||
<div class="field editable-field" @mouseenter="startEdit('examName')" @mouseleave="stopEdit('examName')">
|
||||
<span v-if="!editingField.examName">{{ examName }}</span>
|
||||
<input v-else v-model="examName" @blur="stopEdit('examName')" @keyup.enter="stopEdit('examName')"
|
||||
class="field-input" />
|
||||
</div>
|
||||
<div class="field editable-field" @mouseenter="startEdit('examScore')"
|
||||
@mouseleave="stopEdit('examScore')">
|
||||
<span v-if="!editingField.examScore">{{ examScore }}</span>
|
||||
<input v-else v-model="examScore" @blur="stopEdit('examScore')" @keyup.enter="stopEdit('examScore')"
|
||||
class="field-input" />
|
||||
</div>
|
||||
<div class="field">考试评语</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 证书信息 -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">证书信息</h3>
|
||||
<div class="info-fields">
|
||||
<div class="field">证书编号</div>
|
||||
<div class="field">发证日期</div>
|
||||
<div class="field">颁发机构</div>
|
||||
<div class="field">有效期至</div>
|
||||
<div class="field editable-field" @mouseenter="startEdit('certificationDate')"
|
||||
@mouseleave="stopEdit('certificationDate')">
|
||||
<span v-if="!editingField.certificationDate">{{ certificationDate }}</span>
|
||||
<input v-else v-model="certificationDate" @blur="stopEdit('certificationDate')"
|
||||
@keyup.enter="stopEdit('certificationDate')" class="field-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收起/展开按钮 -->
|
||||
@ -189,7 +220,26 @@
|
||||
|
||||
<div class="certificate-content" :class="{ 'preview-mode': isPreviewMode }"
|
||||
:style="{ backgroundColor: selectedColor }">
|
||||
<img src="/images/teacher/certificate.png" alt="">
|
||||
<div class="certificate-wrapper">
|
||||
<img src="/images/teacher/certificate.png" alt="证书背景" class="certificate-bg">
|
||||
|
||||
<!-- 荣誉证书标题 -->
|
||||
<h1 class="certificate-title">荣誉证书</h1>
|
||||
|
||||
<!-- 学员姓名和成就描述 -->
|
||||
<div class="achievement-section">
|
||||
<div class="student-name">{{ studentName }}</div>
|
||||
<div class="achievement-middle">
|
||||
在<span class="exam-name">{{ examName }}</span>恭喜您取得<span class="exam-score">{{ examScore }}</span>的优异成绩,
|
||||
</div>
|
||||
<div class="achievement-end">特发此状,以资鼓励!</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证时间 -->
|
||||
<div class="certification-date">
|
||||
<span class="label">{{ certificationDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -238,6 +288,20 @@ const validityDuration = ref(60)
|
||||
const validityEndDate = ref('2000-10-11T09:00')
|
||||
const currentValidityText = ref('永久有效')
|
||||
|
||||
// 证书文字数据
|
||||
const studentName = ref('学员姓名')
|
||||
const examName = ref('考试名称')
|
||||
const examScore = ref('考试分数')
|
||||
const certificationDate = ref('认证时间')
|
||||
|
||||
// 编辑状态
|
||||
const editingField = ref({
|
||||
studentName: false,
|
||||
examName: false,
|
||||
examScore: false,
|
||||
certificationDate: false
|
||||
})
|
||||
|
||||
// 切换收起/展开状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
@ -309,6 +373,16 @@ const closeCategoryPopover = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑字段
|
||||
const startEdit = (fieldName: keyof typeof editingField.value) => {
|
||||
editingField.value[fieldName] = true
|
||||
}
|
||||
|
||||
// 停止编辑字段
|
||||
const stopEdit = (fieldName: keyof typeof editingField.value) => {
|
||||
editingField.value[fieldName] = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -402,6 +476,7 @@ onUnmounted(() => {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.top-section {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
@ -750,6 +825,7 @@ onUnmounted(() => {
|
||||
.btn-confirm:hover {
|
||||
background: #0277BD;
|
||||
}
|
||||
|
||||
.right-section {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -768,6 +844,7 @@ onUnmounted(() => {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.collapse-button {
|
||||
position: absolute;
|
||||
left: -40px;
|
||||
@ -998,6 +1075,28 @@ onUnmounted(() => {
|
||||
background: #E8E8E8;
|
||||
}
|
||||
|
||||
.editable-field {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editable-field:hover {
|
||||
background: #E8E8E8;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 内联颜色选择器样式 */
|
||||
.color-input-inline {
|
||||
width: 100%;
|
||||
@ -1025,8 +1124,77 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.certificate-content img {
|
||||
width: 800px;
|
||||
.certificate-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.certificate-bg {
|
||||
width: 800px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 荣誉证书标题 */
|
||||
.certificate-title {
|
||||
position: absolute;
|
||||
top: 23%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 40px;
|
||||
color: #CC9E4A;
|
||||
margin: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 成就描述区域 */
|
||||
.achievement-section {
|
||||
width: 68%;
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-size: 22px;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 19px;
|
||||
}
|
||||
|
||||
.achievement-middle {
|
||||
margin-left: 55px;
|
||||
font-size: 22px;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.achievement-end {
|
||||
margin-left: 55px;
|
||||
font-size: 22px;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.exam-name,
|
||||
.exam-score {
|
||||
color: #333333;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
/* 认证时间 */
|
||||
.certification-date {
|
||||
position: absolute;
|
||||
bottom: 13%;
|
||||
right: 18%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.certification-date .label {
|
||||
font-size: 22px;
|
||||
color: #333333;
|
||||
}
|
||||
</style>
|
@ -65,7 +65,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { NBadge, NTabs, NTabPane } from 'naive-ui'
|
||||
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui'
|
||||
import { ChatApi } from '@/api'
|
||||
|
||||
// 导入子组件
|
||||
import NotificationMessages from './components/NotificationMessages.vue'
|
||||
@ -75,13 +76,17 @@ import SystemMessages from './components/SystemMessages.vue'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('notification') // 当前激活的tab
|
||||
const message = useMessage()
|
||||
|
||||
// 各类消息数量(角标显示)
|
||||
const notificationCount = ref(5) // 即时消息数量
|
||||
const commentCount = ref(3) // 评论和@数量
|
||||
const notificationCount = ref(0) // 即时消息数量
|
||||
const commentCount = ref(0) // 评论和@数量
|
||||
const favoriteCount = ref(0) // 赞和收藏数量
|
||||
const systemCount = ref(0) // 系统消息数量
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 初始化逻辑
|
||||
@ -89,9 +94,48 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 加载各类消息数量
|
||||
const loadMessageCounts = () => {
|
||||
// TODO: 这里后续可以调用API获取实际的消息数量
|
||||
// 暂时使用模拟数据
|
||||
const loadMessageCounts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 加载即时消息数量
|
||||
await loadNotificationCount()
|
||||
|
||||
// TODO: 后续可以添加其他类型的消息数量加载
|
||||
// await loadCommentCount()
|
||||
// await loadFavoriteCount()
|
||||
// await loadSystemCount()
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载消息数量失败:', error)
|
||||
message.error('加载消息数量失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载即时消息数量
|
||||
const loadNotificationCount = async () => {
|
||||
try {
|
||||
const response = await ChatApi.getUnreadCount()
|
||||
if (response.data) {
|
||||
notificationCount.value = response.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取未读消息数量失败:', error)
|
||||
// 如果API调用失败,尝试获取会话列表计算未读数量
|
||||
try {
|
||||
const chatsResponse = await ChatApi.getMyChats()
|
||||
if (chatsResponse.data && chatsResponse.data.success) {
|
||||
notificationCount.value = chatsResponse.data.result.reduce((total: number, chat: any) => {
|
||||
return total + (chat.unreadCount || 0)
|
||||
}, 0)
|
||||
}
|
||||
} catch (chatError) {
|
||||
console.error('获取会话列表失败:', chatError)
|
||||
// 如果都失败了,保持默认值0
|
||||
notificationCount.value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,7 +1,25 @@
|
||||
<template>
|
||||
<div class="message-center">
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="messages.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<n-icon size="48" color="#d9d9d9">
|
||||
<ChatbubbleEllipsesOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
<p class="empty-text">暂无评论和@消息</p>
|
||||
<p class="empty-desc">当有人评论或@你时,消息会显示在这里</p>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list">
|
||||
<div v-else class="message-list">
|
||||
<div v-for="message in messages" :key="message.id" class="message-item">
|
||||
<!-- 用户头像 -->
|
||||
<div class="avatar-container">
|
||||
@ -36,14 +54,7 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="message-btns">
|
||||
<div class="message-actions">
|
||||
<button class="action-btn" @click="toggleReply(message.id)">
|
||||
<n-icon size="16">
|
||||
<ChatbubbleEllipsesOutline />
|
||||
</n-icon>
|
||||
回复
|
||||
</button>
|
||||
<button class="action-btn" @click="toggleFavorite(message.id)">
|
||||
<!-- <i class="icon-favorite" :class="{ active: message.isFavorited }"></i> -->
|
||||
<button class="action-btn" :class="{ liked: message.isLiked }" @click="toggleLike(message.id)">
|
||||
<n-icon size="16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 32 32">
|
||||
@ -52,21 +63,45 @@
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
</n-icon>
|
||||
点赞
|
||||
{{ message.isLiked ? '已赞' : '点赞' }}
|
||||
<span v-if="message.likeCount && message.likeCount > 0" class="count">({{ message.likeCount }})</span>
|
||||
</button>
|
||||
<button class="action-btn delete-btn">
|
||||
|
||||
<button class="action-btn" :class="{ favorited: message.isFavorited }"
|
||||
@click="toggleFavorite(message.id)">
|
||||
<n-icon size="16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 32 32">
|
||||
<path d="M24 4H8a2.002 2.002 0 0 0-2 2v20l6-6h12a2.002 2.002 0 0 0 2-2V6a2.002 2.002 0 0 0-2-2z"
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
</n-icon>
|
||||
{{ message.isFavorited ? '已收藏' : '收藏' }}
|
||||
</button>
|
||||
|
||||
<button class="action-btn" @click="toggleReply(message.id)">
|
||||
<n-icon size="16">
|
||||
<ChatbubbleEllipsesOutline />
|
||||
</n-icon>
|
||||
回复
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="message-actions">
|
||||
<button class="action-btn delete-btn" @click="deleteMessage(message.id)">
|
||||
<n-icon size="16">
|
||||
<TrashOutline />
|
||||
</n-icon>
|
||||
删除
|
||||
</button>
|
||||
|
||||
<button class="action-btn delete-btn" @click="reportMessage(message.id)">
|
||||
<n-icon size="16">
|
||||
<WarningOutline />
|
||||
</n-icon>
|
||||
举报
|
||||
</button>
|
||||
</div>
|
||||
<button class="action-btn delete-btn">
|
||||
<n-icon size="16">
|
||||
<WarningOutline />
|
||||
</n-icon>
|
||||
举报
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 回复输入框 -->
|
||||
@ -97,17 +132,20 @@
|
||||
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
|
||||
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useMessage, NIcon } from 'naive-ui'
|
||||
import { ChatbubbleEllipsesOutline, TrashOutline, WarningOutline } from '@vicons/ionicons5'
|
||||
import { CourseApi } from '@/api'
|
||||
|
||||
// 消息类型定义
|
||||
interface Message {
|
||||
id: number
|
||||
type: number
|
||||
id: string
|
||||
type: number // 0=评论, 1=@
|
||||
username: string
|
||||
avatar: string
|
||||
courseInfo: string
|
||||
@ -117,54 +155,29 @@ interface Message {
|
||||
isFavorited: boolean
|
||||
showReplyBox: boolean
|
||||
replyContent: string
|
||||
courseId?: string
|
||||
userId?: string
|
||||
images?: string[]
|
||||
likeCount?: number
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const messages = ref<Message[]>([
|
||||
{
|
||||
id: 1,
|
||||
type: 1,
|
||||
username: '王建华化学老师',
|
||||
avatar: 'https://picsum.photos/200/200',
|
||||
courseInfo: '《教师小学期制实验》',
|
||||
content: '这里是老师留言的内容了',
|
||||
timestamp: '7月20日',
|
||||
isLiked: false,
|
||||
isFavorited: false,
|
||||
showReplyBox: false,
|
||||
replyContent: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 0,
|
||||
username: '叶仲学习分子',
|
||||
avatar: 'https://picsum.photos/200/200',
|
||||
courseInfo: '《教师小学期制实验》',
|
||||
content: '还好期末考成分学的记忆',
|
||||
timestamp: '7月20日 12:41',
|
||||
isLiked: false,
|
||||
isFavorited: false,
|
||||
showReplyBox: false,
|
||||
replyContent: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 1,
|
||||
username: '课程学习端课学',
|
||||
avatar: 'https://picsum.photos/200/200',
|
||||
courseInfo: '《教师小学期制实验》',
|
||||
content: '没事多看看课程你就懂了',
|
||||
timestamp: '7月20日',
|
||||
isLiked: false,
|
||||
isFavorited: false,
|
||||
showReplyBox: false,
|
||||
replyContent: ''
|
||||
}
|
||||
const messages = ref<Message[]>([])
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
|
||||
// 可用课程列表
|
||||
const availableCourses = ref<Array<{ id: string, name: string }>>([
|
||||
{ id: '1', name: '测试课程1' },
|
||||
{ id: '2', name: '测试课程2' },
|
||||
{ id: '3', name: '测试课程3' }
|
||||
])
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(29)
|
||||
const pageSize = ref(10)
|
||||
const totalPages = ref(1)
|
||||
const totalCount = ref(0)
|
||||
|
||||
// 计算显示的页码
|
||||
const visiblePages = computed(() => {
|
||||
@ -189,20 +202,90 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadMessages = () => {
|
||||
// TODO: 调用API加载消息数据
|
||||
}
|
||||
const loadMessages = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取所有课程的评论数据
|
||||
const allComments: Message[] = []
|
||||
|
||||
const toggleFavorite = (messageId: number) => {
|
||||
const message = messages.value.find(m => m.id === messageId)
|
||||
if (message) {
|
||||
message.isFavorited = !message.isFavorited
|
||||
for (const course of availableCourses.value) {
|
||||
try {
|
||||
const response = await CourseApi.getCourseComments(course.id)
|
||||
if (response.data && response.data.length > 0) {
|
||||
// 转换评论数据为消息格式
|
||||
const courseComments = response.data.map((comment: any) => ({
|
||||
id: comment.id,
|
||||
type: 0, // 评论类型
|
||||
username: comment.userName || '匿名用户',
|
||||
avatar: comment.userAvatar || '',
|
||||
courseInfo: course.name,
|
||||
content: comment.content || '',
|
||||
timestamp: comment.timeAgo || comment.createTime,
|
||||
isLiked: false,
|
||||
isFavorited: false,
|
||||
showReplyBox: false,
|
||||
replyContent: '',
|
||||
courseId: course.id,
|
||||
userId: comment.userId,
|
||||
images: comment.images || [],
|
||||
likeCount: comment.likeCount || 0
|
||||
}))
|
||||
|
||||
allComments.push(...courseComments)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`获取课程 ${course.name} 评论失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序(最新的在前)
|
||||
allComments.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (currentPage.value - 1) * pageSize.value
|
||||
const endIndex = startIndex + pageSize.value
|
||||
messages.value = allComments.slice(startIndex, endIndex)
|
||||
|
||||
// 更新分页信息
|
||||
totalCount.value = allComments.length
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
|
||||
console.log('✅ 加载评论和@消息成功:', {
|
||||
total: totalCount.value,
|
||||
current: messages.value.length,
|
||||
page: currentPage.value
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 加载评论和@消息失败:', error)
|
||||
message.error('加载消息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleReply = (messageId: number) => {
|
||||
const message = messages.value.find(m => m.id === messageId)
|
||||
if (message) {
|
||||
const toggleFavorite = (messageId: string) => {
|
||||
const msg = messages.value.find(m => m.id === messageId)
|
||||
if (msg) {
|
||||
msg.isFavorited = !msg.isFavorited
|
||||
// TODO: 调用API更新收藏状态
|
||||
console.log('切换收藏状态:', messageId, msg.isFavorited)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleLike = (messageId: string) => {
|
||||
const msg = messages.value.find(m => m.id === messageId)
|
||||
if (msg) {
|
||||
msg.isLiked = !msg.isLiked
|
||||
msg.likeCount = (msg.likeCount || 0) + (msg.isLiked ? 1 : -1)
|
||||
// TODO: 调用API更新点赞状态
|
||||
console.log('切换点赞状态:', messageId, msg.isLiked)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleReply = (messageId: string) => {
|
||||
const msg = messages.value.find(m => m.id === messageId)
|
||||
if (msg) {
|
||||
// 先隐藏所有其他消息的回复框
|
||||
messages.value.forEach(m => {
|
||||
if (m.id !== messageId) {
|
||||
@ -212,28 +295,71 @@ const toggleReply = (messageId: number) => {
|
||||
})
|
||||
|
||||
// 切换当前消息的回复框状态
|
||||
message.showReplyBox = !message.showReplyBox
|
||||
if (message.showReplyBox) {
|
||||
message.replyContent = ''
|
||||
msg.showReplyBox = !msg.showReplyBox
|
||||
if (msg.showReplyBox) {
|
||||
msg.replyContent = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cancelReply = (messageId: number) => {
|
||||
const message = messages.value.find(m => m.id === messageId)
|
||||
if (message) {
|
||||
message.showReplyBox = false
|
||||
message.replyContent = ''
|
||||
const cancelReply = (messageId: string) => {
|
||||
const msg = messages.value.find(m => m.id === messageId)
|
||||
if (msg) {
|
||||
msg.showReplyBox = false
|
||||
msg.replyContent = ''
|
||||
}
|
||||
}
|
||||
|
||||
const sendReply = (messageId: number) => {
|
||||
const message = messages.value.find(m => m.id === messageId)
|
||||
if (message && message.replyContent.trim()) {
|
||||
// TODO: 调用API发送回复
|
||||
console.log('发送回复:', message.replyContent)
|
||||
message.showReplyBox = false
|
||||
message.replyContent = ''
|
||||
const sendReply = async (messageId: string) => {
|
||||
const msg = messages.value.find(m => m.id === messageId)
|
||||
if (msg && msg.replyContent.trim()) {
|
||||
try {
|
||||
// TODO: 调用API发送回复
|
||||
console.log('发送回复:', {
|
||||
commentId: messageId,
|
||||
content: msg.replyContent,
|
||||
courseId: msg.courseId
|
||||
})
|
||||
|
||||
// 模拟发送成功
|
||||
message.success('回复发送成功')
|
||||
msg.showReplyBox = false
|
||||
msg.replyContent = ''
|
||||
|
||||
// 重新加载消息
|
||||
await loadMessages()
|
||||
} catch (error) {
|
||||
console.error('发送回复失败:', error)
|
||||
message.error('发送回复失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMessage = async (messageId: string) => {
|
||||
try {
|
||||
// TODO: 调用API删除评论
|
||||
console.log('删除评论:', messageId)
|
||||
|
||||
// 模拟删除成功
|
||||
message.success('删除成功')
|
||||
|
||||
// 重新加载消息
|
||||
await loadMessages()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const reportMessage = async (messageId: string) => {
|
||||
try {
|
||||
// TODO: 调用API举报评论
|
||||
console.log('举报评论:', messageId)
|
||||
|
||||
message.success('举报已提交')
|
||||
} catch (error) {
|
||||
console.error('举报失败:', error)
|
||||
message.error('举报失败')
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,6 +369,7 @@ const goToPage = (page: number) => {
|
||||
loadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -338,7 +465,7 @@ const goToPage = (page: number) => {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-btns{
|
||||
.message-btns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -372,6 +499,82 @@ const goToPage = (page: number) => {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.action-btn.liked {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.action-btn.favorited {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 11px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top: 3px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
.action-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@ -9,26 +9,46 @@
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<div class="contacts-list">
|
||||
<div v-for="contact in contacts" :key="contact.id" class="contact-item"
|
||||
:class="{ active: contact.id === activeContactId, unread: contact.unreadCount > 0 }"
|
||||
@click="selectContact(contact.id)">
|
||||
<div class="contact-avatar">
|
||||
<img :src="contact.avatar" :alt="contact.name" />
|
||||
<div v-if="contact.type === 'group'" class="group-indicator">
|
||||
<n-icon size="12" color="#fff">
|
||||
<PeopleOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="contacts.length === 0 && !loading" class="contacts-empty">
|
||||
<div class="empty-content">
|
||||
<n-icon size="48" color="#d9d9d9">
|
||||
<ChatbubbleEllipsesOutline />
|
||||
</n-icon>
|
||||
<p class="empty-text">暂无会话</p>
|
||||
<p class="empty-desc">还没有任何聊天会话</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info">
|
||||
<div class="contact-header">
|
||||
<span class="contact-name">{{ contact.name }}</span>
|
||||
<span class="contact-time">{{ contact.lastMessageTime }}</span>
|
||||
<!-- 联系人列表 -->
|
||||
<div v-else>
|
||||
<div v-for="contact in contacts" :key="contact.id" class="contact-item"
|
||||
:class="{ active: contact.id === activeContactId, unread: contact.unreadCount > 0 }"
|
||||
@click="selectContact(contact.id)">
|
||||
<div class="contact-avatar">
|
||||
<img v-if="contact.avatar" :src="contact.avatar" :alt="contact.name" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ contact.name.charAt(0) }}
|
||||
</div>
|
||||
<div v-if="contact.type === 'group'" class="group-indicator">
|
||||
<n-icon size="12" color="#fff">
|
||||
<PeopleOutline />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-preview">
|
||||
<span class="last-message">{{ contact.lastMessage }}</span>
|
||||
<n-badge v-if="contact.unreadCount > 0" :value="contact.unreadCount" :max="99" class="unread-badge" />
|
||||
|
||||
<div class="contact-info">
|
||||
<div class="contact-header">
|
||||
<span class="contact-name">
|
||||
{{ contact.name }}
|
||||
<span v-if="contact.type === 'group'" class="member-count">({{ contact.memberCount || 0 }}人)</span>
|
||||
</span>
|
||||
<span class="contact-time">{{ contact.lastMessageTime }}</span>
|
||||
</div>
|
||||
<div class="contact-preview">
|
||||
<span class="last-message">{{ contact.lastMessage }}</span>
|
||||
<n-badge v-if="contact.unreadCount > 0" :value="contact.unreadCount" :max="99" class="unread-badge" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +72,11 @@
|
||||
<div></div>
|
||||
<div class="chat-user-info">
|
||||
<div class="chat-user-details">
|
||||
<h4 class="chat-user-name">{{ activeContact?.name }}</h4>
|
||||
<h4 class="chat-user-name">
|
||||
{{ activeContact?.name }}
|
||||
<span v-if="activeContact?.type === 'group'" class="member-count">({{ activeContact?.memberCount || 0
|
||||
}}人)</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-actions">
|
||||
@ -67,44 +91,68 @@
|
||||
<!-- 聊天消息区域 -->
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div class="messages-content">
|
||||
<div v-for="message in currentMessages" :key="message.id" class="message-wrapper">
|
||||
<!-- 日期分隔符 -->
|
||||
<div v-if="message.showDateDivider" class="date-divider">
|
||||
<span class="date-text">{{ message.dateText }}</span>
|
||||
<!-- 消息空状态 -->
|
||||
<div v-if="currentMessages.length === 0 && !messagesLoading" class="messages-empty">
|
||||
<div class="empty-content">
|
||||
<n-icon size="48" color="#d9d9d9">
|
||||
<ChatbubbleEllipsesOutline />
|
||||
</n-icon>
|
||||
<p class="empty-text">暂无消息</p>
|
||||
<p class="empty-desc">开始你们的对话吧</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-item" :class="{ 'message-own': message.isOwn }">
|
||||
<div v-if="!message.isOwn" class="message-avatar">
|
||||
<img :src="message.avatar" :alt="message.senderName" />
|
||||
<!-- 消息列表 -->
|
||||
<div v-else>
|
||||
<div v-for="message in currentMessages" :key="message.id" class="message-wrapper">
|
||||
<!-- 日期分隔符 -->
|
||||
<div v-if="message.showDateDivider" class="date-divider">
|
||||
<span class="date-text">{{ message.dateText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="message-content">
|
||||
<div v-if="!message.isOwn" class="message-sender">{{ message.senderName }}</div>
|
||||
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="message.type === 'text'" class="message-bubble">
|
||||
<p class="message-text">{{ message.content }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 图片消息 -->
|
||||
<div v-else-if="message.type === 'image'" class="message-bubble image-bubble">
|
||||
<img :src="message.content" class="message-image" @click="previewImage(message.content)" />
|
||||
</div>
|
||||
|
||||
<!-- 文件消息 -->
|
||||
<div v-else-if="message.type === 'file'" class="message-bubble file-bubble">
|
||||
<div class="file-info">
|
||||
<n-icon size="20" color="#1890ff">
|
||||
<DocumentOutline />
|
||||
</n-icon>
|
||||
<div class="file-details">
|
||||
<span class="file-name">{{ message.fileName }}</span>
|
||||
<span class="file-size">{{ message.fileSize }}</span>
|
||||
</div>
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-item" :class="{ 'message-own': message.isOwn }">
|
||||
<div v-if="!message.isOwn" class="message-avatar">
|
||||
<img v-if="message.avatar" :src="message.avatar" :alt="message.senderName" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ message.senderName.charAt(0) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-content">
|
||||
<div v-if="!message.isOwn" class="message-sender">{{ message.senderName }}</div>
|
||||
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="message.type === 'text'" class="message-bubble">
|
||||
<p class="message-text">{{ message.content }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 图片消息 -->
|
||||
<div v-else-if="message.type === 'image'" class="message-bubble image-bubble">
|
||||
<img :src="message.content" class="message-image" @click="previewImage(message.content)" />
|
||||
</div>
|
||||
|
||||
<!-- 文件消息 -->
|
||||
<div v-else-if="message.type === 'file'" class="message-bubble file-bubble">
|
||||
<div class="file-info">
|
||||
<n-icon size="20" color="#1890ff">
|
||||
<DocumentOutline />
|
||||
</n-icon>
|
||||
<div class="file-details">
|
||||
<span class="file-name">{{ message.fileName }}</span>
|
||||
<span class="file-size">{{ message.fileSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-time">
|
||||
{{ message.time }}
|
||||
<span v-if="message.isOwn" class="read-status"
|
||||
:class="{ 'read': message.isRead, 'unread': !message.isRead }">
|
||||
{{ message.isRead ? '已读' : '未读' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-time">{{ message.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,7 +170,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||
import { NIcon, NBadge } from 'naive-ui'
|
||||
import { NIcon, NBadge, useMessage } from 'naive-ui'
|
||||
import {
|
||||
EllipsisVertical,
|
||||
PeopleOutline,
|
||||
@ -130,10 +178,11 @@ import {
|
||||
DocumentOutline
|
||||
} from '@vicons/ionicons5'
|
||||
import MessageInput from './MessageInput.vue'
|
||||
import { ChatApi } from '@/api'
|
||||
|
||||
// 联系人类型定义
|
||||
// 联系人类型定义(兼容API返回的数据)
|
||||
interface Contact {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
type: 'user' | 'group'
|
||||
@ -144,148 +193,182 @@ interface Contact {
|
||||
memberCount?: number
|
||||
}
|
||||
|
||||
// 消息类型定义
|
||||
// 消息类型定义(兼容API返回的数据)
|
||||
interface Message {
|
||||
id: number
|
||||
contactId: number
|
||||
id: string
|
||||
contactId: string
|
||||
type: 'text' | 'image' | 'file'
|
||||
content: string
|
||||
senderName: string
|
||||
avatar: string
|
||||
time: string
|
||||
isOwn: boolean
|
||||
isRead: boolean // 消息是否已读
|
||||
showDateDivider?: boolean
|
||||
dateText?: string
|
||||
fileName?: string
|
||||
fileSize?: string
|
||||
fileUrl?: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const activeContactId = ref<number | null>(null)
|
||||
const activeContactId = ref<string | null>(null)
|
||||
const messagesContainer = ref<HTMLElement>()
|
||||
const messageInputRef = ref()
|
||||
const message = useMessage()
|
||||
|
||||
// 模拟联系人数据
|
||||
const contacts = ref<Contact[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '李小多',
|
||||
avatar: 'https://picsum.photos/40/40?random=1',
|
||||
type: 'user',
|
||||
lastMessage: '这里是智慧你人的语法数字和信息',
|
||||
lastMessageTime: '10:22',
|
||||
unreadCount: 0,
|
||||
isOnline: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '直播学习小组群 (9)',
|
||||
avatar: 'https://picsum.photos/40/40?random=2',
|
||||
type: 'group',
|
||||
lastMessage: '这里新是智慧你人的语法数字和信息',
|
||||
lastMessageTime: '2024年7月23日',
|
||||
unreadCount: 0,
|
||||
memberCount: 9
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王明',
|
||||
avatar: 'https://picsum.photos/40/40?random=3',
|
||||
type: 'user',
|
||||
lastMessage: '好的,我知道了',
|
||||
lastMessageTime: '昨天',
|
||||
unreadCount: 2,
|
||||
isOnline: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '张老师',
|
||||
avatar: 'https://picsum.photos/40/40?random=4',
|
||||
type: 'user',
|
||||
lastMessage: '明天的课程安排已经发布',
|
||||
lastMessageTime: '昨天',
|
||||
unreadCount: 1,
|
||||
isOnline: true
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '陆娜娜',
|
||||
avatar: 'https://picsum.photos/40/40?random=5',
|
||||
type: 'user',
|
||||
lastMessage: '课程资料我已经整理好了',
|
||||
lastMessageTime: '昨天',
|
||||
unreadCount: 0,
|
||||
isOnline: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '李科度',
|
||||
avatar: 'https://picsum.photos/40/40?random=6',
|
||||
type: 'user',
|
||||
lastMessage: '下次见面详谈',
|
||||
lastMessageTime: '昨天',
|
||||
unreadCount: 0,
|
||||
isOnline: true
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '王小滑',
|
||||
avatar: 'https://picsum.photos/40/40?random=7',
|
||||
type: 'user',
|
||||
lastMessage: '收到,谢谢!',
|
||||
lastMessageTime: '昨天',
|
||||
unreadCount: 0,
|
||||
isOnline: false
|
||||
}
|
||||
])
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const messagesLoading = ref(false)
|
||||
|
||||
// 模拟消息数据
|
||||
const messages = ref<Message[]>([
|
||||
{
|
||||
id: 1,
|
||||
contactId: 1,
|
||||
type: 'text',
|
||||
content: '这里新是智慧你人的语法数字和信息章,多归程回目记录',
|
||||
senderName: '李小多',
|
||||
avatar: 'https://picsum.photos/40/40?random=1',
|
||||
time: '10:22',
|
||||
isOwn: false,
|
||||
showDateDivider: true,
|
||||
dateText: '2024年7月23日'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
contactId: 1,
|
||||
type: 'text',
|
||||
content: '收到',
|
||||
senderName: '我',
|
||||
avatar: 'https://picsum.photos/40/40?random=me',
|
||||
time: '10:23',
|
||||
isOwn: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
contactId: 2,
|
||||
type: 'image',
|
||||
content: 'https://picsum.photos/300/200?random=1',
|
||||
senderName: '张三',
|
||||
avatar: 'https://picsum.photos/40/40?random=8',
|
||||
time: '10:25',
|
||||
isOwn: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
contactId: 2,
|
||||
type: 'file',
|
||||
content: '',
|
||||
senderName: '李四',
|
||||
avatar: 'https://picsum.photos/40/40?random=9',
|
||||
time: '10:30',
|
||||
isOwn: false,
|
||||
fileName: '2025年全家爱词学习人工智能老师考级试卷-点击下载.pptx',
|
||||
fileSize: '2.5MB'
|
||||
// 联系人数据(从API获取)
|
||||
const contacts = ref<Contact[]>([])
|
||||
|
||||
// 当前会话的消息数据
|
||||
const messages = ref<Message[]>([])
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadContacts()
|
||||
})
|
||||
|
||||
// 加载联系人列表
|
||||
const loadContacts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await ChatApi.getMyChats()
|
||||
if (response.data && response.data.success) {
|
||||
// 转换API数据为组件需要的格式
|
||||
contacts.value = response.data.result.map((chat: any) => {
|
||||
// 根据API返回的数字类型进行判断:0=私聊,1=群聊
|
||||
const contactType = chat.type === 1 ? 'group' : 'user'
|
||||
|
||||
return {
|
||||
id: chat.id,
|
||||
name: chat.name,
|
||||
avatar: chat.avatar || '', // 如果没有头像就不显示
|
||||
type: contactType,
|
||||
lastMessage: chat.lastMessage || '暂无消息',
|
||||
lastMessageTime: formatTime(chat.lastMessageTime || chat.updateTime),
|
||||
unreadCount: chat.unreadCount || 0,
|
||||
isOnline: chat.isOnline,
|
||||
memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果是群聊且没有memberCount,尝试获取群成员数量
|
||||
for (const contact of contacts.value) {
|
||||
if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) {
|
||||
loadGroupMemberCount(contact.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取我的会话失败:', error)
|
||||
message.error('获取会话列表失败')
|
||||
contacts.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 加载群聊成员数量
|
||||
const loadGroupMemberCount = async (chatId: string) => {
|
||||
try {
|
||||
const response = await ChatApi.getChatMembers(chatId)
|
||||
|
||||
if (response.data && response.data.success && response.data.result) {
|
||||
const memberCount = response.data.result.length
|
||||
|
||||
// 更新对应联系人的成员数量
|
||||
const contact = contacts.value.find(c => c.id === chatId)
|
||||
if (contact) {
|
||||
contact.memberCount = memberCount
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取群聊成员数量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指定会话的消息
|
||||
const loadMessages = async (chatId: string) => {
|
||||
messagesLoading.value = true
|
||||
try {
|
||||
console.log('🚀 开始获取会话消息,chatId:', chatId)
|
||||
const response = await ChatApi.getChatMessages(chatId)
|
||||
console.log('✅ 会话消息API响应:', response)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
console.log('📝 消息数据:', response.data.result)
|
||||
// 转换API数据为组件需要的格式
|
||||
messages.value = response.data.result.map((msg: any): Message => {
|
||||
console.log('🔍 处理消息:', msg)
|
||||
console.log('🔍 消息类型:', msg.messageType, '发送者信息:', msg.senderInfo)
|
||||
|
||||
// 根据messageType数字判断消息类型:0=文本,1=图片,2=文件
|
||||
let messageType = 'text'
|
||||
if (msg.messageType === 1) {
|
||||
messageType = 'image'
|
||||
} else if (msg.messageType === 2) {
|
||||
messageType = 'file'
|
||||
}
|
||||
|
||||
return {
|
||||
id: msg.id,
|
||||
contactId: msg.chatId,
|
||||
type: messageType as 'text' | 'image' | 'file',
|
||||
content: msg.content,
|
||||
senderName: msg.senderInfo?.realname || '未知用户',
|
||||
avatar: msg.senderInfo?.avatar || '',
|
||||
time: formatTime(msg.createTime),
|
||||
isOwn: false, // TODO: 需要根据当前用户ID判断
|
||||
isRead: msg.isRead || false, // 消息是否已读
|
||||
fileName: msg.fileName,
|
||||
fileSize: msg.fileSize,
|
||||
fileUrl: msg.fileUrl
|
||||
}
|
||||
})
|
||||
console.log('✅ 转换后的消息列表:', messages.value)
|
||||
} else {
|
||||
console.warn('⚠️ API返回失败:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取会话消息失败:', error)
|
||||
message.error('获取消息失败')
|
||||
messages.value = []
|
||||
} finally {
|
||||
messagesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 时间格式化函数
|
||||
const formatTime = (timeStr: string) => {
|
||||
if (!timeStr) return ''
|
||||
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
// 如果是今天
|
||||
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 如果是昨天
|
||||
if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
|
||||
return '昨天'
|
||||
}
|
||||
|
||||
// 如果是今年
|
||||
if (date.getFullYear() === now.getFullYear()) {
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
||||
}
|
||||
|
||||
// 其他情况显示完整日期
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
|
||||
// 计算属性
|
||||
const activeContact = computed(() => {
|
||||
@ -297,46 +380,83 @@ const currentMessages = computed(() => {
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const selectContact = (contactId: number) => {
|
||||
const selectContact = async (contactId: string) => {
|
||||
activeContactId.value = contactId
|
||||
|
||||
// 清除未读数量
|
||||
const contact = contacts.value.find(c => c.id === contactId)
|
||||
const contact = contacts.value.find((c: Contact) => c.id === contactId)
|
||||
if (contact) {
|
||||
contact.unreadCount = 0
|
||||
|
||||
// 如果是群聊且没有成员数量,尝试获取
|
||||
if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) {
|
||||
loadGroupMemberCount(contactId)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载该会话的消息
|
||||
await loadMessages(contactId)
|
||||
|
||||
// 标记消息为已读
|
||||
try {
|
||||
await ChatApi.markAsRead(contactId)
|
||||
} catch (error) {
|
||||
console.warn('标记消息已读失败:', error)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSendMessage = (content: string) => {
|
||||
const handleSendMessage = async (content: string) => {
|
||||
if (!activeContactId.value) return
|
||||
|
||||
const newMessage: Message = {
|
||||
id: Date.now(),
|
||||
id: Date.now().toString(),
|
||||
contactId: activeContactId.value,
|
||||
type: 'text',
|
||||
content,
|
||||
senderName: '我',
|
||||
avatar: 'https://picsum.photos/40/40?random=me',
|
||||
avatar: '',
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
isOwn: true
|
||||
isOwn: true,
|
||||
isRead: false // 新发送的消息默认为未读
|
||||
}
|
||||
|
||||
// 先添加到本地消息列表
|
||||
messages.value.push(newMessage)
|
||||
|
||||
// 更新联系人最后消息
|
||||
const contact = contacts.value.find(c => c.id === activeContactId.value)
|
||||
const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value)
|
||||
if (contact) {
|
||||
contact.lastMessage = content
|
||||
contact.lastMessageTime = newMessage.time
|
||||
}
|
||||
|
||||
// 发送到服务器
|
||||
try {
|
||||
await ChatApi.sendMessage({
|
||||
chatId: activeContactId.value,
|
||||
content,
|
||||
messageType: 'text'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
message.error('发送消息失败')
|
||||
// 如果发送失败,可以从本地消息列表中移除
|
||||
const index = messages.value.findIndex(msg => msg.id === newMessage.id)
|
||||
if (index > -1) {
|
||||
messages.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
@ -455,6 +575,20 @@ onMounted(() => {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.group-indicator {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
@ -490,6 +624,13 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.contact-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
@ -515,6 +656,57 @@ onMounted(() => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.read-status {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.read-status.read {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.read-status.unread {
|
||||
background: #fff2e8;
|
||||
color: #fa8c16;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.contacts-empty,
|
||||
.messages-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.contacts-empty .empty-content,
|
||||
.messages-empty .empty-content {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.contacts-empty .empty-text,
|
||||
.messages-empty .empty-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 12px 0 8px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.contacts-empty .empty-desc,
|
||||
.messages-empty .empty-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 右侧聊天面板 */
|
||||
.chat-panel {
|
||||
flex: 1;
|
||||
@ -574,6 +766,13 @@ onMounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chat-user-details .member-count {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.chat-user-status {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
@ -638,6 +837,20 @@ onMounted(() => {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.message-avatar .avatar-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 60%;
|
||||
}
|
||||
@ -777,4 +990,4 @@ onMounted(() => {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -31,9 +31,8 @@
|
||||
</n-collapse>
|
||||
</div>
|
||||
<div class="class-right">
|
||||
<ClassManagement ref="classManagementRef" :class-id="activeClassId"
|
||||
:class-name="classList.find(item => item.id === activeClassId)?.name"
|
||||
@class-changed="handleClassChanged" />
|
||||
<ClassManagement ref="classManagementRef" type="student" :class-id="activeClassId"
|
||||
:class-name="classList.find(item => item.id === activeClassId)?.name" @class-changed="handleClassChanged" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -44,10 +43,10 @@ import { CaretForward, EllipsisVertical } from '@vicons/ionicons5'
|
||||
import { onMounted, ref } from "vue"
|
||||
import { ClassApi } from '@/api/modules/teachCourse'
|
||||
|
||||
const classList = ref<Array<{ id: number; name: string }>>([])
|
||||
const classList = ref<Array<{ id: string; name: string }>>([])
|
||||
|
||||
// 当前激活的班级ID
|
||||
const activeClassId = ref<number | null>(1)
|
||||
const activeClassId = ref<string | null>('1')
|
||||
|
||||
// 引用ClassManagement组件
|
||||
const classManagementRef = ref<InstanceType<typeof ClassManagement> | null>(null)
|
||||
@ -77,7 +76,7 @@ const getClassItemOptions = () => [
|
||||
]
|
||||
|
||||
// 点击班级项的处理函数
|
||||
const handleClassClick = (classId: number) => {
|
||||
const handleClassClick = (classId: string) => {
|
||||
console.log(`🖱️ 用户点击班级: ${classId}, 当前激活班级: ${activeClassId.value}`)
|
||||
if (activeClassId.value !== classId) {
|
||||
activeClassId.value = classId
|
||||
@ -98,7 +97,7 @@ const handleClassMenuSelect = (value: string) => {
|
||||
}
|
||||
|
||||
// 处理班级项菜单选择
|
||||
const handleClassItemMenuSelect = (value: string, classId: number) => {
|
||||
const handleClassItemMenuSelect = (value: string, classId: string) => {
|
||||
if (!classManagementRef.value) return
|
||||
|
||||
const selectedClass = classList.value.find(item => item.id === classId)
|
||||
|
Loading…
x
Reference in New Issue
Block a user