feat: 消息中心接入接口,修复分页功能样式,新增班级Excel导出,统计页课程详情接入接口

This commit is contained in:
QDKF 2025-09-19 20:15:10 +08:00
parent e687fa8ebd
commit 23c54eaf40
21 changed files with 2119 additions and 391 deletions

View File

@ -13,6 +13,8 @@ export { default as UploadApi } from './modules/upload'
export { default as StatisticsApi } from './modules/statistics' export { default as StatisticsApi } from './modules/statistics'
export { default as ExamApi } from './modules/exam' export { default as ExamApi } from './modules/exam'
export { ChatApi } from './modules/chat' export { ChatApi } from './modules/chat'
export { default as MessageApi } from './modules/message'
export type { MessageItem, BackendMessageItem, SystemMessage } from './modules/message'
// API 基础配置 // API 基础配置
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot' export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
@ -233,6 +235,17 @@ export const API_ENDPOINTS = {
UNREAD_COUNT: '/aiol/aiolChat/unread-count', UNREAD_COUNT: '/aiol/aiolChat/unread-count',
FOLLOW: '/aiol/aiolUserFollow/follow', FOLLOW: '/aiol/aiolUserFollow/follow',
}, },
// 消息相关
MESSAGE: {
LIKES: '/aiol/message/likes',
COMMENTS_AT: '/aiol/message/comments_at',
UNREAD_COUNT: '/aiol/message/unread_count',
MARK_READ: '/aiol/message/likes/:id/read',
BATCH_MARK_READ: '/aiol/message/likes/batch-read',
DELETE: '/aiol/message/likes/:id',
BATCH_DELETE: '/aiol/message/likes/batch-delete',
},
// 资源相关 // 资源相关
RESOURCES: { RESOURCES: {

View File

@ -1228,19 +1228,10 @@ export class CourseApi {
// 获取课程评论列表 // 获取课程评论列表
static async getCourseComments(courseId: string): Promise<ApiResponse<CourseComment[]>> { static async getCourseComments(courseId: string): Promise<ApiResponse<CourseComment[]>> {
try { try {
console.log('🔍 获取课程评论数据课程ID:', courseId)
console.log('🔍 API请求URL: /aiol/aiolComment/course/' + courseId + '/list')
const response = await ApiRequest.get<any>(`/aiol/aiolComment/course/${courseId}/list`) const response = await ApiRequest.get<any>(`/aiol/aiolComment/course/${courseId}/list`)
console.log('🔍 评论API响应:', response)
// 处理后端响应格式 // 处理后端响应格式
if (response.data && response.data.success && response.data.result) { if (response.data && response.data.success && response.data.result) {
console.log('✅ 响应状态码:', response.data.code)
console.log('✅ 响应消息:', response.data.message)
console.log('✅ 原始评论数据:', response.data.result)
console.log('✅ 评论数据数量:', response.data.result.length || 0)
// 适配数据格式 // 适配数据格式
const adaptedComments: CourseComment[] = response.data.result.map((comment: BackendComment) => ({ const adaptedComments: CourseComment[] = response.data.result.map((comment: BackendComment) => ({
id: comment.id, id: comment.id,
@ -1256,8 +1247,6 @@ export class CourseApi {
timeAgo: this.formatTimeAgo(comment.createTime) // 计算相对时间 timeAgo: this.formatTimeAgo(comment.createTime) // 计算相对时间
})) }))
console.log('✅ 适配后的评论数据:', adaptedComments)
return { return {
code: response.data.code, code: response.data.code,
message: response.data.message, message: response.data.message,

428
src/api/modules/message.ts Normal file
View File

@ -0,0 +1,428 @@
import request from '../request'
import type { ApiResponse } from '../types'
// 后端原始消息类型
export interface BackendMessageItem {
id: string
anntId: string
userId: string
titile: string
msgContent: string // JSON字符串需要解析
sender: string
priority: string
readFlag: number
sendTime: string
pageNo: null
pageSize: null
msgCategory: string
busId: null
busType: null
openType: null
openPage: null
bizSource: null
msgAbstract: null
sendTimeBegin: null
sendTimeEnd: null
files: null
visitsNum: null
izTop: number
}
// 解析后的消息内容类型
export interface ParsedMessageContent {
sender: {
id: string
username: string
}
entity: {
type: string // "course", "comment" 等
id: string
title: string
}
action: string // "like", "favorite" 等
actionTime: string
}
// 前端使用的消息类型
export interface MessageItem {
id: string
type: number // 0-点赞, 1-收藏
username: string
avatar: string
courseInfo: string
content: string
timestamp: string
courseImage?: string
isLiked: boolean
isFavorited: boolean
showReplyBox: boolean
replyContent: string
readFlag: number // 是否已读
action: string // 动作类型
}
// 系统消息类型
export interface SystemMessage {
id: string
title: string
content: string
timestamp: string
isRead: boolean
type: 'info' | 'warning' | 'success' | 'error'
}
// 消息列表响应类型
export interface MessageListResponse {
records: BackendMessageItem[]
total: number
size: number
current: number
pages: number
}
// 消息数量响应类型
export interface MessageCountResponse {
total: number
unread: number
}
// 后端消息列表响应格式(实际接口返回的格式)
export interface BackendMessageListResponse {
data: {
success: boolean
message: string
code: number
result: MessageListResponse
timestamp: number
}
}
// 后端消息数量响应格式(实际接口返回的格式)
export interface BackendMessageCountResponse {
data: {
success: boolean
message: string
code: number
result: MessageCountResponse
timestamp: number
}
}
// 数据转换函数
export const transformMessageData = (backendItem: BackendMessageItem): MessageItem => {
let parsedContent: ParsedMessageContent | null = null
try {
parsedContent = JSON.parse(backendItem.msgContent)
} catch (error) {
console.error('解析消息内容失败:', error)
}
// 根据action确定消息类型
const action = parsedContent?.action || 'unknown'
const type = action === 'like' ? 0 : action === 'favorite' ? 1 : 0
return {
id: backendItem.id,
type,
username: parsedContent?.sender?.username || backendItem.sender || '未知用户',
avatar: `https://picsum.photos/200/200?random=${backendItem.id}`, // 使用随机头像
courseInfo: parsedContent?.entity?.title || '未知课程',
content: action === 'like' ? '赞了我的评论' : '收藏了我的课程',
timestamp: backendItem.sendTime,
courseImage: action === 'favorite' ? `https://picsum.photos/300/200?random=${backendItem.id}` : undefined,
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
readFlag: backendItem.readFlag,
action
}
}
// 消息API类
class MessageApi {
/**
*
* @param params
* @returns Promise<BackendMessageListResponse>
*/
async getLikesMessages(params: {
current?: number
size?: number
type?: number // 可选:筛选消息类型
} = {}): Promise<BackendMessageListResponse> {
return request({
url: '/aiol/message/likes',
method: 'GET',
params: {
current: params.current || 1,
size: params.size || 20,
...params
}
})
}
/**
* @消息列表
* @param params
* @returns Promise<BackendMessageListResponse>
*/
async getCommentsAtMessages(params: {
current?: number
size?: number
type?: number // 可选:筛选消息类型
} = {}): Promise<BackendMessageListResponse> {
return request({
url: '/aiol/message/comments_at',
method: 'GET',
params: {
current: params.current || 1,
size: params.size || 20,
...params
}
})
}
/**
*
* @returns Promise<BackendMessageCountResponse>
*/
async getLikesMessageCount(): Promise<BackendMessageCountResponse> {
try {
// 通过获取第一页数据来获取总数和未读数量
const response = await this.getLikesMessages({ current: 1, size: 10 })
console.log('🔍 getLikesMessageCount 响应数据:', response)
if (response.data?.success) {
const total = response.data.result.total || 0
// 计算未读数量readFlag为0表示未读
const unread = response.data.result.records.filter(item => item.readFlag === 0).length
console.log('📊 赞和收藏消息统计:', {
total,
unread,
records: response.data.result.records.map(item => ({
id: item.id,
readFlag: item.readFlag,
sendTime: item.sendTime
}))
})
return {
data: {
success: true,
message: '获取成功',
code: 200,
result: {
total,
unread
},
timestamp: Date.now()
}
}
} else {
return {
data: {
success: false,
message: response.data?.message || '获取失败',
code: response.data?.code || 500,
result: { total: 0, unread: 0 },
timestamp: Date.now()
}
}
}
} catch (error) {
console.error('获取消息数量失败:', error)
return {
data: {
success: false,
message: '获取消息数量失败',
code: 500,
result: { total: 0, unread: 0 },
timestamp: Date.now()
}
}
}
}
/**
* @消息数量
* @returns Promise<BackendMessageCountResponse>
*/
async getCommentsAtMessageCount(): Promise<BackendMessageCountResponse> {
try {
// 通过获取第一页数据来获取总数和未读数量
const response = await this.getCommentsAtMessages({ current: 1, size: 10 })
if (response.data?.success) {
const total = response.data.result.total || 0
// 计算未读数量readFlag为0表示未读
const unread = response.data.result.records.filter(item => item.readFlag === 0).length
return {
data: {
success: true,
message: '获取成功',
code: 200,
result: {
total,
unread
},
timestamp: Date.now()
}
}
} else {
return {
data: {
success: false,
message: response.data?.message || '获取失败',
code: response.data?.code || 500,
result: { total: 0, unread: 0 },
timestamp: Date.now()
}
}
}
} catch (error) {
console.error('获取评论@消息数量失败:', error)
return {
data: {
success: false,
message: '获取评论@消息数量失败',
code: 500,
result: { total: 0, unread: 0 },
timestamp: Date.now()
}
}
}
}
/**
*
* @param messageId ID
* @returns Promise<ApiResponse<null>>
*/
async markAsRead(messageId: number): Promise<ApiResponse<null>> {
return request({
url: `/aiol/message/likes/${messageId}/read`,
method: 'PUT'
})
}
/**
*
* @param messageIds ID数组
* @returns Promise<ApiResponse<null>>
*/
async batchMarkAsRead(messageIds: number[]): Promise<ApiResponse<null>> {
return request({
url: '/aiol/message/likes/batch-read',
method: 'PUT',
data: { messageIds }
})
}
/**
*
* @param messageId ID
* @returns Promise<ApiResponse<null>>
*/
async deleteMessage(messageId: number): Promise<ApiResponse<null>> {
return request({
url: `/aiol/message/likes/${messageId}`,
method: 'DELETE'
})
}
/**
*
* @param messageIds ID数组
* @returns Promise<ApiResponse<null>>
*/
async batchDeleteMessages(messageIds: number[]): Promise<ApiResponse<null>> {
return request({
url: '/aiol/message/likes/batch-delete',
method: 'DELETE',
data: { messageIds }
})
}
/**
*
* @returns Promise<ApiResponse<{ total: number, unread: number }>>
*/
async getUnreadMessageCount(): Promise<ApiResponse<{ total: number, unread: number }>> {
try {
const response = await request({
url: '/aiol/message/unread_count',
method: 'GET'
})
return {
code: 200,
message: '获取成功',
data: response.data || { total: 0, unread: 0 }
}
} catch (error) {
console.error('获取未读消息数量失败:', error)
return {
code: 500,
message: '获取未读消息数量失败',
data: { total: 0, unread: 0 }
}
}
}
/**
*
* @param params
* @returns Promise<ApiResponse<{ records: BackendMessageItem[], total: number, current: number, pages: number }>>
*/
async getSystemMessages(params?: { pageNo?: number, pageSize?: number }): Promise<ApiResponse<{
records: BackendMessageItem[]
total: number
current: number
pages: number
}>> {
return request({
url: '/aiol/message/system',
method: 'GET',
params
})
}
/**
*
* @returns Promise<ApiResponse<{ total: number, unread: number }>>
*/
async getSystemMessageCount(): Promise<ApiResponse<{ total: number, unread: number }>> {
try {
const response = await this.getSystemMessages({ pageNo: 1, pageSize: 1 })
if (response.data && response.data.records) {
const total = response.data.total || 0
const unread = response.data.records.filter((item: BackendMessageItem) => item.readFlag === 0).length
return {
code: 200,
message: '获取成功',
data: { total, unread }
}
}
return {
code: 500,
message: '获取系统消息数量失败',
data: { total: 0, unread: 0 }
}
} catch (error) {
console.error('获取系统消息数量失败:', error)
return {
code: 500,
message: '获取系统消息数量失败',
data: { total: 0, unread: 0 }
}
}
}
}
export default new MessageApi()

View File

@ -27,6 +27,7 @@ export interface TeachCourse {
max_enroll?: number | null max_enroll?: number | null
status?: number | null status?: number | null
question?: string | null question?: string | null
categoryId?: string | number | null // 课程分类ID
} }
// 新建课程请求参数 // 新建课程请求参数

View File

@ -62,15 +62,15 @@ request.interceptors.request.use(
// 添加请求时间戳 // 添加请求时间戳
config.headers['X-Request-Time'] = Date.now().toString() config.headers['X-Request-Time'] = Date.now().toString()
// 开发环境下打印请求信息 // 开发环境下打印请求信息(已禁用)
if (import.meta.env.DEV) { // if (import.meta.env.DEV) {
console.log('🚀 Request:', { // console.log('🚀 Request:', {
url: config.url, // url: config.url,
method: config.method, // method: config.method,
params: config.params, // params: config.params,
data: config.data, // data: config.data,
}) // })
} // }
return config return config
}, },
@ -85,14 +85,14 @@ request.interceptors.response.use(
(response: AxiosResponse<any>) => { (response: AxiosResponse<any>) => {
const { data } = response const { data } = response
// 开发环境下打印响应信息 // 开发环境下打印响应信息(已禁用)
if (import.meta.env.DEV) { // if (import.meta.env.DEV) {
console.log('✅ Response:', { // console.log('✅ Response:', {
url: response.config.url, // url: response.config.url,
status: response.status, // status: response.status,
data: data, // data: data,
}) // })
} // }
// 如果是blob响应直接返回 // 如果是blob响应直接返回
if (response.config.responseType === 'blob') { if (response.config.responseType === 'blob') {
@ -105,6 +105,21 @@ request.interceptors.response.use(
// 如果响应已经是标准格式 // 如果响应已经是标准格式
if (data && typeof data === 'object' && 'code' in data && 'message' in data) { if (data && typeof data === 'object' && 'code' in data && 'message' in data) {
normalizedData = data normalizedData = data
} else if (data && typeof data === 'object' && 'success' in data && 'result' in data) {
// 处理 { success, message, code, result } 格式
normalizedData = {
code: data.code || 200,
message: data.message || '请求成功',
data: data.result
}
} else if (data && typeof data === 'object' && 'data' in data && data.data && typeof data.data === 'object' && 'success' in data.data) {
// 处理 { data: { success, message, code, result } } 格式
const innerData = data.data
normalizedData = {
code: innerData.code || 200,
message: innerData.message || '请求成功',
data: innerData.result
}
} else { } else {
// 如果响应不是标准格式,包装成标准格式 // 如果响应不是标准格式,包装成标准格式
normalizedData = { normalizedData = {
@ -192,7 +207,10 @@ request.interceptors.response.use(
errorMessage = '网络连接失败,请检查网络设置' errorMessage = '网络连接失败,请检查网络设置'
} }
showMessage(errorMessage, 'error') // 对于404错误不显示错误消息因为可能是接口不存在
if (response?.status !== 404) {
showMessage(errorMessage, 'error')
}
return Promise.reject(error) return Promise.reject(error)
} }
) )

View File

@ -91,9 +91,7 @@ interface HotSearchItem {
// //
const fetchHotSearch = async () => { const fetchHotSearch = async () => {
try { try {
console.log('🚀 获取热门搜索数据...')
const response = await ApiRequest.get<HotSearchItem[]>('/aiol/index/hot_search') const response = await ApiRequest.get<HotSearchItem[]>('/aiol/index/hot_search')
console.log('📊 热门搜索API响应:', response)
if (response.data) { if (response.data) {
const apiResponse = response.data as any const apiResponse = response.data as any
@ -104,7 +102,6 @@ const fetchHotSearch = async () => {
if (success && apiResponse.result) { if (success && apiResponse.result) {
hotSearchList.value = apiResponse.result hotSearchList.value = apiResponse.result
} else { } else {
console.error('❌ 获取热门搜索失败:', apiResponse.message || '未知错误')
// 使 // 使
hotSearchList.value = getMockHotSearch() hotSearchList.value = getMockHotSearch()
} }
@ -112,17 +109,12 @@ const fetchHotSearch = async () => {
// //
hotSearchList.value = apiResponse hotSearchList.value = apiResponse
} else { } else {
console.error('❌ 热门搜索数据格式错误')
hotSearchList.value = getMockHotSearch() hotSearchList.value = getMockHotSearch()
} }
} else { } else {
console.error('❌ 热门搜索响应为空')
hotSearchList.value = getMockHotSearch() hotSearchList.value = getMockHotSearch()
} }
console.log('✅ 热门搜索数据加载完成:', hotSearchList.value)
} catch (error) { } catch (error) {
console.error('❌ 获取热门搜索异常:', error)
// 使 // 使
hotSearchList.value = getMockHotSearch() hotSearchList.value = getMockHotSearch()
} }

View File

@ -64,7 +64,7 @@
<n-button type="primary" ghost @click="showImportModal = true"> <n-button type="primary" ghost @click="showImportModal = true">
导入 导入
</n-button> </n-button>
<n-button type="primary" ghost> <n-button type="primary" ghost @click="handleExport">
导出 导出
</n-button> </n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px" <n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px"
@ -290,6 +290,7 @@ import {
} from 'naive-ui' } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui' import type { DataTableColumns } from 'naive-ui'
import ImportModal from '@/components/common/ImportModal.vue' import ImportModal from '@/components/common/ImportModal.vue'
import { exportTableToExcel, type ExportColumn } from '@/utils/excelExport'
// props // props
interface Props { interface Props {
@ -733,6 +734,82 @@ const handleBatchTransfer = () => {
showBatchTransferModal.value = true showBatchTransferModal.value = true
} }
//
const handleExport = () => {
try {
//
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要导出的学员')
return
}
//
const currentData = filteredData.value.length > 0 ? filteredData.value : data.value
// keys
const exportData = currentData.filter((item: StudentItem) => selectedRowKeys.value.includes(item.id))
if (exportData.length === 0) {
message.warning('没有找到选中的学员数据')
return
}
//
const exportColumns: ExportColumn[] = [
{
title: '姓名',
key: 'studentName'
},
{
title: '账号',
key: 'accountNumber'
},
{
title: '班级',
key: 'className',
render: (value: any) => {
//
if (typeof value === 'string' && value.includes(',')) {
return value.split(',').join(';')
}
return value || ''
}
},
{
title: '所在学院',
key: 'college'
},
{
title: '加入时间',
key: 'joinTime'
}
]
//
const currentClassName = props.type === 'student'
? (classList.value.find(item => item.id === props.classId)?.className || '全部班级')
: '班级管理'
const filename = `${currentClassName}_选中学员信息`
//
exportTableToExcel(exportData, exportColumns, filename)
message.success(`成功导出 ${exportData.length} 条选中学员信息`)
console.log('✅ 导出完成:', {
selectedCount: selectedRowKeys.value.length,
exportCount: exportData.length,
filename,
selectedIds: selectedRowKeys.value,
data: exportData.slice(0, 3) // 3
})
} catch (error) {
console.error('❌ 导出失败:', error)
message.error('导出失败,请重试')
}
}
// //
const confirmBatchTransfer = async () => { const confirmBatchTransfer = async () => {
if (!selectedTargetClass.value) { if (!selectedTargetClass.value) {

View File

@ -365,6 +365,42 @@ const routes: RouteRecordRaw[] = [
component: AIAssistantDetail, component: AIAssistantDetail,
meta: { title: '查看详情' } meta: { title: '查看详情' }
}, },
{
path: 'ai-orchestration',
name: 'AIOrchestration',
component: () => import('@/views/teacher/ai-orchestration/index.vue'),
meta: { title: '智能体编排' }
},
{
path: 'ai-orchestration/app-management',
name: 'AIAppManagement',
component: () => import('@/views/teacher/ai-orchestration/app-management.vue'),
meta: { title: 'AI应用管理' }
},
{
path: 'ai-orchestration/knowledge-base',
name: 'AIKnowledgeBase',
component: () => import('@/views/teacher/ai-orchestration/knowledge-base.vue'),
meta: { title: 'AI知识库' }
},
{
path: 'ai-orchestration/process-design',
name: 'AIProcessDesign',
component: () => import('@/views/teacher/ai-orchestration/process-design.vue'),
meta: { title: 'AI流程设计' }
},
{
path: 'ai-orchestration/model-config',
name: 'AIModelConfig',
component: () => import('@/views/teacher/ai-orchestration/model-config.vue'),
meta: { title: 'AI模型配置' }
},
{
path: 'ai-orchestration/ocr-recognition',
name: 'AIOCRRecognition',
component: () => import('@/views/teacher/ai-orchestration/ocr-recognition.vue'),
meta: { title: 'OCR识别' }
},
{ {
path: 'student-management', path: 'student-management',
name: 'StudentManagement', name: 'StudentManagement',

117
src/utils/excelExport.ts Normal file
View File

@ -0,0 +1,117 @@
/**
* Excel导出工具函数
* 使API实现前端Excel导出CSV格式Excel打开
*/
// 定义导出数据的类型
export interface ExportColumn {
title: string
key: string
width?: number
render?: (value: any, row: any) => string
}
export interface ExportOptions {
filename?: string
columns: ExportColumn[]
data: any[]
}
/**
* CSV字符串转换为Excel兼容的格式
* @param csvContent CSV内容
* @returns Excel兼容的Blob
*/
function csvToExcelBlob(csvContent: string): Blob {
// 添加BOM以支持中文
const BOM = '\uFEFF'
const content = BOM + csvContent
return new Blob([content], {
type: 'application/vnd.ms-excel;charset=utf-8'
})
}
/**
* CSV字段值
* @param value
* @returns
*/
function escapeCsvField(value: any): string {
if (value === null || value === undefined) {
return ''
}
const str = String(value)
// 如果包含逗号、引号或换行符,需要用引号包围并转义引号
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}
/**
* Excel文件CSV格式
* @param options
*/
export function exportToExcel(options: ExportOptions): void {
const { filename = '导出数据', columns, data } = options
// 准备CSV内容
const csvRows: string[] = []
// 添加表头
const headers = columns.map(col => escapeCsvField(col.title))
csvRows.push(headers.join(','))
// 添加数据行
data.forEach(row => {
const rowData = columns.map(col => {
let value = row[col.key] || ''
// 如果有自定义渲染函数,使用它
if (col.render) {
value = col.render(value, row)
}
return escapeCsvField(value)
})
csvRows.push(rowData.join(','))
})
// 生成CSV内容
const csvContent = csvRows.join('\n')
// 创建Excel兼容的Blob
const blob = csvToExcelBlob(csvContent)
// 下载文件
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${filename}_${new Date().toISOString().slice(0, 10)}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
}
/**
*
* @param data
* @param columns
* @param filename
*/
export function exportTableToExcel(
data: any[],
columns: ExportColumn[],
filename: string = '表格数据'
): void {
exportToExcel({
filename,
columns,
data
})
}

View File

@ -11,7 +11,9 @@
<div class="sidebar-container" v-if="!hideSidebar"> <div class="sidebar-container" v-if="!hideSidebar">
<!-- 头像 --> <!-- 头像 -->
<div class="avatar-container"> <div class="avatar-container">
<img :src="userStore.user?.avatar" :alt="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username" class="avatar"> <img :src="userStore.user?.avatar"
:alt="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username"
class="avatar">
<div class="avatar-text"> <div class="avatar-text">
{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }} {{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}
</div> </div>
@ -54,7 +56,8 @@
<!-- 学员中心 - 可展开菜单 --> <!-- 学员中心 - 可展开菜单 -->
<div class="nav-item" :class="{ active: activeNavItem === 1 }" @click="toggleStudentMenu('/teacher/student-management/student-library')"> <div class="nav-item" :class="{ active: activeNavItem === 1 }"
@click="toggleStudentMenu('/teacher/student-management/student-library')">
<img :src="activeNavItem === 1 ? '/images/teacher/学院管理(选中).png' : '/images/teacher/学员管理.png'" alt=""> <img :src="activeNavItem === 1 ? '/images/teacher/学院管理(选中).png' : '/images/teacher/学员管理.png'" alt="">
<span>学员中心</span> <span>学员中心</span>
<n-icon class="expand-icon" :class="{ expanded: studentMenuExpanded }"> <n-icon class="expand-icon" :class="{ expanded: studentMenuExpanded }">
@ -82,7 +85,8 @@
</router-link> </router-link>
<router-link to="/teacher/message-center" class="nav-item" :class="{ active: activeNavItem === 5 }" <router-link to="/teacher/message-center" class="nav-item" :class="{ active: activeNavItem === 5 }"
@click="setActiveNavItem(5)"> @click="setActiveNavItem(5)">
<img :src="activeNavItem === 5 ? '/images/profile/message-active.png' : '/images/profile/message.png'" alt=""> <img :src="activeNavItem === 5 ? '/images/profile/message-active.png' : '/images/profile/message.png'"
alt="">
<span>消息中心</span> <span>消息中心</span>
</router-link> </router-link>
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }" <router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
@ -101,6 +105,42 @@
<span>AI助教</span> <span>AI助教</span>
</router-link> </router-link>
</div> </div>
<!-- 智能体编排 - 可展开菜单 -->
<div class="nav-container orchestration-nav">
<div class="nav-item" :class="{ active: activeNavItem === 6 }" @click="toggleOrchestrationMenu">
<img :src="activeNavItem === 6 ? '/images/aiAssistant/AI助教1.png' : '/images/aiAssistant/AI助教2.png'" alt="">
<span>智能体编排</span>
<n-icon class="expand-icon" :class="{ expanded: orchestrationMenuExpanded }">
<ChevronDownOutline />
</n-icon>
</div>
<!-- 智能体编排子菜单 -->
<div class="submenu-container" :class="{ expanded: orchestrationMenuExpanded }">
<router-link to="/teacher/ai-orchestration/app-management" class="submenu-item"
:class="{ active: activeSubNavItem === 'app-management' }" @click="setActiveSubNavItem('app-management')">
<span>AI应用管理</span>
</router-link>
<router-link to="/teacher/ai-orchestration/knowledge-base" class="submenu-item"
:class="{ active: activeSubNavItem === 'knowledge-base' }" @click="setActiveSubNavItem('knowledge-base')">
<span>AI知识库</span>
</router-link>
<router-link to="/teacher/ai-orchestration/process-design" class="submenu-item"
:class="{ active: activeSubNavItem === 'process-design' }" @click="setActiveSubNavItem('process-design')">
<span>AI流程设计</span>
</router-link>
<router-link to="/teacher/ai-orchestration/model-config" class="submenu-item"
:class="{ active: activeSubNavItem === 'model-config' }" @click="setActiveSubNavItem('model-config')">
<span>AI模型配置</span>
</router-link>
<router-link to="/teacher/ai-orchestration/ocr-recognition" class="submenu-item"
:class="{ active: activeSubNavItem === 'ocr-recognition' }"
@click="setActiveSubNavItem('ocr-recognition')">
<span>OCR识别</span>
</router-link>
</div>
</div>
</div> </div>
<!-- 右侧路由视图 --> <!-- 右侧路由视图 -->
@ -146,10 +186,11 @@ const height = window.innerHeight;
console.log(`当前屏幕宽度: ${width}px, 高度: ${height}px`); console.log(`当前屏幕宽度: ${width}px, 高度: ${height}px`);
// //
const activeNavItem = ref(0); // 0: , 1: , 2: , 3: , 4: , 5: const activeNavItem = ref(0); // 0: , 1: , 2: , 3: , 4: , 5: , 6:
const activeSubNavItem = ref(''); // const activeSubNavItem = ref(''); //
const examMenuExpanded = ref(false); // const examMenuExpanded = ref(false); //
const studentMenuExpanded = ref(false); // const studentMenuExpanded = ref(false); //
const orchestrationMenuExpanded = ref(false); //
const showTopImage = ref(true); // / const showTopImage = ref(true); // /
// //
@ -165,7 +206,7 @@ const isAiHovered = ref(false);
const isAiActive = computed(() => route.path.includes('/teacher/ai-assistant')); const isAiActive = computed(() => route.path.includes('/teacher/ai-assistant'));
const breadcrumbDisplay = computed(() => { const breadcrumbDisplay = computed(() => {
const currentPath = route.path; const currentPath = route.path;
let arr = ['certificate/new', 'ai-assistant']; let arr = ['certificate/new', 'ai-assistant', 'ai-orchestration'];
let found = arr.find(item => currentPath.includes(item)); let found = arr.find(item => currentPath.includes(item));
if (found) { if (found) {
return false; return false;
@ -183,8 +224,12 @@ const setActiveNavItem = (index: number) => {
if (index !== 1) { if (index !== 1) {
studentMenuExpanded.value = false; studentMenuExpanded.value = false;
} }
//
if (index !== 6) {
orchestrationMenuExpanded.value = false;
}
// //
if (index !== 4 && index !== 1) { if (index !== 4 && index !== 1 && index !== 6) {
activeSubNavItem.value = ''; activeSubNavItem.value = '';
} }
} }
@ -222,6 +267,17 @@ const toggleStudentMenu = (path: string) => {
} }
} }
//
const toggleOrchestrationMenu = () => {
orchestrationMenuExpanded.value = !orchestrationMenuExpanded.value;
activeNavItem.value = 6;
//
if (orchestrationMenuExpanded.value && !activeSubNavItem.value) {
activeSubNavItem.value = 'app-management';
}
}
// //
const setActiveSubNavItem = (subItem: string) => { const setActiveSubNavItem = (subItem: string) => {
activeSubNavItem.value = subItem; activeSubNavItem.value = subItem;
@ -235,6 +291,10 @@ const setActiveSubNavItem = (subItem: string) => {
// //
activeNavItem.value = 1; activeNavItem.value = 1;
studentMenuExpanded.value = true; studentMenuExpanded.value = true;
} else if (subItem === 'app-management' || subItem === 'knowledge-base' || subItem === 'process-design' || subItem === 'model-config' || subItem === 'ocr-recognition') {
//
activeNavItem.value = 6;
orchestrationMenuExpanded.value = true;
} }
} }
@ -647,6 +707,80 @@ const breadcrumbPathItems = computed(() => {
return breadcrumbs; return breadcrumbs;
} }
//
if (currentPath.includes('ai-orchestration')) {
console.log('智能体编排页面路径:', currentPath);
let breadcrumbs: Array<{ title: string, path: string }> = [];
//
if (currentPath.includes('app-management')) {
breadcrumbs = [
{
title: '智能体编排',
path: '/teacher/ai-orchestration'
},
{
title: 'AI应用管理',
path: currentPath
}
];
} else if (currentPath.includes('knowledge-base')) {
breadcrumbs = [
{
title: '智能体编排',
path: '/teacher/ai-orchestration'
},
{
title: 'AI知识库',
path: currentPath
}
];
} else if (currentPath.includes('process-design')) {
breadcrumbs = [
{
title: '智能体编排',
path: '/teacher/ai-orchestration'
},
{
title: 'AI流程设计',
path: currentPath
}
];
} else if (currentPath.includes('model-config')) {
breadcrumbs = [
{
title: '智能体编排',
path: '/teacher/ai-orchestration'
},
{
title: 'AI模型配置',
path: currentPath
}
];
} else if (currentPath.includes('ocr-recognition')) {
breadcrumbs = [
{
title: '智能体编排',
path: '/teacher/ai-orchestration'
},
{
title: 'OCR识别',
path: currentPath
}
];
} else {
//
breadcrumbs = [
{
title: '智能体编排',
path: currentPath
}
];
}
console.log('智能体编排页面面包屑:', breadcrumbs);
return breadcrumbs;
}
// //
const matchedRoutes = route.matched; const matchedRoutes = route.matched;
@ -731,6 +865,23 @@ const updateActiveNavItem = () => {
activeSubNavItem.value = ''; activeSubNavItem.value = '';
examMenuExpanded.value = false; examMenuExpanded.value = false;
studentMenuExpanded.value = false; studentMenuExpanded.value = false;
} else if (path.includes('ai-orchestration')) {
//
activeNavItem.value = 6; //
orchestrationMenuExpanded.value = true;
//
if (path.includes('app-management')) {
activeSubNavItem.value = 'app-management';
} else if (path.includes('knowledge-base')) {
activeSubNavItem.value = 'knowledge-base';
} else if (path.includes('process-design')) {
activeSubNavItem.value = 'process-design';
} else if (path.includes('model-config')) {
activeSubNavItem.value = 'model-config';
} else if (path.includes('ocr-recognition')) {
activeSubNavItem.value = 'ocr-recognition';
}
} }
} }
@ -869,7 +1020,17 @@ const updateActiveNavItem = () => {
width: 240px; width: 240px;
height: calc(100vh - var(--top-height, 130px)); height: calc(100vh - var(--top-height, 130px));
background: #FFFFFF; background: #FFFFFF;
overflow: auto; overflow-y: auto;
/* 隐藏滚动条但保持滚动功能 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
}
/* 隐藏Webkit浏览器的滚动条 */
.sidebar-container::-webkit-scrollbar {
display: none;
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
@ -966,6 +1127,10 @@ const updateActiveNavItem = () => {
} }
/* 智能体编排菜单移除顶部外边距 */
.orchestration-nav {
margin-top: 10px;
}
.nav-container .nav-item { .nav-container .nav-item {
margin-left: 15px; margin-left: 15px;
width: 210px; width: 210px;

View File

@ -0,0 +1,71 @@
<template>
<div class="development-page">
<div class="page-header">
<h1>AI应用管理</h1>
<p>管理和配置各种AI应用</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<h2>功能开发中</h2>
<p>AI应用管理功能正在紧张开发中敬请期待</p>
</div>
</div>
</template>
<script setup lang="ts">
// AI
</script>
<style scoped>
.development-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 48px;
}
.page-header h1 {
font-size: 32px;
color: #1890ff;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #666;
}
.development-notice {
text-align: center;
padding: 48px 24px;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 12px;
border: 1px solid #91d5ff;
}
.notice-icon {
margin-bottom: 24px;
}
.development-notice h2 {
font-size: 24px;
color: #1890ff;
margin-bottom: 12px;
}
.development-notice p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="ai-orchestration-page">
<div class="page-header">
<h1>AI编排中心</h1>
<p>统一管理和配置AI相关功能</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<div class="notice-content">
<h3>功能开发中</h3>
<p>AI编排中心功能正在开发中敬请期待...</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// AI
</script>
<style scoped>
.ai-orchestration-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 32px;
}
.page-header h1 {
font-size: 32px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #6b7280;
}
.development-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
text-align: center;
}
.notice-icon {
margin-right: 24px;
color: #6b7280;
}
.notice-content h3 {
font-size: 20px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.notice-content p {
font-size: 14px;
color: #6b7280;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="development-page">
<div class="page-header">
<h1>AI知识库</h1>
<p>构建和管理AI知识库</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<h2>功能开发中</h2>
<p>AI知识库功能正在紧张开发中敬请期待</p>
</div>
</div>
</template>
<script setup lang="ts">
// AI
</script>
<style scoped>
.development-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 48px;
}
.page-header h1 {
font-size: 32px;
color: #1890ff;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #666;
}
.development-notice {
text-align: center;
padding: 48px 24px;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 12px;
border: 1px solid #91d5ff;
}
.notice-icon {
margin-bottom: 24px;
}
.development-notice h2 {
font-size: 24px;
color: #1890ff;
margin-bottom: 12px;
}
.development-notice p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="development-page">
<div class="page-header">
<h1>AI模型配置</h1>
<p>配置和优化AI模型参数</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<h2>功能开发中</h2>
<p>AI模型配置功能正在紧张开发中敬请期待</p>
</div>
</div>
</template>
<script setup lang="ts">
// AI
</script>
<style scoped>
.development-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 48px;
}
.page-header h1 {
font-size: 32px;
color: #1890ff;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #666;
}
.development-notice {
text-align: center;
padding: 48px 24px;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 12px;
border: 1px solid #91d5ff;
}
.notice-icon {
margin-bottom: 24px;
}
.development-notice h2 {
font-size: 24px;
color: #1890ff;
margin-bottom: 12px;
}
.development-notice p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="development-page">
<div class="page-header">
<h1>OCR识别</h1>
<p>智能文字识别和处理</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<h2>功能开发中</h2>
<p>OCR识别功能正在紧张开发中敬请期待</p>
</div>
</div>
</template>
<script setup lang="ts">
// OCR
</script>
<style scoped>
.development-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 48px;
}
.page-header h1 {
font-size: 32px;
color: #1890ff;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #666;
}
.development-notice {
text-align: center;
padding: 48px 24px;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 12px;
border: 1px solid #91d5ff;
}
.notice-icon {
margin-bottom: 24px;
}
.development-notice h2 {
font-size: 24px;
color: #1890ff;
margin-bottom: 12px;
}
.development-notice p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="development-page">
<div class="page-header">
<h1>AI流程设计</h1>
<p>可视化设计AI工作流程</p>
</div>
<div class="development-notice">
<div class="notice-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#1890ff" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<h2>功能开发中</h2>
<p>AI流程设计功能正在紧张开发中敬请期待</p>
</div>
</div>
</template>
<script setup lang="ts">
// AI
</script>
<style scoped>
.development-page {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 48px;
}
.page-header h1 {
font-size: 32px;
color: #1890ff;
margin-bottom: 8px;
}
.page-header p {
font-size: 16px;
color: #666;
}
.development-notice {
text-align: center;
padding: 48px 24px;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 12px;
border: 1px solid #91d5ff;
}
.notice-icon {
margin-bottom: 24px;
}
.development-notice h2 {
font-size: 24px;
color: #1890ff;
margin-bottom: 12px;
}
.development-notice p {
font-size: 16px;
color: #666;
}
</style>

View File

@ -15,50 +15,75 @@
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<div class="main-content"> <div class="main-content">
<!-- 左侧课程图片 --> <!-- 加载状态 -->
<div class="course-image-section"> <div v-if="loading" class="loading-container">
<img src="/images/teacher/fj.png" alt="课程封面" class="course-image" /> <div class="loading-content">
</div> <p>正在加载课程详情...</p>
<!-- 右侧课程信息 -->
<div class="course-info-section">
<h1 class="course-title">{{ courseInfo.title }}</h1>
<p class="course-description">{{ courseInfo.description }}</p>
<!-- 课程关键信息 -->
<div class="course-metrics">
<div class="metric-item">
<span class="metric-label">课程时间:</span>
<span class="metric-value">{{ courseInfo.courseTime }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程分类:</span>
<span class="metric-value">{{ courseInfo.category }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课时:</span>
<span class="metric-value">{{ courseInfo.duration }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程讲师:</span>
<span class="metric-value">{{ courseInfo.instructor }}</span>
</div>
<div class="metric-item">
<span class="metric-label">教师团队:</span>
<span class="metric-value">{{ courseInfo.teacherCount }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程积分:</span>
<span class="metric-value">{{ courseInfo.credits }}</span>
</div>
</div>
<!-- 开课学期选择 -->
<div class="semester-section">
<span class="semester-label">开课1学期</span>
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select" size="small" />
</div> </div>
</div> </div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-content">
<p>{{ error }}</p>
<button @click="loadCourseDetail" class="retry-btn">重试</button>
</div>
</div>
<!-- 课程内容 -->
<template v-else>
<!-- 左侧课程图片 -->
<div class="course-image-section">
<img :src="courseInfo.thumbnail || '/images/teacher/fj.png'" :alt="courseInfo.title" class="course-image" />
</div>
<!-- 右侧课程信息 -->
<div class="course-info-section">
<h1 class="course-title">{{ courseInfo.title }}</h1>
<div class="course-description" v-html="cleanHtmlContent(courseInfo.description)"></div>
<!-- 课程关键信息 -->
<div class="course-metrics">
<div class="metric-item">
<span class="metric-label">课程时间:</span>
<span class="metric-value">{{ courseInfo.courseTime }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程分类:</span>
<span class="metric-value">{{ courseInfo.category }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课时:</span>
<span class="metric-value">{{ courseInfo.duration }}</span>
</div>
<div class="metric-item">
<span class="metric-label">课程讲师:</span>
<span class="metric-value">
<span v-if="instructorsLoading">加载中...</span>
<span v-else>{{ courseInfo.instructor }}</span>
</span>
</div>
<div class="metric-item">
<span class="metric-label">教师团队:</span>
<span class="metric-value">
<span v-if="instructorsLoading">加载中...</span>
<span v-else>{{ courseInfo.teacherCount }}</span>
</span>
</div>
<div class="metric-item">
<span class="metric-label">课程积分:</span>
<span class="metric-value">{{ courseInfo.credits }}</span>
</div>
</div>
<!-- 开课学期选择 -->
<div class="semester-section">
<span class="semester-label">开课1学期</span>
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select"
size="small" />
</div>
</div>
</template>
</div> </div>
<!-- 底部统计数据 --> <!-- 底部统计数据 -->
@ -133,15 +158,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { NButton, NSelect } from 'naive-ui' import { NButton, NSelect } from 'naive-ui'
import { CourseApi } from '@/api/modules/course'
import { TeachCourseApi } from '@/api/modules/teachCourse'
import CourseIntro from './tabs/CourseIntro.vue' import CourseIntro from './tabs/CourseIntro.vue'
import TeachingTeam from './tabs/TeachingTeam.vue' import TeachingTeam from './tabs/TeachingTeam.vue'
import ChapterList from './tabs/ChapterList.vue' import ChapterList from './tabs/ChapterList.vue'
import CourseComments from './tabs/CourseComments.vue' import CourseComments from './tabs/CourseComments.vue'
const router = useRouter() const router = useRouter()
const route = useRoute()
const courseId = ref(route.params.id as string)
// //
const showTopImage = ref(true) // / const showTopImage = ref(true) // /
@ -155,9 +184,18 @@ const courseInfo = ref({
duration: '4小时28分钟', duration: '4小时28分钟',
instructor: '王建国', instructor: '王建国',
teacherCount: 1, teacherCount: 1,
credits: 60 credits: 60,
thumbnail: '/images/teacher/fj.png'
}) })
//
const loading = ref(false)
const error = ref('')
//
const instructors = ref<any[]>([])
const instructorsLoading = ref(false)
// //
const courseStats = ref({ const courseStats = ref({
views: 0, views: 0,
@ -188,6 +226,213 @@ const goBack = () => {
const handleClose = () => { const handleClose = () => {
showTopImage.value = false // showTopImage.value = false //
} }
//
const loadCourseDetail = async () => {
console.log('🚀 开始加载教师课程详情课程ID:', courseId.value)
if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效'
console.error('❌ 课程ID无效:', courseId.value)
return
}
try {
loading.value = true
error.value = ''
console.log('📡 调用课程详情API...')
const response = await CourseApi.getCourseDetail(courseId.value)
console.log('📊 课程详情API响应:', response)
if (response.code === 0 || response.code === 200) {
const course = response.data
console.log('✅ 课程数据设置成功:', course)
console.log('📋 课程基本信息:', {
id: course?.id,
title: course?.title,
description: course?.description,
thumbnail: course?.thumbnail,
category: course?.category,
instructor: course?.instructor,
duration: course?.duration,
studentsCount: course?.studentsCount,
createdAt: course?.createdAt,
updatedAt: course?.updatedAt
})
//
courseInfo.value = {
title: course?.title || '课程名称课程名称课',
description: course?.description || '本课程旨在带领学生系统地学习【课程核心领域】的知识。我们将从【最基础的概念】讲起,逐步深入到【高级主题或应用】。通过理论与实践相结合的方式,学生不仅能够掌握【具体的理论知识】,还能获得【具体的实践技能,如解决XX问题、开发XX应用等】。',
courseTime: formatCourseTime(course?.createdAt, course?.updatedAt),
category: course?.category?.name || '分类名称',
duration: course?.duration || '4小时28分钟',
instructor: course?.instructor?.name || '王建国',
teacherCount: 1, // 1
credits: 60, // 60
thumbnail: course?.thumbnail || '/images/teacher/fj.png'
}
// API
await loadCourseCategoryFromManagementAPI()
// API
courseStats.value = {
views: course?.studentsCount || 0,
enrollments: course?.studentsCount || 0,
learners: Math.floor((course?.studentsCount || 0) * 0.8),
comments: Math.floor((course?.studentsCount || 0) * 0.3)
}
console.log('🎯 课程信息更新完成:', courseInfo.value)
console.log('📈 统计数据更新完成:', courseStats.value)
} else {
error.value = response.message || '获取课程详情失败'
console.error('❌ API返回错误:', response)
}
} catch (err) {
console.error('❌ 加载课程详情失败:', err)
error.value = '网络错误,请稍后重试'
} finally {
loading.value = false
}
}
//
const loadCourseInstructors = async () => {
console.log('👥 开始加载课程教师团队课程ID:', courseId.value)
if (!courseId.value || courseId.value.trim() === '') {
console.error('❌ 课程ID无效无法加载教师团队')
return
}
try {
instructorsLoading.value = true
console.log('📡 调用教师团队API...')
const response = await CourseApi.getCourseInstructors(courseId.value)
console.log('📊 教师团队API响应:', response)
if (response.code === 0 || response.code === 200) {
instructors.value = response.data || []
console.log('✅ 教师团队数据设置成功:', instructors.value)
console.log('👥 教师团队数量:', instructors.value.length)
//
courseInfo.value.teacherCount = instructors.value.length
console.log('🎯 更新教师团队人数:', courseInfo.value.teacherCount)
// sortOrder
if (instructors.value.length > 0) {
// sortOrdersortOrder
const sortedInstructors = [...instructors.value].sort((a, b) => {
const aOrder = a.sortOrder || 0
const bOrder = b.sortOrder || 0
return aOrder - bOrder
})
//
const allInstructorNames = sortedInstructors.map(teacher => teacher.name).join('、')
courseInfo.value.instructor = allInstructorNames || '王建国'
console.log('👨‍🏫 更新所有教师:', courseInfo.value.instructor)
console.log('📋 教师团队排序:', sortedInstructors.map(t => ({ name: t.name, sortOrder: t.sortOrder })))
}
} else {
console.warn('⚠️ 教师团队API返回错误:', response)
//
courseInfo.value.teacherCount = 1
}
} catch (err) {
console.error('❌ 加载教师团队失败:', err)
//
courseInfo.value.teacherCount = 1
} finally {
instructorsLoading.value = false
}
}
//
const formatCourseTime = (createdAt?: string, updatedAt?: string) => {
if (createdAt && updatedAt) {
const startDate = new Date(createdAt).toLocaleDateString('zh-CN')
const endDate = new Date(updatedAt).toLocaleDateString('zh-CN')
return `${startDate}-${endDate}`
}
return '2025-08-25-2026.08-25'
}
// HTML
const cleanHtmlContent = (content: string) => {
if (!content) return ''
// HTML
// HTMLscript
return content
}
// API
const loadCourseCategoryFromManagementAPI = async () => {
try {
// APIcategoryId
const response = await TeachCourseApi.getTeacherCourseList({})
if (response.data && response.data.result && response.data.result.length > 0) {
// ID
const courseData = response.data.result.find(course => course.id === courseId.value)
if (!courseData) {
//
try {
const categoryResponse = await CourseApi.getCategories()
if (categoryResponse.code === 200 && categoryResponse.data && categoryResponse.data.length > 0) {
const firstCategory = categoryResponse.data[0]
courseInfo.value.category = firstCategory.name
}
} catch (error) {
console.error('❌ 备选方案失败:', error)
}
return
}
if (courseData.categoryId) {
//
const categoryResponse = await CourseApi.getCategories()
if (categoryResponse.code === 200 && categoryResponse.data) {
// categoryId
const categoryIds = courseData.categoryId.toString().split(',').map((id: string) => parseInt(id.trim())).filter((id: number) => !isNaN(id))
// ID
const categoryNames = categoryIds.map((id: number) => {
const category = categoryResponse.data.find(cat => cat.id === id)
return category ? category.name : `未知分类${id}`
}).filter(Boolean)
//
if (categoryNames.length > 0) {
courseInfo.value.category = categoryNames.join('、')
}
}
}
}
} catch (error) {
console.error('❌ 获取分类信息失败:', error)
}
}
//
onMounted(async () => {
console.log('🎬 教师课程详情页面加载完成课程ID:', courseId.value)
//
await Promise.all([
loadCourseDetail(),
loadCourseInstructors()
])
console.log('🎉 所有数据加载完成')
})
</script> </script>
<style scoped> <style scoped>
@ -281,6 +526,38 @@ const handleClose = () => {
margin: 25px auto; margin: 25px auto;
} }
/* 加载和错误状态 */
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
text-align: center;
width: 100%;
}
.loading-content p,
.error-content p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
.retry-btn {
background: #0C99DA;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.retry-btn:hover {
background: #0A8BC7;
}
/* 左侧课程图片 */ /* 左侧课程图片 */
.course-image-section { .course-image-section {
flex: 0 0 305px; flex: 0 0 305px;

View File

@ -65,8 +65,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui' import { NBadge, NTabs, NTabPane } from 'naive-ui'
import { ChatApi } from '@/api' import { ChatApi, MessageApi } from '@/api'
// //
import NotificationMessages from './components/NotificationMessages.vue' import NotificationMessages from './components/NotificationMessages.vue'
@ -76,7 +76,6 @@ import SystemMessages from './components/SystemMessages.vue'
// //
const activeTab = ref('notification') // tab const activeTab = ref('notification') // tab
const message = useMessage()
// //
const notificationCount = ref(0) // const notificationCount = ref(0) //
@ -111,23 +110,48 @@ const loadMessageCounts = async (forceRefresh = false) => {
// //
const now = Date.now() const now = Date.now()
if (!forceRefresh && now - cacheTimestamp.value < CACHE_DURATION) { if (!forceRefresh && now - cacheTimestamp.value < CACHE_DURATION) {
console.log('📋 使用缓存的消息数量,跳过加载')
return return
} }
console.log('🔄 开始加载消息数量...')
loading.value = true loading.value = true
try { try {
// // 使
await Promise.allSettled([ const unreadCountResult = await Promise.allSettled([loadTotalUnreadCount()])
loadNotificationCount(), console.log('🔍 统一接口结果:', unreadCountResult[0])
loadCommentCount(),
loadFavoriteCount(), // 使
loadSystemCount() if (unreadCountResult[0].status === 'rejected') {
]) console.log('⚠️ 统一接口失败,使用分别加载方式')
// 使 Promise.allSettled 使
const results = await Promise.allSettled([
loadNotificationCount(),
loadCommentCount(),
loadFavoriteCount(),
loadSystemCount()
])
console.log('📊 分别加载结果:', results)
//
const failedCount = results.filter(result => result.status === 'rejected').length
if (failedCount > 0) {
console.log(`⚠️ ${failedCount} 个接口加载失败`)
//
}
}
console.log('📈 最终消息数量:', {
notification: notificationCount.value,
comment: commentCount.value,
favorite: favoriteCount.value,
system: systemCount.value
})
cacheTimestamp.value = now cacheTimestamp.value = now
} catch (error) { } catch (error) {
console.error('加载消息数量失败:', error) console.error('加载消息数量失败:', error)
message.error('加载消息数量失败') //
} finally { } finally {
loading.value = false loading.value = false
} }
@ -143,28 +167,49 @@ const debouncedRefresh = () => {
}, 1000) }, 1000)
} }
//
const loadTotalUnreadCount = async () => {
try {
console.log('🔄 尝试调用统一未读消息数接口...')
const response = await MessageApi.getUnreadMessageCount()
console.log('🔍 统一接口响应:', response)
if (response.code === 200) {
const result = response.data || {}
console.log('📊 统一接口返回数据:', result)
// 使
// APItotalunread
//
commentCount.value = 0
favoriteCount.value = 0
systemCount.value = 0
notificationCount.value = 0
return true
}
console.log('❌ 统一接口响应格式不正确')
return false
} catch (error) {
console.error('❌ 获取总未读消息数失败:', error)
return false
}
}
// //
const loadNotificationCount = async () => { const loadNotificationCount = async () => {
try { // /aiol/aiolChat/unread-count 使
const response = await ChatApi.getUnreadCount()
if (response.data) {
notificationCount.value = response.data.total || 0
return
}
} catch (error) {
console.warn('获取未读消息数量失败,尝试备用方案:', error)
}
//
try { try {
const chatsResponse = await ChatApi.getMyChats() const chatsResponse = await ChatApi.getMyChats()
if (chatsResponse.data?.success) { if (chatsResponse.data?.success) {
notificationCount.value = chatsResponse.data.result.reduce((total: number, chat: any) => { notificationCount.value = chatsResponse.data.result.reduce((total: number, chat: any) => {
return total + (chat.unreadCount || 0) return total + (chat.unreadCount || 0)
}, 0) }, 0)
} else {
notificationCount.value = 0
} }
} catch (chatError) { } catch (chatError) {
console.error('获取会话列表失败:', chatError) // 0
notificationCount.value = 0 notificationCount.value = 0
} }
} }
@ -172,8 +217,12 @@ const loadNotificationCount = async () => {
// @ // @
const loadCommentCount = async () => { const loadCommentCount = async () => {
try { try {
// TODO: @API const response = await MessageApi.getCommentsAtMessageCount()
commentCount.value = 0 if (response.data?.success && (response.data.code === 200 || response.data.code === 0)) {
commentCount.value = response.data.result?.unread || 0
} else {
commentCount.value = 0
}
} catch (error) { } catch (error) {
console.error('获取评论数量失败:', error) console.error('获取评论数量失败:', error)
commentCount.value = 0 commentCount.value = 0
@ -183,10 +232,19 @@ const loadCommentCount = async () => {
// //
const loadFavoriteCount = async () => { const loadFavoriteCount = async () => {
try { try {
// TODO: API const response = await MessageApi.getLikesMessageCount()
favoriteCount.value = 0 console.log('🔍 loadFavoriteCount 响应数据:', response)
if (response.data?.success && (response.data.code === 200 || response.data.code === 0)) {
favoriteCount.value = response.data.result?.unread || 0
console.log('✅ 赞和收藏未读数量设置为:', favoriteCount.value)
} else {
favoriteCount.value = 0
console.log('❌ 赞和收藏接口响应失败设置为0')
}
} catch (error) { } catch (error) {
console.error('获取收藏数量失败:', error) console.error('获取收藏数量失败:', error)
// 0
favoriteCount.value = 0 favoriteCount.value = 0
} }
} }
@ -194,10 +252,19 @@ const loadFavoriteCount = async () => {
// //
const loadSystemCount = async () => { const loadSystemCount = async () => {
try { try {
// TODO: API console.log('🔍 开始加载系统消息数量')
systemCount.value = 0 const response = await MessageApi.getSystemMessageCount()
console.log('🔍 系统消息数量API响应:', response)
if (response.code === 200 && response.data) {
systemCount.value = response.data.unread || 0
console.log('✅ 系统消息数量:', systemCount.value)
} else {
console.warn('⚠️ 系统消息数量API返回错误:', response)
systemCount.value = 0
}
} catch (error) { } catch (error) {
console.error('获取系统消息数量失败:', error) console.error('获取系统消息数量失败:', error)
systemCount.value = 0 systemCount.value = 0
} }
} }

View File

@ -127,7 +127,7 @@
</button> </button>
<span v-if="showEllipsis" class="ellipsis">...</span> <span v-if="showEllipsis" class="ellipsis">...</span>
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button> <button v-if="showLastPage" class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
@ -140,7 +140,7 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useMessage, NIcon } from 'naive-ui' import { useMessage, NIcon } from 'naive-ui'
import { ChatbubbleEllipsesOutline, TrashOutline, WarningOutline } from '@vicons/ionicons5' import { ChatbubbleEllipsesOutline, TrashOutline, WarningOutline } from '@vicons/ionicons5'
import { CourseApi } from '@/api' import { MessageApi, BackendMessageItem } from '@/api'
// //
interface Message { interface Message {
@ -161,17 +161,54 @@ interface Message {
likeCount?: number likeCount?: number
} }
// @
const processAtMentions = (content: string): string => {
// [user:id:username] @username
return content.replace(/\[user:(\d+):([^\]]+)\]/g, '@$2')
}
//
const transformMessageData = (backendItem: BackendMessageItem): Message => {
let parsedContent: any = null
try {
parsedContent = JSON.parse(backendItem.msgContent)
} catch (error) {
console.error('解析消息内容失败:', error)
}
// @
const rawCommentContent = parsedContent?.comment?.content || ''
const hasAtMention = rawCommentContent.includes('[user:')
const type = hasAtMention ? 1 : 0 // 1=@, 0=
// @
const processedContent = processAtMentions(rawCommentContent)
return {
id: backendItem.id,
type,
username: parsedContent?.sender?.username || backendItem.sender || '未知用户',
avatar: '/images/activity/5.png', // 使
courseInfo: parsedContent?.entity?.title || '未知课程',
content: processedContent,
timestamp: backendItem.sendTime,
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
courseId: parsedContent?.entity?.id,
userId: parsedContent?.sender?.id,
images: [],
likeCount: 0
}
}
// //
const messages = ref<Message[]>([]) const messages = ref<Message[]>([])
const message = useMessage() const message = useMessage()
const loading = ref(false) 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 currentPage = ref(1)
@ -196,6 +233,11 @@ const showEllipsis = computed(() => {
return currentPage.value + 2 < totalPages.value - 1 return currentPage.value + 2 < totalPages.value - 1
}) })
//
const showLastPage = computed(() => {
return totalPages.value > 1 && currentPage.value + 2 < totalPages.value
})
// //
onMounted(() => { onMounted(() => {
loadMessages() loadMessages()
@ -205,60 +247,52 @@ onMounted(() => {
const loadMessages = async () => { const loadMessages = async () => {
loading.value = true loading.value = true
try { try {
// const response = await MessageApi.getCommentsAtMessages({
const allComments: Message[] = [] current: currentPage.value,
size: pageSize.value
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) { if (response.data?.success && (response.data.code === 200 || response.data.code === 0)) {
console.error('❌ 加载评论和@消息失败:', error) const data = response.data.result
message.error('加载消息失败') //
messages.value = (data.records || []).map(transformMessageData)
totalPages.value = data.pages || 1
totalCount.value = data.total || 0
} else {
//
messages.value = []
}
} catch (error: any) {
console.error('加载消息失败:', error)
//
// 404使
if (error.response?.status === 404) {
// 使
messages.value = [
{
id: '1',
type: 1, // @
username: '小明',
avatar: '/images/activity/5.png',
courseInfo: '《python语言基础与应用》',
content: '老师讲得真棒!@李四 快来看这个课程!',
timestamp: '2025-09-17 04:27:25',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
courseId: '1954463468539371522',
userId: '1966804797404344321',
images: [],
likeCount: 0
}
]
totalPages.value = 1
totalCount.value = 1
} else {
//
messages.value = []
}
} finally { } finally {
loading.value = false loading.value = false
} }
@ -575,6 +609,7 @@ const goToPage = (page: number) => {
color: #999; color: #999;
margin: 0; margin: 0;
} }
.action-btn i { .action-btn i {
font-size: 14px; font-size: 14px;
} }
@ -695,21 +730,21 @@ const goToPage = (page: number) => {
} }
.page-btn:hover:not(:disabled) { .page-btn:hover:not(:disabled) {
border-color: #1890ff; border-color: #0089D1;
color: #1890ff; color: #0089D1;
} }
.page-btn.active { .page-btn.active {
background: #1890ff; background: #0089D1;
border-color: #1890ff; border-color: #0089D1;
color: #fff; color: #fff;
} }
.page-btn:disabled { .page-btn:disabled {
color: #bfbfbf; color: #878787;
border-color: #f0f0f0; border: none;
cursor: not-allowed; cursor: not-allowed;
background: #fafafa; background: #fff;
} }
.ellipsis { .ellipsis {

View File

@ -1,44 +1,52 @@
<template> <template>
<div class="message-center"> <div class="message-center">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-text">加载中...</div>
</div>
<!-- 消息列表 --> <!-- 消息列表 -->
<div class="message-list"> <div v-else class="message-list">
<div v-for="message in messages" :key="message.id" class="message-item"> <div v-if="messages.length === 0" class="empty-container">
<div class="empty-text">暂无消息</div>
</div>
<div v-else v-for="messageItem in messages" :key="messageItem.id" class="message-item">
<!-- 用户头像 --> <!-- 用户头像 -->
<div class="avatar-container"> <div class="avatar-container">
<img :src="message.avatar" :alt="message.username" class="avatar" /> <img :src="messageItem.avatar" :alt="messageItem.username" class="avatar" />
</div> </div>
<!-- 消息内容 --> <!-- 消息内容 -->
<div class="message-content"> <div class="message-content">
<!-- 点赞消息 --> <!-- 点赞消息 -->
<div class="message-header" v-if="message.type === 0"> <div class="message-header" v-if="messageItem.type === 0">
<div> <div>
<span class="username"> <span class="username">
{{ message.username }}赞了我的评论 {{ messageItem.username }}赞了我的评论
</span> </span>
<span class="content">{{ message.content }}</span> <span class="content">{{ messageItem.content }}</span>
</div> </div>
<span class="timestamp">{{ message.timestamp }}</span> <span class="timestamp">{{ messageItem.timestamp }}</span>
</div> </div>
<!-- 收藏消息 --> <!-- 收藏消息 -->
<div class="message-header" v-if="message.type === 1"> <div class="message-header" v-if="messageItem.type === 1">
<div class="header-left"> <div class="header-left">
<div> <div>
<span class="username"> <span class="username">
{{ message.username }}收藏了我的课程 {{ messageItem.username }}收藏了我的课程
</span> </span>
<span class="course-info">{{ message.courseInfo }}</span> <span class="course-info">{{ messageItem.courseInfo }}</span>
</div> </div>
<span class="timestamp">{{ message.timestamp }}</span> <span class="timestamp">{{ messageItem.timestamp }}</span>
</div> </div>
<!-- 课程封面图片 - 只有收藏消息才显示 --> <!-- 课程封面图片 - 只有收藏消息才显示 -->
<div v-if="message.type === 1 && message.courseImage" class="course-image-container"> <div v-if="messageItem.type === 1 && messageItem.courseImage" class="course-image-container">
<img :src="message.courseImage" :alt="message.courseInfo" class="course-image" /> <img :src="messageItem.courseImage" :alt="messageItem.courseInfo" class="course-image" />
</div> </div>
</div> </div>
<div class="message-text" v-if="message.type === 0">课程: <div class="message-text" v-if="messageItem.type === 0">课程:
<span class="course-info">{{ message.courseInfo }}</span> <span class="course-info">{{ messageItem.courseInfo }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -55,7 +63,7 @@
</button> </button>
<span v-if="showEllipsis" class="ellipsis">...</span> <span v-if="showEllipsis" class="ellipsis">...</span>
<button class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button> <button v-if="showLastPage" class="page-btn" @click="goToPage(totalPages)">{{ totalPages }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(totalPages)">尾页</button>
@ -65,124 +73,54 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { MessageApi, type MessageItem, type BackendMessageItem } from '@/api'
// // 使API
interface Message { type Message = MessageItem
id: number
type: number // 0-, 1- //
username: string const transformMessageData = (backendItem: BackendMessageItem): MessageItem => {
avatar: string let parsedContent: any = null
courseInfo: string
content: string try {
timestamp: string parsedContent = JSON.parse(backendItem.msgContent)
courseImage?: string } catch (error) {
isLiked: boolean console.error('解析消息内容失败:', error)
isFavorited: boolean }
showReplyBox: boolean
replyContent: string // action
const action = parsedContent?.action || 'unknown'
const type = action === 'like' ? 0 : action === 'favorite' ? 1 : 0
return {
id: backendItem.id,
type,
username: parsedContent?.sender?.username || backendItem.sender || '未知用户',
avatar: '/images/activity/5.png', // 使
courseInfo: parsedContent?.entity?.title || '未知课程',
content: action === 'like' ? '赞了我的评论' : '收藏了我的课程',
timestamp: backendItem.sendTime,
courseImage: action === 'favorite' ? '/images/courses/course-bg.png' : undefined,
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
readFlag: backendItem.readFlag,
action
}
} }
// //
const messages = ref<Message[]>([ const messages = ref<Message[]>([])
{
id: 1,
type: 0, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '这里老师营养饮用的说明了',
timestamp: '7月20日\n12:41',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 2,
type: 1, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '',
timestamp: '7月20日\n12:41',
courseImage: 'https://picsum.photos/300/200',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 3,
type: 0, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '这里老师营养饮用的说明了',
timestamp: '7月20日\n12:41',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 4,
type: 1, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '',
timestamp: '7月20日\n12:41',
courseImage: 'https://picsum.photos/300/200',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 5,
type: 1, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '',
timestamp: '7月20日\n12:41',
courseImage: 'https://picsum.photos/300/200',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 6,
type: 0, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '这里老师营养饮用的说明了',
timestamp: '7月20日\n12:41',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
},
{
id: 7,
type: 0, // -
username: '王建华化学老师',
avatar: 'https://picsum.photos/200/200',
courseInfo: '《教师小学期制实验》',
content: '这里老师营养饮用的说明了',
timestamp: '7月20日\n12:41',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: ''
}
])
// //
const currentPage = ref(1) const currentPage = ref(1)
const totalPages = ref(29) const totalPages = ref(1)
const total = ref(0)
const pageSize = ref(20)
//
const loading = ref(false)
// //
const visiblePages = computed(() => { const visiblePages = computed(() => {
@ -201,14 +139,83 @@ const showEllipsis = computed(() => {
return currentPage.value + 2 < totalPages.value - 1 return currentPage.value + 2 < totalPages.value - 1
}) })
//
const showLastPage = computed(() => {
return totalPages.value > 1 && currentPage.value + 2 < totalPages.value
})
// //
onMounted(() => { onMounted(() => {
loadMessages() loadMessages()
}) })
// //
const loadMessages = () => { const loadMessages = async () => {
// TODO: API loading.value = true
try {
const response = await MessageApi.getLikesMessages({
current: currentPage.value,
size: pageSize.value
})
if (response.data?.success && (response.data.code === 200 || response.data.code === 0)) {
const data = response.data.result
//
messages.value = (data.records || []).map(transformMessageData)
totalPages.value = data.pages || 1
total.value = data.total || 0
} else {
//
messages.value = []
}
} catch (error: any) {
console.error('加载消息失败:', error)
//
// 404使
if (error.response?.status === 404) {
// 使
messages.value = [
{
id: '1',
type: 0,
username: '王建华化学老师',
avatar: '/images/activity/5.png',
courseInfo: '《教师小学期制实验》',
content: '这里老师营养饮用的说明了',
timestamp: '7月20日\n12:41',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
readFlag: 0,
action: 'like'
},
{
id: '2',
type: 1,
username: '王建华化学老师',
avatar: '/images/activity/5.png',
courseInfo: '《教师小学期制实验》',
content: '',
timestamp: '7月20日\n12:41',
courseImage: '/images/courses/course-bg.png',
isLiked: false,
isFavorited: false,
showReplyBox: false,
replyContent: '',
readFlag: 0,
action: 'favorite'
}
]
totalPages.value = 1
total.value = 2
} else {
//
messages.value = []
}
} finally {
loading.value = false
}
} }
const goToPage = (page: number) => { const goToPage = (page: number) => {
@ -224,6 +231,30 @@ const goToPage = (page: number) => {
background-color: #fff; background-color: #fff;
} }
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
}
.empty-text {
color: #999;
font-size: 14px;
}
.message-list { .message-list {
padding: 0; padding: 0;
} }
@ -265,7 +296,7 @@ const goToPage = (page: number) => {
gap: 8px; gap: 8px;
} }
.header-left{ .header-left {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-direction: column; flex-direction: column;
@ -284,7 +315,7 @@ const goToPage = (page: number) => {
margin-left: 4px; margin-left: 4px;
} }
.course-info{ .course-info {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
color: #585858; color: #585858;
@ -451,21 +482,21 @@ const goToPage = (page: number) => {
} }
.page-btn:hover:not(:disabled) { .page-btn:hover:not(:disabled) {
border-color: #1890ff; border-color: #0089D1;
color: #1890ff; color: #0089D1;
} }
.page-btn.active { .page-btn.active {
background: #1890ff; background: #0089D1;
border-color: #1890ff; border-color: #0089D1;
color: #fff; color: #fff;
} }
.page-btn:disabled { .page-btn:disabled {
color: #bfbfbf; color: #878787;
border-color: #f0f0f0; border: none;
cursor: not-allowed; cursor: not-allowed;
background: #fafafa; background: #fff;
} }
.ellipsis { .ellipsis {

View File

@ -66,72 +66,17 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
import { NotificationsOffOutline } from '@vicons/ionicons5' import { NotificationsOffOutline } from '@vicons/ionicons5'
import { MessageApi, type SystemMessage, type BackendMessageItem } from '@/api'
//
interface SystemMessage {
id: number
title: string
content: string
timestamp: string
isRead: boolean
type: 'info' | 'warning' | 'success' | 'error'
}
// //
const messages = ref<SystemMessage[]>([ const messages = ref<SystemMessage[]>([])
{ const loading = ref(false)
id: 1, const total = ref(0)
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!', const pageSize = ref(10)
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
},
{
id: 2,
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
},
{
id: 3,
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
},
{
id: 4,
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
},
{
id: 5,
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
},
{
id: 6,
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
}
])
// //
const currentPage = ref(1) const currentPage = ref(1)
const totalPages = ref(29) const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
// //
const visiblePages = computed(() => { const visiblePages = computed(() => {
@ -150,14 +95,117 @@ const showEllipsis = computed(() => {
return currentPage.value + 2 < totalPages.value - 1 return currentPage.value + 2 < totalPages.value - 1
}) })
//
const transformSystemMessageData = (backendItem: BackendMessageItem): SystemMessage => {
let title = backendItem.titile || '系统消息'
let content = '暂无内容'
// msgContent JSON
try {
if (backendItem.msgContent) {
const parsedContent = JSON.parse(backendItem.msgContent)
console.log('🔍 解析系统消息内容:', parsedContent)
//
if (parsedContent.sender && parsedContent.comment) {
//
title = `${parsedContent.sender.username} 评论了你的课程`
content = parsedContent.comment.content || '暂无评论内容'
// @
content = processAtMentions(content)
} else if (parsedContent.sender && parsedContent.entity) {
//
title = `${parsedContent.sender.username}${parsedContent.entity.type}消息`
content = parsedContent.entity.title || '暂无内容'
} else {
// 使
content = backendItem.msgContent
}
}
} catch (error) {
console.warn('⚠️ 解析系统消息内容失败:', error)
// 使
content = backendItem.msgContent || '暂无内容'
}
return {
id: backendItem.id,
title,
content,
timestamp: backendItem.sendTime || new Date().toLocaleString('zh-CN'),
isRead: backendItem.readFlag === 1,
type: 'info' //
}
}
// @
const processAtMentions = (content: string): string => {
if (!content) return content
// [user:id:username] @username
return content.replace(/\[user:(\d+):([^\]]+)\]/g, '@$2')
}
// //
onMounted(() => { onMounted(() => {
loadMessages() loadMessages()
}) })
// //
const loadMessages = () => { const loadMessages = async () => {
// TODO: API try {
loading.value = true
console.log('🔍 开始加载系统消息,页码:', currentPage.value)
const response = await MessageApi.getSystemMessages({
pageNo: currentPage.value,
pageSize: pageSize.value
})
console.log('🔍 系统消息API响应:', response)
if (response.data && response.data.records) {
const result = response.data
console.log('✅ 系统消息数据:', result)
//
messages.value = result.records.map(transformSystemMessageData)
total.value = result.total || 0
console.log('✅ 转换后的系统消息:', messages.value)
} else {
console.warn('⚠️ 系统消息API返回错误:', response)
// 使
messages.value = [
{
id: '1',
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
}
]
total.value = 1
}
} catch (error) {
console.error('❌ 加载系统消息失败:', error)
// 使
messages.value = [
{
id: '1',
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
content: '好消息好消息BiliBiliWorld2024年在线大学6月29日周六正式开课',
timestamp: '7月20日 12:41',
isRead: false,
type: 'info'
}
]
total.value = 1
} finally {
loading.value = false
}
} }
const goToPage = (page: number) => { const goToPage = (page: number) => {