feat: 修复班级管理报错;添加课程内容,字幕列表页面;即时消息接入部分接口且完善接口所需样式
This commit is contained in:
parent
62143affd5
commit
b2ec1e2015
@ -201,5 +201,37 @@ export const ChatApi = {
|
||||
*/
|
||||
getChatDetail: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.get(`/aiol/aiolChat/${chatId}/detail`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 开启显示教师标签
|
||||
* POST /aiol/aiolChat/{chatId}/show_label
|
||||
*/
|
||||
showLabel: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/show_label`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭显示教师标签
|
||||
* POST /aiol/aiolChat/{chatId}/hide_label
|
||||
*/
|
||||
hideLabel: (chatId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/hide_label`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 禁言会话用户
|
||||
* POST /aiol/aiolChat/{chatId}/mute_member/{userId}
|
||||
*/
|
||||
muteMember: (chatId: string, userId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/mute_member/${userId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 解除禁言会话用户
|
||||
* POST /aiol/aiolChat/{chatId}/unmute_member/{userId}
|
||||
*/
|
||||
unmuteMember: (chatId: string, userId: string): Promise<ApiResponse<any>> => {
|
||||
return ApiRequest.post(`/aiol/aiolChat/${chatId}/unmute_member/${userId}`)
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,34 @@
|
||||
|
||||
<n-data-table :columns="columns" :data="paginatedData" :pagination="pagination" :loading="loading"
|
||||
:row-key="(row: StudentItem) => row.id" v-model:checked-row-keys="selectedRowKeys" striped bordered
|
||||
size="small" />
|
||||
size="small">
|
||||
<template #empty>
|
||||
<div class="custom-empty">
|
||||
<n-empty v-if="!selectedDepartment && !props.classId" description="请先选择班级查看学员信息">
|
||||
<template #icon>
|
||||
<n-icon size="48" color="#ccc">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 6.5V7.5C15 8.3 14.3 9 13.5 9H10.5C9.7 9 9 8.3 9 7.5V6.5L3 7V9L9 8.5V9.5C9 10.3 9.7 11 10.5 11H13.5C14.3 11 15 10.3 15 9.5V8.5L21 9ZM6 13.5C6 12.7 6.7 12 7.5 12H16.5C17.3 12 18 12.7 18 13.5V20.5C18 21.3 17.3 22 16.5 22H7.5C6.7 22 6 21.3 6 20.5V13.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
<n-empty v-else description="暂无学员数据">
|
||||
<template #icon>
|
||||
<n-icon size="48" color="#ccc">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 6.5V7.5C15 8.3 14.3 9 13.5 9H10.5C9.7 9 9 8.3 9 7.5V6.5L3 7V9L9 8.5V9.5C9 10.3 9.7 11 10.5 11H13.5C14.3 11 15 10.3 15 9.5V8.5L21 9ZM6 13.5C6 12.7 6.7 12 7.5 12H16.5C17.3 12 18 12.7 18 13.5V20.5C18 21.3 17.3 22 16.5 22H7.5C6.7 22 6 21.3 6 20.5V13.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
|
||||
<!-- 添加班级弹窗 -->
|
||||
<n-modal v-model:show="showAddClassModal" :title="isRenameMode ? '重命名' : '添加班级'">
|
||||
@ -472,7 +499,7 @@ const paginatedData = computed(() => {
|
||||
|
||||
// 部门选项(用于页面顶部筛选)
|
||||
const departmentOptions = computed(() => [
|
||||
{ label: '默认班级', value: '' },
|
||||
{ label: '请选择班级', value: '' },
|
||||
...masterClassList.value.map(item => ({
|
||||
label: item.className,
|
||||
value: item.id
|
||||
@ -1239,8 +1266,9 @@ const loadData = async (classId?: string | number | null) => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
if (classId === null || classId === undefined) {
|
||||
if (classId === null || classId === undefined || classId === '' || classId === 'undefined') {
|
||||
// 未选择班级时显示空数据
|
||||
console.log('📋 未选择班级,显示空数据')
|
||||
data.value = []
|
||||
totalStudents.value = 0
|
||||
} else {
|
||||
@ -1367,7 +1395,16 @@ onMounted(async () => {
|
||||
|
||||
// 初始加载时,优先使用使用传入的classId,其次使用选择器的值
|
||||
const initialClassId = props.classId ? props.classId : selectedDepartment.value
|
||||
loadData(initialClassId)
|
||||
|
||||
// 只有当classId有效时才加载数据
|
||||
if (initialClassId && initialClassId !== '' && initialClassId !== 'undefined') {
|
||||
loadData(initialClassId)
|
||||
} else {
|
||||
// 如果没有有效的classId,显示空状态
|
||||
data.value = []
|
||||
totalStudents.value = 0
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 获取课程id 只有课程管理下有课程id
|
||||
if (route.path.includes('/teacher/course-editor')) {
|
||||
@ -1402,7 +1439,7 @@ defineExpose({
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.student-title{
|
||||
.student-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
@ -1503,7 +1540,7 @@ defineExpose({
|
||||
|
||||
.invite-code-display {
|
||||
text-align: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.invite-title {
|
||||
@ -1712,4 +1749,14 @@ defineExpose({
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* 自定义空状态样式 */
|
||||
.custom-empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-empty .n-empty {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
304
src/components/teacher/CourseContentManagement.vue
Normal file
304
src/components/teacher/CourseContentManagement.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="course-content-management">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<n-select
|
||||
v-model:value="selectedCourse"
|
||||
:options="courseOptions"
|
||||
placeholder="请选择课程"
|
||||
style="width: 200px"
|
||||
@update:value="handleCourseChange"
|
||||
/>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="showAddSummaryModal = true">
|
||||
添加总结
|
||||
</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 内容管理区域 -->
|
||||
<div class="content-area">
|
||||
<div class="summary-section">
|
||||
<n-data-table
|
||||
:columns="summaryColumns"
|
||||
:data="filteredSummaryList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-key="(row: any) => row.timestamp + row.title"
|
||||
striped
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加总结弹窗 -->
|
||||
<n-modal v-model:show="showAddSummaryModal" title="添加课程总结">
|
||||
<n-card style="width: 600px" title="添加课程总结" :bordered="false" size="huge">
|
||||
<n-form ref="summaryFormRef" :model="summaryForm" :rules="summaryRules" label-placement="left" label-width="auto">
|
||||
<n-form-item label="时间戳" path="timestamp">
|
||||
<n-input v-model:value="summaryForm.timestamp" placeholder="例如: 00:23" />
|
||||
</n-form-item>
|
||||
<n-form-item label="标题" path="title">
|
||||
<n-input v-model:value="summaryForm.title" placeholder="请输入总结标题" />
|
||||
</n-form-item>
|
||||
<n-form-item label="内容" path="description">
|
||||
<n-input
|
||||
v-model:value="summaryForm.description"
|
||||
type="textarea"
|
||||
placeholder="请输入总结内容"
|
||||
:rows="4"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="showAddSummaryModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="handleAddSummary">确定</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import {
|
||||
NSelect,
|
||||
NButton,
|
||||
NModal,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NDataTable,
|
||||
NSpace,
|
||||
useMessage,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedCourse = ref('')
|
||||
const showAddSummaryModal = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单引用
|
||||
const summaryFormRef = ref<FormInst | null>(null)
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
loadData()
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
|
||||
// 课程选项
|
||||
const courseOptions = ref([
|
||||
{ label: '职业探索与选择', value: 'course1' },
|
||||
{ label: '软件工程导论', value: 'course2' },
|
||||
{ label: '数据结构与算法', value: 'course3' }
|
||||
])
|
||||
|
||||
// 总结数据
|
||||
const summaryList = ref([
|
||||
{
|
||||
timestamp: '00:23',
|
||||
title: '职业探索与选择:追求卓越与实现自我价值',
|
||||
description: '本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。'
|
||||
},
|
||||
{
|
||||
timestamp: '00:45',
|
||||
title: '职业探索与选择:追求卓越与实现自我价值',
|
||||
description: '本次课程旨在引导学生探索自身的职业目标及价值观,强调了根据个人兴趣和优势做出职业选择的重要性。通过分享不同领域的职场榜样,如一位对C罗的远见和执行力表示赞赏的学生、另一位崇拜Elon Musk的学习能力和创新精神、以及第三位钦佩俞敏洪的社会洞察力和团队合作精神,课程鼓励学生思考并追求自己理想中的职业生涯。这些榜样人物不仅在各自领域取得了巨大成功,而且还展现了持续学习、勇于创新和积极影响社会的价值观。'
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
// 表单数据
|
||||
const summaryForm = ref({
|
||||
timestamp: '',
|
||||
title: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const summaryRules: FormRules = {
|
||||
timestamp: [
|
||||
{ required: true, message: '请输入时间戳', trigger: 'blur' }
|
||||
],
|
||||
title: [
|
||||
{ required: true, message: '请输入标题', trigger: 'blur' }
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const summaryColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: '时间戳',
|
||||
key: 'timestamp',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (row: any) => {
|
||||
return h('span', {
|
||||
style: {
|
||||
background: '#1890ff',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
}
|
||||
}, row.timestamp)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
width: 300,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (row: any, index: number) => {
|
||||
return h(NSpace, {
|
||||
size: 'small',
|
||||
justify: 'center'
|
||||
}, {
|
||||
default: () => [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
ghost: true,
|
||||
onClick: () => editSummary(row)
|
||||
}, { default: () => '编辑' }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
ghost: true,
|
||||
onClick: () => deleteSummary(index)
|
||||
}, { default: () => '删除' })
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredSummaryList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return summaryList.value
|
||||
}
|
||||
return summaryList.value.filter((item: any) =>
|
||||
item.title.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.description.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleCourseChange = (courseId: string) => {
|
||||
console.log('切换课程:', courseId)
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 这里可以加载对应课程的内容数据
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
message.error('加载数据失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索功能已通过计算属性实现
|
||||
console.log('搜索关键词:', searchKeyword.value)
|
||||
}
|
||||
|
||||
const editSummary = (item: any) => {
|
||||
summaryForm.value = { ...item }
|
||||
showAddSummaryModal.value = true
|
||||
}
|
||||
|
||||
const deleteSummary = (index: number) => {
|
||||
summaryList.value.splice(index, 1)
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
const handleAddSummary = async () => {
|
||||
try {
|
||||
await summaryFormRef.value?.validate()
|
||||
summaryList.value.push({ ...summaryForm.value })
|
||||
showAddSummaryModal.value = false
|
||||
summaryForm.value = { timestamp: '', title: '', description: '' }
|
||||
message.success('添加成功')
|
||||
} catch (error) {
|
||||
message.error('请检查表单信息')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
if (courseOptions.value.length > 0) {
|
||||
selectedCourse.value = courseOptions.value[0].value
|
||||
}
|
||||
// 立即加载数据
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.course-content-management {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
304
src/components/teacher/SubtitleManagement.vue
Normal file
304
src/components/teacher/SubtitleManagement.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="subtitle-management">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<n-select
|
||||
v-model:value="selectedCourse"
|
||||
:options="courseOptions"
|
||||
placeholder="请选择课程"
|
||||
style="width: 200px"
|
||||
@update:value="handleCourseChange"
|
||||
/>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="showAddSubtitleModal = true">
|
||||
添加字幕
|
||||
</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 字幕管理区域 -->
|
||||
<div class="content-area">
|
||||
<div class="subtitles-section">
|
||||
<n-data-table
|
||||
:columns="subtitleColumns"
|
||||
:data="filteredSubtitlesList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-key="(row: any) => row.startTime + row.endTime + row.text"
|
||||
striped
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加字幕弹窗 -->
|
||||
<n-modal v-model:show="showAddSubtitleModal" title="添加字幕">
|
||||
<n-card style="width: 600px" title="添加字幕" :bordered="false" size="huge">
|
||||
<n-form ref="subtitleFormRef" :model="subtitleForm" :rules="subtitleRules" label-placement="left" label-width="auto">
|
||||
<n-form-item label="开始时间" path="startTime">
|
||||
<n-input v-model:value="subtitleForm.startTime" placeholder="例如: 00:23" />
|
||||
</n-form-item>
|
||||
<n-form-item label="结束时间" path="endTime">
|
||||
<n-input v-model:value="subtitleForm.endTime" placeholder="例如: 00:45" />
|
||||
</n-form-item>
|
||||
<n-form-item label="字幕内容" path="text">
|
||||
<n-input
|
||||
v-model:value="subtitleForm.text"
|
||||
type="textarea"
|
||||
placeholder="请输入字幕内容"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<n-button @click="showAddSubtitleModal = false">取消</n-button>
|
||||
<n-button type="primary" @click="handleAddSubtitle">确定</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import {
|
||||
NSelect,
|
||||
NButton,
|
||||
NModal,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NDataTable,
|
||||
NSpace,
|
||||
useMessage,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedCourse = ref('')
|
||||
const showAddSubtitleModal = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单引用
|
||||
const subtitleFormRef = ref<FormInst | null>(null)
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
loadData()
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
|
||||
// 课程选项
|
||||
const courseOptions = ref([
|
||||
{ label: '职业探索与选择', value: 'course1' },
|
||||
{ label: '软件工程导论', value: 'course2' },
|
||||
{ label: '数据结构与算法', value: 'course3' }
|
||||
])
|
||||
|
||||
// 字幕数据
|
||||
const subtitlesList = ref([
|
||||
{
|
||||
startTime: '00:23',
|
||||
endTime: '00:45',
|
||||
text: '欢迎来到职业探索与选择课程,今天我们将探讨如何追求卓越与实现自我价值。'
|
||||
},
|
||||
{
|
||||
startTime: '00:45',
|
||||
endTime: '01:12',
|
||||
text: '首先,让我们来了解一下职业规划的重要性,以及如何根据个人兴趣和优势做出选择。'
|
||||
},
|
||||
{
|
||||
startTime: '01:12',
|
||||
endTime: '01:35',
|
||||
text: '通过分享不同领域的职场榜样,我们可以学习到成功人士的共同特质。'
|
||||
}
|
||||
])
|
||||
|
||||
// 表单数据
|
||||
const subtitleForm = ref({
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
text: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const subtitleRules: FormRules = {
|
||||
startTime: [
|
||||
{ required: true, message: '请输入开始时间', trigger: 'blur' }
|
||||
],
|
||||
endTime: [
|
||||
{ required: true, message: '请输入结束时间', trigger: 'blur' }
|
||||
],
|
||||
text: [
|
||||
{ required: true, message: '请输入字幕内容', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const subtitleColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: '时间范围',
|
||||
key: 'timeRange',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (row: any) => {
|
||||
return h('span', {
|
||||
style: {
|
||||
background: '#1890ff',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
}
|
||||
}, `${row.startTime} - ${row.endTime}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '字幕内容',
|
||||
key: 'text'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (row: any, index: number) => {
|
||||
return h(NSpace, {
|
||||
size: 'small',
|
||||
justify: 'center'
|
||||
}, {
|
||||
default: () => [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
ghost: true,
|
||||
onClick: () => editSubtitle(row)
|
||||
}, { default: () => '编辑' }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
ghost: true,
|
||||
onClick: () => deleteSubtitle(index)
|
||||
}, { default: () => '删除' })
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredSubtitlesList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return subtitlesList.value
|
||||
}
|
||||
return subtitlesList.value.filter((item: any) =>
|
||||
item.text.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.startTime.includes(searchKeyword.value) ||
|
||||
item.endTime.includes(searchKeyword.value)
|
||||
)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleCourseChange = (courseId: string) => {
|
||||
console.log('切换课程:', courseId)
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 这里可以加载对应课程的字幕数据
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
message.error('加载数据失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索功能已通过计算属性实现
|
||||
console.log('搜索关键词:', searchKeyword.value)
|
||||
}
|
||||
|
||||
const editSubtitle = (item: any) => {
|
||||
subtitleForm.value = { ...item }
|
||||
showAddSubtitleModal.value = true
|
||||
}
|
||||
|
||||
const deleteSubtitle = (index: number) => {
|
||||
subtitlesList.value.splice(index, 1)
|
||||
message.success('删除成功')
|
||||
}
|
||||
|
||||
const handleAddSubtitle = async () => {
|
||||
try {
|
||||
await subtitleFormRef.value?.validate()
|
||||
subtitlesList.value.push({ ...subtitleForm.value })
|
||||
showAddSubtitleModal.value = false
|
||||
subtitleForm.value = { startTime: '', endTime: '', text: '' }
|
||||
message.success('添加成功')
|
||||
} catch (error) {
|
||||
message.error('请检查表单信息')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
if (courseOptions.value.length > 0) {
|
||||
selectedCourse.value = courseOptions.value[0].value
|
||||
}
|
||||
// 立即加载数据
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subtitle-management {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
@ -191,7 +191,7 @@ const activeSubNavItem = ref(''); // 子菜单激活状态
|
||||
const examMenuExpanded = ref(false); // 考试管理菜单展开状态
|
||||
const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
||||
const orchestrationMenuExpanded = ref(false); // 智能体编排菜单展开状态
|
||||
const showTopImage = ref(true); // 控制顶部图片显示/隐藏
|
||||
const showTopImage = ref(false); // 控制顶部图片显示/隐藏
|
||||
|
||||
// 需要隐藏顶部图片的路由路径数组
|
||||
const hideTopImageRoutes = [
|
||||
@ -800,6 +800,9 @@ onMounted(() => {
|
||||
// 初始设置
|
||||
updateActiveNavItem();
|
||||
|
||||
// 确保广告默认隐藏
|
||||
showTopImage.value = false;
|
||||
|
||||
// 初始化CSS变量
|
||||
if (showTopImage.value) {
|
||||
document.documentElement.style.setProperty('--top-height', '130px');
|
||||
@ -825,7 +828,8 @@ const updateTopImageVisibility = () => {
|
||||
currentPath.includes(routePath)
|
||||
);
|
||||
|
||||
showTopImage.value = !shouldHideTopImage;
|
||||
// 默认隐藏广告,只在特定路由下显示(如果需要的话)
|
||||
showTopImage.value = false; // 默认隐藏
|
||||
console.log('顶部图片显示状态:', showTopImage.value);
|
||||
};
|
||||
|
||||
|
@ -173,7 +173,7 @@ const route = useRoute()
|
||||
const courseId = ref(route.params.id as string)
|
||||
|
||||
// 顶部图片控制
|
||||
const showTopImage = ref(true) // 控制顶部图片显示/隐藏
|
||||
const showTopImage = ref(false) // 控制顶部图片显示/隐藏
|
||||
|
||||
// 课程信息
|
||||
const courseInfo = ref({
|
||||
|
@ -10,6 +10,12 @@
|
||||
<n-tab-pane name="log" tab="操作日志">
|
||||
<OperationLog />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="content" tab="课程内容">
|
||||
<CourseContentManagement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="subtitles" tab="字幕列表">
|
||||
<SubtitleManagement />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@ -20,6 +26,8 @@ import { NTabs, NTabPane } from 'naive-ui'
|
||||
import ClassManagement from '@/components/teacher/ClassManagement.vue'
|
||||
import TeamManagement from '@/components/teacher/TeamManagement.vue'
|
||||
import OperationLog from '@/components/teacher/OperationLog.vue'
|
||||
import CourseContentManagement from '@/components/teacher/CourseContentManagement.vue'
|
||||
import SubtitleManagement from '@/components/teacher/SubtitleManagement.vue'
|
||||
|
||||
// 当前激活的tab
|
||||
const activeTab = ref('class')
|
||||
|
@ -132,7 +132,10 @@
|
||||
</div>
|
||||
|
||||
<div class="message-content">
|
||||
<div v-if="!message.isOwn" class="message-sender">{{ message.senderName }}</div>
|
||||
<div v-if="!message.isOwn" class="message-sender">
|
||||
{{ message.senderName }}
|
||||
<span v-if="shouldShowTeacherLabel(message.senderId)" class="teacher-label">讲师</span>
|
||||
</div>
|
||||
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="message.type === 'text'" class="message-bubble">
|
||||
@ -207,11 +210,14 @@
|
||||
<div class="members-section">
|
||||
<!-- 有搜索结果时显示成员列表 -->
|
||||
<div v-if="displayedMembers.length > 0" class="members-grid">
|
||||
<div v-for="member in displayedMembers" :key="member.id" class="member-item">
|
||||
<div v-for="member in displayedMembers" :key="member.id" class="member-item"
|
||||
@click="openMemberModal(member)">
|
||||
<div class="member-avatar">
|
||||
<img :src="member.avatar || '/images/profile/default-avatar.png'" :alt="member.realname" />
|
||||
</div>
|
||||
<div class="member-name">{{ member.realname }}</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">{{ member.realname }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -268,8 +274,8 @@
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">发言加讲师标签</span>
|
||||
<div class="switch-wrapper">
|
||||
<input type="checkbox" id="addTeacherTag" v-model="groupSettings.addTeacherTag"
|
||||
class="switch-input">
|
||||
<input type="checkbox" id="addTeacherTag" v-model="showTeacherLabel"
|
||||
@change="handleTeacherLabelToggle" class="switch-input">
|
||||
<label for="addTeacherTag" class="switch-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
@ -333,6 +339,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成员操作弹框 -->
|
||||
<div v-if="showMemberModal" class="modal-overlay" @click="closeMemberModal">
|
||||
<div class="member-modal" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>成员操作</h3>
|
||||
<button class="close-btn" @click="closeMemberModal">×</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="member-profile">
|
||||
<div class="member-avatar-large">
|
||||
<img :src="selectedMember?.avatar || '/images/profile/default-avatar.png'"
|
||||
:alt="selectedMember?.realname" />
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<h4>{{ selectedMember?.realname }}</h4>
|
||||
<p v-if="selectedMember?.isTeacher" class="member-role">讲师</p>
|
||||
<p v-else class="member-role">学员</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button v-if="!selectedMember?.isTeacher" @click="handleMuteMember" class="modal-btn mute-btn"
|
||||
:disabled="isMuteLoading">
|
||||
{{ selectedMember?.isMuted ? '解除禁言' : '禁言' }}
|
||||
</button>
|
||||
<button v-if="!selectedMember?.isTeacher" @click="handleRemoveMember" class="modal-btn remove-btn"
|
||||
:disabled="isMuteLoading">
|
||||
移除群聊
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -346,6 +385,7 @@ import {
|
||||
import MessageInput from './MessageInput.vue'
|
||||
import { ChatApi } from '@/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { TeachCourseApi } from '@/api/modules/teachCourse'
|
||||
|
||||
// 联系人类型定义(兼容API返回的数据)
|
||||
interface Contact {
|
||||
@ -359,6 +399,7 @@ interface Contact {
|
||||
isOnline?: boolean
|
||||
memberCount?: number
|
||||
izAllMuted?: boolean | number // 全员禁言状态,支持布尔值和数字(1/0)
|
||||
showLabel?: boolean | number // 是否显示教师标签,支持布尔值和数字(1/0)
|
||||
}
|
||||
|
||||
// 群聊成员类型定义
|
||||
@ -387,6 +428,7 @@ interface Message {
|
||||
type: 'text' | 'image' | 'file'
|
||||
content: string
|
||||
senderName: string
|
||||
senderId: string // 发送者ID
|
||||
avatar: string
|
||||
time: string
|
||||
isOwn: boolean
|
||||
@ -416,6 +458,9 @@ const groupSettings = ref({
|
||||
addTeacherTag: false
|
||||
})
|
||||
|
||||
// 教师标签显示状态
|
||||
const showTeacherLabel = ref(false)
|
||||
|
||||
// 私聊设置状态
|
||||
const privateSettings = ref({
|
||||
messageMute: false,
|
||||
@ -453,6 +498,14 @@ const maxDisplayMembers = 20 // 5行 × 4列 = 20个成员
|
||||
const memberSearchKeyword = ref('')
|
||||
const isSearchingMembers = ref(false)
|
||||
|
||||
// 教师标签相关
|
||||
const teacherUserIds = ref<string[]>([])
|
||||
const currentChatShowLabel = ref(0) // 当前群聊的showLabel状态
|
||||
|
||||
// 成员弹框相关
|
||||
const showMemberModal = ref(false) // 是否显示成员弹框
|
||||
const selectedMember = ref<any>(null) // 选中的成员
|
||||
|
||||
// 计算属性:过滤后的群成员列表
|
||||
const filteredMembers = computed(() => {
|
||||
if (!memberSearchKeyword.value.trim()) {
|
||||
@ -484,14 +537,46 @@ const shouldShowViewMore = computed(() => {
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadContacts()
|
||||
loadTeacherList()
|
||||
})
|
||||
|
||||
// 获取教师用户ID列表
|
||||
const loadTeacherList = async () => {
|
||||
try {
|
||||
const response = await TeachCourseApi.getTeacherList()
|
||||
if (response.data && response.data.result) {
|
||||
teacherUserIds.value = response.data.result.map((teacher: any) => teacher.id || teacher.userId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 获取教师列表失败:', error)
|
||||
// 添加一些测试数据
|
||||
teacherUserIds.value = ['1000000000', '1000000001']
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为教师
|
||||
const isTeacher = (senderId: string) => {
|
||||
return teacherUserIds.value.includes(senderId)
|
||||
}
|
||||
|
||||
// 判断是否显示教师标签
|
||||
const shouldShowTeacherLabel = (senderId: string) => {
|
||||
// 使用showTeacherLabel开关状态来判断是否显示教师标签
|
||||
const showLabel = showTeacherLabel.value
|
||||
const isTeacherUser = isTeacher(senderId)
|
||||
const shouldShow = showLabel && isTeacherUser
|
||||
|
||||
|
||||
return shouldShow
|
||||
}
|
||||
|
||||
// 加载联系人列表
|
||||
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=群聊
|
||||
@ -507,10 +592,12 @@ const loadContacts = async () => {
|
||||
unreadCount: chat.unreadCount || 0,
|
||||
isOnline: chat.isOnline,
|
||||
memberCount: chat.memberCount || (contactType === 'group' ? 0 : undefined),
|
||||
izAllMuted: chat.izAllMuted === 1
|
||||
izAllMuted: chat.izAllMuted === 1,
|
||||
showLabel: chat.showLabel === 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 如果是群聊且没有memberCount,尝试获取群成员数量
|
||||
for (const contact of contacts.value) {
|
||||
if (contact.type === 'group' && (!contact.memberCount || contact.memberCount === 0)) {
|
||||
@ -553,7 +640,6 @@ const loadGroupMembers = async (chatId: string) => {
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
groupMembers.value = response.data.result || []
|
||||
console.log('群成员加载成功:', groupMembers.value.length, '个成员')
|
||||
|
||||
// 从联系人数据中获取群组名称和禁言状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||
@ -566,7 +652,6 @@ const loadGroupMembers = async (chatId: string) => {
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('API响应失败:', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取群成员失败:', error)
|
||||
@ -592,6 +677,8 @@ const loadChatDetail = async (chatId: string) => {
|
||||
if (response.data && response.data.success) {
|
||||
const chatDetail = response.data.result
|
||||
|
||||
// 打印群聊详情信息
|
||||
|
||||
// 更新禁言状态
|
||||
if (chatDetail.izAllMuted !== undefined) {
|
||||
isAllMuted.value = chatDetail.izAllMuted === 1
|
||||
@ -603,6 +690,19 @@ const loadChatDetail = async (chatId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新教师标签显示状态
|
||||
if (chatDetail.showLabel !== undefined) {
|
||||
showTeacherLabel.value = chatDetail.showLabel === 1
|
||||
currentChatShowLabel.value = chatDetail.showLabel
|
||||
} else {
|
||||
// 备用方案:从联系人数据中获取教师标签显示状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === chatId)
|
||||
if (contact && contact.showLabel !== undefined) {
|
||||
showTeacherLabel.value = contact.showLabel === 1 || contact.showLabel === true
|
||||
currentChatShowLabel.value = contact.showLabel === 1 || contact.showLabel === true ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
return chatDetail
|
||||
}
|
||||
} catch (error) {
|
||||
@ -652,20 +752,130 @@ const handleMuteAllToggle = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理教师标签显示切换
|
||||
const handleTeacherLabelToggle = async () => {
|
||||
if (!activeContactId.value) return
|
||||
|
||||
try {
|
||||
if (showTeacherLabel.value) {
|
||||
// 开启显示教师标签
|
||||
await ChatApi.showLabel(activeContactId.value)
|
||||
message.success('已开启教师标签显示')
|
||||
} else {
|
||||
// 关闭显示教师标签
|
||||
await ChatApi.hideLabel(activeContactId.value)
|
||||
message.success('已关闭教师标签显示')
|
||||
}
|
||||
|
||||
// 更新联系人列表中的状态
|
||||
const contact = contacts.value.find((c: Contact) => c.id === activeContactId.value)
|
||||
if (contact) {
|
||||
contact.showLabel = showTeacherLabel.value ? 1 : 0
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 切换教师标签显示状态失败:', error)
|
||||
message.error('操作失败,请重试')
|
||||
// 回滚状态
|
||||
showTeacherLabel.value = !showTeacherLabel.value
|
||||
}
|
||||
}
|
||||
|
||||
// 打开成员操作弹框
|
||||
const openMemberModal = (member: any) => {
|
||||
selectedMember.value = member
|
||||
showMemberModal.value = true
|
||||
}
|
||||
|
||||
// 关闭成员操作弹框
|
||||
const closeMemberModal = () => {
|
||||
showMemberModal.value = false
|
||||
selectedMember.value = null
|
||||
}
|
||||
|
||||
// 处理禁言/解除禁言成员
|
||||
const handleMuteMember = async () => {
|
||||
if (!activeContactId.value || !selectedMember.value) return
|
||||
|
||||
isMuteLoading.value = true
|
||||
|
||||
try {
|
||||
const member = selectedMember.value
|
||||
const isCurrentlyMuted = member.isMuted || false
|
||||
|
||||
if (isCurrentlyMuted) {
|
||||
// 解除禁言
|
||||
console.log('🔓 解除禁言:', { chatId: activeContactId.value, userId: member.id, memberName: member.realname })
|
||||
await ChatApi.unmuteMember(activeContactId.value, member.id)
|
||||
message.success(`已解除 ${member.realname} 的禁言`)
|
||||
|
||||
// 更新本地状态
|
||||
member.isMuted = false
|
||||
} else {
|
||||
// 禁言
|
||||
console.log('🔒 禁言用户:', { chatId: activeContactId.value, userId: member.id, memberName: member.realname })
|
||||
await ChatApi.muteMember(activeContactId.value, member.id)
|
||||
message.success(`已禁言 ${member.realname}`)
|
||||
|
||||
// 更新本地状态
|
||||
member.isMuted = true
|
||||
}
|
||||
|
||||
// 关闭弹框
|
||||
closeMemberModal()
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 禁言操作失败:', error)
|
||||
message.error('操作失败,请重试')
|
||||
} finally {
|
||||
isMuteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理移除成员
|
||||
const handleRemoveMember = async () => {
|
||||
if (!activeContactId.value || !selectedMember.value) return
|
||||
|
||||
isMuteLoading.value = true
|
||||
|
||||
try {
|
||||
const member = selectedMember.value
|
||||
// await ChatApi.removeMember(activeContactId.value, member.id)
|
||||
message.success(`已移除 ${member.realname}`)
|
||||
|
||||
// 从本地列表中移除
|
||||
const index = groupMembers.value.findIndex((m: any) => m.id === member.id)
|
||||
if (index > -1) {
|
||||
groupMembers.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 关闭弹框
|
||||
closeMemberModal()
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 移除成员操作失败:', error)
|
||||
message.error('操作失败,请重试')
|
||||
} finally {
|
||||
isMuteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指定会话的消息
|
||||
const loadMessages = async (chatId: string) => {
|
||||
messagesLoading.value = true
|
||||
try {
|
||||
console.log('🚀 开始获取会话消息,chatId:', chatId)
|
||||
const response = await ChatApi.getChatMessages(chatId)
|
||||
console.log('✅ 会话消息API响应:', response)
|
||||
|
||||
// 确保教师列表已加载
|
||||
if (teacherUserIds.value.length === 0) {
|
||||
await loadTeacherList()
|
||||
}
|
||||
|
||||
const response = await ChatApi.getChatMessages(chatId)
|
||||
if (response.data && response.data.success) {
|
||||
console.log('📝 消息数据:', response.data.result)
|
||||
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'
|
||||
@ -681,6 +891,7 @@ const loadMessages = async (chatId: string) => {
|
||||
type: messageType as 'text' | 'image' | 'file',
|
||||
content: msg.content,
|
||||
senderName: msg.senderInfo?.realname || '未知用户',
|
||||
senderId: msg.senderInfo?.id || msg.senderId || '', // 发送者ID
|
||||
avatar: msg.senderInfo?.avatar || '',
|
||||
time: formatTime(msg.createTime),
|
||||
isOwn: false, // TODO: 需要根据当前用户ID判断
|
||||
@ -690,7 +901,100 @@ const loadMessages = async (chatId: string) => {
|
||||
fileUrl: msg.fileUrl
|
||||
}
|
||||
})
|
||||
console.log('✅ 转换后的消息列表:', messages.value)
|
||||
|
||||
// 添加多条模拟消息用于测试滚动效果
|
||||
const mockMessages: Message[] = [
|
||||
{
|
||||
id: 'mock_1_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '大家好,欢迎来到我们的学习群!',
|
||||
senderName: '李老师',
|
||||
senderId: teacherUserIds.value[0] || '1956303241317928961',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 30).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_2_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '今天我们来学习Vue.js的基础知识',
|
||||
senderName: '王同学',
|
||||
senderId: 'student_001',
|
||||
avatar: '',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 25).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_3_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '老师,Vue的响应式原理是什么?',
|
||||
senderName: '赵同学',
|
||||
senderId: 'student_002',
|
||||
avatar: '',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 20).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_4_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: 'Vue的响应式是通过Object.defineProperty或Proxy实现的',
|
||||
senderName: '张老师',
|
||||
senderId: teacherUserIds.value[0] || '1956303241317928961',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 15).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_5_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '谢谢老师!',
|
||||
senderName: '赵同学',
|
||||
senderId: 'student_002',
|
||||
avatar: '',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 10).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_6_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '不客气,有问题随时提问',
|
||||
senderName: '张老师',
|
||||
senderId: teacherUserIds.value[0] || '1956303241317928961',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date(Date.now() - 1000 * 60 * 5).toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'mock_7_' + Date.now(),
|
||||
contactId: chatId,
|
||||
type: 'text',
|
||||
content: '大家好,我是老师,这是最新的测试消息',
|
||||
senderName: '张老师',
|
||||
senderId: teacherUserIds.value[0] || '1956303241317928961',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date().toISOString()),
|
||||
isOwn: false,
|
||||
isRead: true
|
||||
}
|
||||
]
|
||||
|
||||
// 添加模拟消息到消息列表
|
||||
messages.value.push(...mockMessages)
|
||||
|
||||
console.log('✅ 最终消息列表:', messages.value)
|
||||
|
||||
} else {
|
||||
console.warn('⚠️ API返回失败:', response.data)
|
||||
}
|
||||
@ -700,6 +1004,17 @@ const loadMessages = async (chatId: string) => {
|
||||
messages.value = []
|
||||
} finally {
|
||||
messagesLoading.value = false
|
||||
// 加载完消息后滚动到底部
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
@ -754,6 +1069,9 @@ const selectContact = async (contactId: string) => {
|
||||
memberCount: 0
|
||||
}
|
||||
|
||||
// 设置默认的教师标签显示状态为1(用于测试)
|
||||
currentChatShowLabel.value = 1
|
||||
|
||||
// 清除未读数量
|
||||
const contact = contacts.value.find((c: Contact) => c.id === contactId)
|
||||
if (contact) {
|
||||
@ -795,6 +1113,7 @@ const handleSendMessage = async (content: string) => {
|
||||
type: 'text',
|
||||
content,
|
||||
senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我',
|
||||
senderId: userStore.user?.id?.toString() || '', // 发送者ID
|
||||
avatar: userStore.user?.avatar || '', // 使用真实用户头像
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
isOwn: true,
|
||||
@ -811,17 +1130,17 @@ const handleSendMessage = async (content: string) => {
|
||||
contact.lastMessageTime = newMessage.time
|
||||
}
|
||||
|
||||
// 发送消息后滚动到底部
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// 模拟发送到服务器
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟成功响应
|
||||
console.log('模拟发送消息成功:', {
|
||||
chatId: activeContactId.value,
|
||||
content,
|
||||
messageType: 'text'
|
||||
})
|
||||
|
||||
// 模拟对方回复(随机回复)
|
||||
setTimeout(() => {
|
||||
@ -868,6 +1187,7 @@ const simulateReply = (chatId: string) => {
|
||||
type: 'text',
|
||||
content: randomReply,
|
||||
senderName: '对方',
|
||||
senderId: '2000000000', // 对方用户ID
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644', // 使用示例头像
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
isOwn: false,
|
||||
@ -884,20 +1204,14 @@ const simulateReply = (chatId: string) => {
|
||||
contact.unreadCount = (contact.unreadCount || 0) + 1
|
||||
}
|
||||
|
||||
// 收到回复后滚动到底部
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const previewImage = (src: string) => {
|
||||
const previewImage = (_src: string) => {
|
||||
// TODO: 实现图片预览功能
|
||||
console.log('预览图片:', src)
|
||||
}
|
||||
|
||||
// 获取文件图标
|
||||
@ -914,7 +1228,6 @@ const getFileIcon = (type: string) => {
|
||||
// 下载文件
|
||||
const downloadFile = (message: any) => {
|
||||
// 这里可以实现文件下载逻辑
|
||||
console.log('下载文件:', message.fileName)
|
||||
// 可以创建一个临时的下载链接
|
||||
if (message.fileUrl) {
|
||||
const link = document.createElement('a')
|
||||
@ -939,7 +1252,6 @@ const closeDetailsPanel = () => {
|
||||
|
||||
// 新增事件处理方法
|
||||
const handleEmoji = () => {
|
||||
console.log('选择表情')
|
||||
// TODO: 实现表情选择功能
|
||||
}
|
||||
|
||||
@ -960,6 +1272,7 @@ const handleImage = (imageData: any) => {
|
||||
fileName: imageData.name || '图片',
|
||||
fileSize: imageData.size?.toString() || '0',
|
||||
senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我',
|
||||
senderId: userStore.user?.id?.toString() || '', // 发送者ID
|
||||
avatar: userStore.user?.avatar || '',
|
||||
time: formatTime(new Date().toISOString()),
|
||||
isOwn: true,
|
||||
@ -991,6 +1304,7 @@ const handleImage = (imageData: any) => {
|
||||
type: 'text',
|
||||
content: '收到图片了!',
|
||||
senderName: '对方',
|
||||
senderId: '2000000000', // 对方用户ID
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date().toISOString()),
|
||||
isOwn: false,
|
||||
@ -1033,6 +1347,7 @@ const handleFile = (fileData: any) => {
|
||||
fileSize: fileData.size.toString(),
|
||||
fileType: fileData.type,
|
||||
senderName: userStore.user?.profile?.realName || userStore.user?.nickname || '我',
|
||||
senderId: userStore.user?.id?.toString() || '', // 发送者ID
|
||||
avatar: userStore.user?.avatar || '',
|
||||
time: formatTime(new Date().toISOString()),
|
||||
isOwn: true,
|
||||
@ -1064,6 +1379,7 @@ const handleFile = (fileData: any) => {
|
||||
type: 'text',
|
||||
content: '收到文件了!',
|
||||
senderName: '对方',
|
||||
senderId: '2000000000', // 对方用户ID
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38358644',
|
||||
time: formatTime(new Date().toISOString()),
|
||||
isOwn: false,
|
||||
@ -1198,8 +1514,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
@ -1234,7 +1550,7 @@ onMounted(() => {
|
||||
.contact-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 2px;
|
||||
color: #3F3F44;
|
||||
}
|
||||
|
||||
@ -1455,6 +1771,7 @@ onMounted(() => {
|
||||
.message-avatar {
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-own .message-avatar {
|
||||
@ -1500,7 +1817,20 @@ onMounted(() => {
|
||||
.message-sender {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.teacher-label {
|
||||
background: #E2F5FF;
|
||||
color: #0088D1;
|
||||
border: 1px solid #0088D1;
|
||||
padding: 0 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
@ -1995,6 +2325,7 @@ onMounted(() => {
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
margin: 0 20px;
|
||||
display: flex;
|
||||
@ -2108,4 +2439,175 @@ onMounted(() => {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 成员弹框样式 - 符合项目风格 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.member-modal {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
width: 380px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
animation: modalSlideIn 0.2s ease-out;
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.member-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.member-avatar-large {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.member-avatar-large img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.member-details h4 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.member-details .member-role {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #e6e6e6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.modal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-btn.mute-btn {
|
||||
background: #ffffff;
|
||||
color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.modal-btn.mute-btn:hover:not(:disabled) {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn.remove-btn {
|
||||
background: #ffffff;
|
||||
color: #666;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.modal-btn.remove-btn:hover:not(:disabled) {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 群成员列表样式 - 保持原有样式,只添加点击效果 */
|
||||
.member-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user