feat: 消息中心接入接口,修复分页功能样式,新增班级Excel导出,统计页课程详情接入接口
This commit is contained in:
parent
e687fa8ebd
commit
23c54eaf40
@ -13,6 +13,8 @@ export { default as UploadApi } from './modules/upload'
|
||||
export { default as StatisticsApi } from './modules/statistics'
|
||||
export { default as ExamApi } from './modules/exam'
|
||||
export { ChatApi } from './modules/chat'
|
||||
export { default as MessageApi } from './modules/message'
|
||||
export type { MessageItem, BackendMessageItem, SystemMessage } from './modules/message'
|
||||
|
||||
// API 基础配置
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
|
||||
@ -234,6 +236,17 @@ export const API_ENDPOINTS = {
|
||||
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: {
|
||||
DOWNLOAD: '/resources/:id/download',
|
||||
|
@ -1228,19 +1228,10 @@ export class CourseApi {
|
||||
// 获取课程评论列表
|
||||
static async getCourseComments(courseId: string): Promise<ApiResponse<CourseComment[]>> {
|
||||
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`)
|
||||
console.log('🔍 评论API响应:', response)
|
||||
|
||||
// 处理后端响应格式
|
||||
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) => ({
|
||||
id: comment.id,
|
||||
@ -1256,8 +1247,6 @@ export class CourseApi {
|
||||
timeAgo: this.formatTimeAgo(comment.createTime) // 计算相对时间
|
||||
}))
|
||||
|
||||
console.log('✅ 适配后的评论数据:', adaptedComments)
|
||||
|
||||
return {
|
||||
code: response.data.code,
|
||||
message: response.data.message,
|
||||
|
428
src/api/modules/message.ts
Normal file
428
src/api/modules/message.ts
Normal 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()
|
@ -27,6 +27,7 @@ export interface TeachCourse {
|
||||
max_enroll?: number | null
|
||||
status?: number | null
|
||||
question?: string | null
|
||||
categoryId?: string | number | null // 课程分类ID
|
||||
}
|
||||
|
||||
// 新建课程请求参数
|
||||
|
@ -62,15 +62,15 @@ request.interceptors.request.use(
|
||||
// 添加请求时间戳
|
||||
config.headers['X-Request-Time'] = Date.now().toString()
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🚀 Request:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
})
|
||||
}
|
||||
// 开发环境下打印请求信息(已禁用)
|
||||
// if (import.meta.env.DEV) {
|
||||
// console.log('🚀 Request:', {
|
||||
// url: config.url,
|
||||
// method: config.method,
|
||||
// params: config.params,
|
||||
// data: config.data,
|
||||
// })
|
||||
// }
|
||||
|
||||
return config
|
||||
},
|
||||
@ -85,14 +85,14 @@ request.interceptors.response.use(
|
||||
(response: AxiosResponse<any>) => {
|
||||
const { data } = response
|
||||
|
||||
// 开发环境下打印响应信息
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ Response:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
// 开发环境下打印响应信息(已禁用)
|
||||
// if (import.meta.env.DEV) {
|
||||
// console.log('✅ Response:', {
|
||||
// url: response.config.url,
|
||||
// status: response.status,
|
||||
// data: data,
|
||||
// })
|
||||
// }
|
||||
|
||||
// 如果是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) {
|
||||
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 {
|
||||
// 如果响应不是标准格式,包装成标准格式
|
||||
normalizedData = {
|
||||
@ -192,7 +207,10 @@ request.interceptors.response.use(
|
||||
errorMessage = '网络连接失败,请检查网络设置'
|
||||
}
|
||||
|
||||
// 对于404错误,不显示错误消息,因为可能是接口不存在
|
||||
if (response?.status !== 404) {
|
||||
showMessage(errorMessage, 'error')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
@ -91,9 +91,7 @@ interface HotSearchItem {
|
||||
// 获取热门搜索数据
|
||||
const fetchHotSearch = async () => {
|
||||
try {
|
||||
console.log('🚀 获取热门搜索数据...')
|
||||
const response = await ApiRequest.get<HotSearchItem[]>('/aiol/index/hot_search')
|
||||
console.log('📊 热门搜索API响应:', response)
|
||||
|
||||
if (response.data) {
|
||||
const apiResponse = response.data as any
|
||||
@ -104,7 +102,6 @@ const fetchHotSearch = async () => {
|
||||
if (success && apiResponse.result) {
|
||||
hotSearchList.value = apiResponse.result
|
||||
} else {
|
||||
console.error('❌ 获取热门搜索失败:', apiResponse.message || '未知错误')
|
||||
// 使用模拟数据
|
||||
hotSearchList.value = getMockHotSearch()
|
||||
}
|
||||
@ -112,17 +109,12 @@ const fetchHotSearch = async () => {
|
||||
// 直接是数组数据
|
||||
hotSearchList.value = apiResponse
|
||||
} else {
|
||||
console.error('❌ 热门搜索数据格式错误')
|
||||
hotSearchList.value = getMockHotSearch()
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 热门搜索响应为空')
|
||||
hotSearchList.value = getMockHotSearch()
|
||||
}
|
||||
|
||||
console.log('✅ 热门搜索数据加载完成:', hotSearchList.value)
|
||||
} catch (error) {
|
||||
console.error('❌ 获取热门搜索异常:', error)
|
||||
// 使用模拟数据
|
||||
hotSearchList.value = getMockHotSearch()
|
||||
}
|
||||
|
@ -64,7 +64,7 @@
|
||||
<n-button type="primary" ghost @click="showImportModal = true">
|
||||
导入
|
||||
</n-button>
|
||||
<n-button type="primary" ghost>
|
||||
<n-button type="primary" ghost @click="handleExport">
|
||||
导出
|
||||
</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入姓名/账号" style="width: 200px"
|
||||
@ -290,6 +290,7 @@ import {
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import ImportModal from '@/components/common/ImportModal.vue'
|
||||
import { exportTableToExcel, type ExportColumn } from '@/utils/excelExport'
|
||||
|
||||
// 定义 props 类型
|
||||
interface Props {
|
||||
@ -733,6 +734,82 @@ const handleBatchTransfer = () => {
|
||||
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 () => {
|
||||
if (!selectedTargetClass.value) {
|
||||
|
@ -365,6 +365,42 @@ const routes: RouteRecordRaw[] = [
|
||||
component: AIAssistantDetail,
|
||||
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',
|
||||
name: 'StudentManagement',
|
||||
|
117
src/utils/excelExport.ts
Normal file
117
src/utils/excelExport.ts
Normal 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
|
||||
})
|
||||
}
|
@ -11,7 +11,9 @@
|
||||
<div class="sidebar-container" v-if="!hideSidebar">
|
||||
<!-- 头像 -->
|
||||
<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">
|
||||
{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}
|
||||
</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="">
|
||||
<span>学员中心</span>
|
||||
<n-icon class="expand-icon" :class="{ expanded: studentMenuExpanded }">
|
||||
@ -82,7 +85,8 @@
|
||||
</router-link>
|
||||
<router-link to="/teacher/message-center" class="nav-item" :class="{ active: activeNavItem === 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>
|
||||
</router-link>
|
||||
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
|
||||
@ -101,6 +105,42 @@
|
||||
<span>AI助教</span>
|
||||
</router-link>
|
||||
</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>
|
||||
|
||||
<!-- 右侧路由视图 -->
|
||||
@ -146,10 +186,11 @@ const height = window.innerHeight;
|
||||
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 examMenuExpanded = ref(false); // 考试管理菜单展开状态
|
||||
const studentMenuExpanded = ref(false); // 学员中心菜单展开状态
|
||||
const orchestrationMenuExpanded = ref(false); // 智能体编排菜单展开状态
|
||||
const showTopImage = ref(true); // 控制顶部图片显示/隐藏
|
||||
|
||||
// 需要隐藏顶部图片的路由路径数组
|
||||
@ -165,7 +206,7 @@ const isAiHovered = ref(false);
|
||||
const isAiActive = computed(() => route.path.includes('/teacher/ai-assistant'));
|
||||
const breadcrumbDisplay = computed(() => {
|
||||
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));
|
||||
if (found) {
|
||||
return false;
|
||||
@ -183,8 +224,12 @@ const setActiveNavItem = (index: number) => {
|
||||
if (index !== 1) {
|
||||
studentMenuExpanded.value = false;
|
||||
}
|
||||
// 如果不是智能体编排,关闭智能体编排子菜单
|
||||
if (index !== 6) {
|
||||
orchestrationMenuExpanded.value = false;
|
||||
}
|
||||
// 如果切换到其他菜单,清空子菜单选中状态
|
||||
if (index !== 4 && index !== 1) {
|
||||
if (index !== 4 && index !== 1 && index !== 6) {
|
||||
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) => {
|
||||
activeSubNavItem.value = subItem;
|
||||
@ -235,6 +291,10 @@ const setActiveSubNavItem = (subItem: string) => {
|
||||
// 学员中心子菜单
|
||||
activeNavItem.value = 1;
|
||||
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;
|
||||
}
|
||||
|
||||
// 智能体编排模块的面包屑逻辑
|
||||
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;
|
||||
@ -731,6 +865,23 @@ const updateActiveNavItem = () => {
|
||||
activeSubNavItem.value = '';
|
||||
examMenuExpanded.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;
|
||||
height: calc(100vh - var(--top-height, 130px));
|
||||
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) {
|
||||
@ -966,6 +1127,10 @@ const updateActiveNavItem = () => {
|
||||
|
||||
}
|
||||
|
||||
/* 智能体编排菜单移除顶部外边距 */
|
||||
.orchestration-nav {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.nav-container .nav-item {
|
||||
margin-left: 15px;
|
||||
width: 210px;
|
||||
|
71
src/views/teacher/ai-orchestration/app-management.vue
Normal file
71
src/views/teacher/ai-orchestration/app-management.vue
Normal 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>
|
79
src/views/teacher/ai-orchestration/index.vue
Normal file
79
src/views/teacher/ai-orchestration/index.vue
Normal 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>
|
71
src/views/teacher/ai-orchestration/knowledge-base.vue
Normal file
71
src/views/teacher/ai-orchestration/knowledge-base.vue
Normal 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>
|
71
src/views/teacher/ai-orchestration/model-config.vue
Normal file
71
src/views/teacher/ai-orchestration/model-config.vue
Normal 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>
|
71
src/views/teacher/ai-orchestration/ocr-recognition.vue
Normal file
71
src/views/teacher/ai-orchestration/ocr-recognition.vue
Normal 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>
|
71
src/views/teacher/ai-orchestration/process-design.vue
Normal file
71
src/views/teacher/ai-orchestration/process-design.vue
Normal 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>
|
@ -15,15 +15,32 @@
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<p>正在加载课程详情...</p>
|
||||
</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="/images/teacher/fj.png" alt="课程封面" class="course-image" />
|
||||
<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>
|
||||
<p class="course-description">{{ courseInfo.description }}</p>
|
||||
<div class="course-description" v-html="cleanHtmlContent(courseInfo.description)"></div>
|
||||
|
||||
<!-- 课程关键信息 -->
|
||||
<div class="course-metrics">
|
||||
@ -41,11 +58,17 @@
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">课程讲师:</span>
|
||||
<span class="metric-value">{{ courseInfo.instructor }}</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">{{ courseInfo.teacherCount }}人</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>
|
||||
@ -56,9 +79,11 @@
|
||||
<!-- 开课学期选择 -->
|
||||
<div class="semester-section">
|
||||
<span class="semester-label">开课1学期</span>
|
||||
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select" size="small" />
|
||||
<n-select v-model:value="selectedSemester" :options="semesterOptions" class="semester-select"
|
||||
size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计数据 -->
|
||||
@ -133,15 +158,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
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 TeachingTeam from './tabs/TeachingTeam.vue'
|
||||
import ChapterList from './tabs/ChapterList.vue'
|
||||
import CourseComments from './tabs/CourseComments.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const courseId = ref(route.params.id as string)
|
||||
|
||||
// 顶部图片控制
|
||||
const showTopImage = ref(true) // 控制顶部图片显示/隐藏
|
||||
@ -155,9 +184,18 @@ const courseInfo = ref({
|
||||
duration: '4小时28分钟',
|
||||
instructor: '王建国',
|
||||
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({
|
||||
views: 0,
|
||||
@ -188,6 +226,213 @@ const goBack = () => {
|
||||
const handleClose = () => {
|
||||
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) {
|
||||
// 按sortOrder排序,如果没有sortOrder则按原始顺序
|
||||
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标签,直接返回(假设后端已经处理过安全性)
|
||||
// 这里可以添加更多的HTML清理逻辑,比如移除script标签等
|
||||
return content
|
||||
}
|
||||
|
||||
// 从课程管理API获取分类信息
|
||||
const loadCourseCategoryFromManagementAPI = async () => {
|
||||
try {
|
||||
// 调用课程管理API获取课程信息(包含categoryId)
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@ -281,6 +526,38 @@ const handleClose = () => {
|
||||
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 {
|
||||
flex: 0 0 305px;
|
||||
|
@ -65,8 +65,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { NBadge, NTabs, NTabPane, useMessage } from 'naive-ui'
|
||||
import { ChatApi } from '@/api'
|
||||
import { NBadge, NTabs, NTabPane } from 'naive-ui'
|
||||
import { ChatApi, MessageApi } from '@/api'
|
||||
|
||||
// 导入子组件
|
||||
import NotificationMessages from './components/NotificationMessages.vue'
|
||||
@ -76,7 +76,6 @@ import SystemMessages from './components/SystemMessages.vue'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('notification') // 当前激活的tab
|
||||
const message = useMessage()
|
||||
|
||||
// 各类消息数量(角标显示)
|
||||
const notificationCount = ref(0) // 即时消息数量
|
||||
@ -111,23 +110,48 @@ const loadMessageCounts = async (forceRefresh = false) => {
|
||||
// 检查缓存
|
||||
const now = Date.now()
|
||||
if (!forceRefresh && now - cacheTimestamp.value < CACHE_DURATION) {
|
||||
console.log('📋 使用缓存的消息数量,跳过加载')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔄 开始加载消息数量...')
|
||||
loading.value = true
|
||||
try {
|
||||
// 并行加载各类消息数量
|
||||
await Promise.allSettled([
|
||||
// 首先尝试使用统一的未读消息数接口
|
||||
const unreadCountResult = await Promise.allSettled([loadTotalUnreadCount()])
|
||||
console.log('🔍 统一接口结果:', unreadCountResult[0])
|
||||
|
||||
// 如果统一接口失败,则使用原有的分别加载方式
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('加载消息数量失败:', error)
|
||||
message.error('加载消息数量失败')
|
||||
// 不显示错误消息,因为可能只是部分接口失败
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -143,28 +167,49 @@ const debouncedRefresh = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 加载即时消息数量
|
||||
const loadNotificationCount = async () => {
|
||||
// 加载总的未读消息数
|
||||
const loadTotalUnreadCount = async () => {
|
||||
try {
|
||||
const response = await ChatApi.getUnreadCount()
|
||||
if (response.data) {
|
||||
notificationCount.value = response.data.total || 0
|
||||
return
|
||||
console.log('🔄 尝试调用统一未读消息数接口...')
|
||||
const response = await MessageApi.getUnreadMessageCount()
|
||||
console.log('🔍 统一接口响应:', response)
|
||||
|
||||
if (response.code === 200) {
|
||||
const result = response.data || {}
|
||||
console.log('📊 统一接口返回数据:', result)
|
||||
|
||||
// 使用统一接口返回的数据
|
||||
// 注意:当前API只返回total和unread字段,我们需要分别调用各个接口获取具体数量
|
||||
// 这里先设置默认值,后续会通过其他接口更新
|
||||
commentCount.value = 0
|
||||
favoriteCount.value = 0
|
||||
systemCount.value = 0
|
||||
notificationCount.value = 0
|
||||
|
||||
return true
|
||||
}
|
||||
console.log('❌ 统一接口响应格式不正确')
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('获取未读消息数量失败,尝试备用方案:', error)
|
||||
console.error('❌ 获取总未读消息数失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 备用方案:通过会话列表计算未读数量
|
||||
// 加载即时消息数量
|
||||
const loadNotificationCount = async () => {
|
||||
// 由于 /aiol/aiolChat/unread-count 接口不存在,直接使用备用方案
|
||||
try {
|
||||
const chatsResponse = await ChatApi.getMyChats()
|
||||
if (chatsResponse.data?.success) {
|
||||
notificationCount.value = chatsResponse.data.result.reduce((total: number, chat: any) => {
|
||||
return total + (chat.unreadCount || 0)
|
||||
}, 0)
|
||||
} else {
|
||||
notificationCount.value = 0
|
||||
}
|
||||
} catch (chatError) {
|
||||
console.error('获取会话列表失败:', chatError)
|
||||
// 静默处理错误,设置为0
|
||||
notificationCount.value = 0
|
||||
}
|
||||
}
|
||||
@ -172,8 +217,12 @@ const loadNotificationCount = async () => {
|
||||
// 加载评论和@数量
|
||||
const loadCommentCount = async () => {
|
||||
try {
|
||||
// TODO: 实现评论和@消息数量API
|
||||
const response = await MessageApi.getCommentsAtMessageCount()
|
||||
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) {
|
||||
console.error('获取评论数量失败:', error)
|
||||
commentCount.value = 0
|
||||
@ -183,10 +232,19 @@ const loadCommentCount = async () => {
|
||||
// 加载赞和收藏数量
|
||||
const loadFavoriteCount = async () => {
|
||||
try {
|
||||
// TODO: 实现赞和收藏消息数量API
|
||||
const response = await MessageApi.getLikesMessageCount()
|
||||
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) {
|
||||
console.error('获取收藏数量失败:', error)
|
||||
// 如果接口不存在,设置为0,不显示错误消息
|
||||
favoriteCount.value = 0
|
||||
}
|
||||
}
|
||||
@ -194,10 +252,19 @@ const loadFavoriteCount = async () => {
|
||||
// 加载系统消息数量
|
||||
const loadSystemCount = async () => {
|
||||
try {
|
||||
// TODO: 实现系统消息数量API
|
||||
console.log('🔍 开始加载系统消息数量')
|
||||
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) {
|
||||
console.error('获取系统消息数量失败:', error)
|
||||
console.error('❌ 获取系统消息数量失败:', error)
|
||||
systemCount.value = 0
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +127,7 @@
|
||||
</button>
|
||||
|
||||
<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(totalPages)">尾页</button>
|
||||
@ -140,7 +140,7 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useMessage, NIcon } from 'naive-ui'
|
||||
import { ChatbubbleEllipsesOutline, TrashOutline, WarningOutline } from '@vicons/ionicons5'
|
||||
import { CourseApi } from '@/api'
|
||||
import { MessageApi, BackendMessageItem } from '@/api'
|
||||
|
||||
// 消息类型定义
|
||||
interface Message {
|
||||
@ -161,17 +161,54 @@ interface Message {
|
||||
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 message = useMessage()
|
||||
const loading = ref(false)
|
||||
|
||||
// 可用课程列表
|
||||
const availableCourses = ref<Array<{ id: string, name: string }>>([
|
||||
{ id: '1', name: '测试课程1' },
|
||||
{ id: '2', name: '测试课程2' },
|
||||
{ id: '3', name: '测试课程3' }
|
||||
])
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
@ -196,6 +233,11 @@ const showEllipsis = computed(() => {
|
||||
return currentPage.value + 2 < totalPages.value - 1
|
||||
})
|
||||
|
||||
// 是否显示最后一页按钮
|
||||
const showLastPage = computed(() => {
|
||||
return totalPages.value > 1 && currentPage.value + 2 < totalPages.value
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadMessages()
|
||||
@ -205,60 +247,52 @@ onMounted(() => {
|
||||
const loadMessages = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取所有课程的评论数据
|
||||
const allComments: Message[] = []
|
||||
const response = await MessageApi.getCommentsAtMessages({
|
||||
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,
|
||||
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
|
||||
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: course.id,
|
||||
userId: comment.userId,
|
||||
images: comment.images || [],
|
||||
likeCount: comment.likeCount || 0
|
||||
}))
|
||||
|
||||
allComments.push(...courseComments)
|
||||
courseId: '1954463468539371522',
|
||||
userId: '1966804797404344321',
|
||||
images: [],
|
||||
likeCount: 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`获取课程 ${course.name} 评论失败:`, error)
|
||||
]
|
||||
totalPages.value = 1
|
||||
totalCount.value = 1
|
||||
} else {
|
||||
// 静默处理其他错误,不显示错误消息
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序(最新的在前)
|
||||
allComments.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (currentPage.value - 1) * pageSize.value
|
||||
const endIndex = startIndex + pageSize.value
|
||||
messages.value = allComments.slice(startIndex, endIndex)
|
||||
|
||||
// 更新分页信息
|
||||
totalCount.value = allComments.length
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
|
||||
console.log('✅ 加载评论和@消息成功:', {
|
||||
total: totalCount.value,
|
||||
current: messages.value.length,
|
||||
page: currentPage.value
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 加载评论和@消息失败:', error)
|
||||
message.error('加载消息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -575,6 +609,7 @@ const goToPage = (page: number) => {
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -695,21 +730,21 @@ const goToPage = (page: number) => {
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
border-color: #0089D1;
|
||||
color: #0089D1;
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
background: #0089D1;
|
||||
border-color: #0089D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
color: #bfbfbf;
|
||||
border-color: #f0f0f0;
|
||||
color: #878787;
|
||||
border: none;
|
||||
cursor: not-allowed;
|
||||
background: #fafafa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
|
@ -1,44 +1,52 @@
|
||||
<template>
|
||||
<div class="message-center">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list">
|
||||
<div v-for="message in messages" :key="message.id" class="message-item">
|
||||
<div v-else class="message-list">
|
||||
<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">
|
||||
<img :src="message.avatar" :alt="message.username" class="avatar" />
|
||||
<img :src="messageItem.avatar" :alt="messageItem.username" class="avatar" />
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-content">
|
||||
<!-- 点赞消息 -->
|
||||
<div class="message-header" v-if="message.type === 0">
|
||||
<div class="message-header" v-if="messageItem.type === 0">
|
||||
<div>
|
||||
<span class="username">
|
||||
{{ message.username }}赞了我的评论:
|
||||
{{ messageItem.username }}赞了我的评论:
|
||||
</span>
|
||||
<span class="content">{{ message.content }}</span>
|
||||
<span class="content">{{ messageItem.content }}</span>
|
||||
</div>
|
||||
<span class="timestamp">{{ message.timestamp }}</span>
|
||||
<span class="timestamp">{{ messageItem.timestamp }}</span>
|
||||
</div>
|
||||
<!-- 收藏消息 -->
|
||||
<div class="message-header" v-if="message.type === 1">
|
||||
<div class="message-header" v-if="messageItem.type === 1">
|
||||
<div class="header-left">
|
||||
<div>
|
||||
<span class="username">
|
||||
{{ message.username }}收藏了我的课程:
|
||||
{{ messageItem.username }}收藏了我的课程:
|
||||
</span>
|
||||
<span class="course-info">{{ message.courseInfo }}</span>
|
||||
<span class="course-info">{{ messageItem.courseInfo }}</span>
|
||||
</div>
|
||||
<span class="timestamp">{{ message.timestamp }}</span>
|
||||
<span class="timestamp">{{ messageItem.timestamp }}</span>
|
||||
</div>
|
||||
<!-- 课程封面图片 - 只有收藏消息才显示 -->
|
||||
<div v-if="message.type === 1 && message.courseImage" class="course-image-container">
|
||||
<img :src="message.courseImage" :alt="message.courseInfo" class="course-image" />
|
||||
<div v-if="messageItem.type === 1 && messageItem.courseImage" class="course-image-container">
|
||||
<img :src="messageItem.courseImage" :alt="messageItem.courseInfo" class="course-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-text" v-if="message.type === 0">课程:
|
||||
<span class="course-info">{{ message.courseInfo }}</span>
|
||||
<div class="message-text" v-if="messageItem.type === 0">课程:
|
||||
<span class="course-info">{{ messageItem.courseInfo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,7 +63,7 @@
|
||||
</button>
|
||||
|
||||
<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(totalPages)">尾页</button>
|
||||
@ -65,124 +73,54 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { MessageApi, type MessageItem, type BackendMessageItem } from '@/api'
|
||||
|
||||
// 消息类型定义
|
||||
interface Message {
|
||||
id: number
|
||||
type: number // 0-点赞, 1-收藏
|
||||
username: string
|
||||
avatar: string
|
||||
courseInfo: string
|
||||
content: string
|
||||
timestamp: string
|
||||
courseImage?: string
|
||||
isLiked: boolean
|
||||
isFavorited: boolean
|
||||
showReplyBox: boolean
|
||||
replyContent: string
|
||||
// 消息类型定义(使用API中的类型)
|
||||
type Message = MessageItem
|
||||
|
||||
// 数据转换函数
|
||||
const transformMessageData = (backendItem: BackendMessageItem): MessageItem => {
|
||||
let parsedContent: any = 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: '/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[]>([
|
||||
{
|
||||
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 messages = ref<Message[]>([])
|
||||
|
||||
// 分页相关
|
||||
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(() => {
|
||||
@ -201,14 +139,83 @@ const showEllipsis = computed(() => {
|
||||
return currentPage.value + 2 < totalPages.value - 1
|
||||
})
|
||||
|
||||
// 是否显示最后一页按钮
|
||||
const showLastPage = computed(() => {
|
||||
return totalPages.value > 1 && currentPage.value + 2 < totalPages.value
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadMessages()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadMessages = () => {
|
||||
// TODO: 调用API加载消息数据
|
||||
const loadMessages = async () => {
|
||||
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) => {
|
||||
@ -224,6 +231,30 @@ const goToPage = (page: number) => {
|
||||
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 {
|
||||
padding: 0;
|
||||
}
|
||||
@ -451,21 +482,21 @@ const goToPage = (page: number) => {
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
border-color: #0089D1;
|
||||
color: #0089D1;
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
background: #0089D1;
|
||||
border-color: #0089D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
color: #bfbfbf;
|
||||
border-color: #f0f0f0;
|
||||
color: #878787;
|
||||
border: none;
|
||||
cursor: not-allowed;
|
||||
background: #fafafa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
|
@ -66,72 +66,17 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { NotificationsOffOutline } from '@vicons/ionicons5'
|
||||
|
||||
// 系统消息类型定义
|
||||
interface SystemMessage {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
timestamp: string
|
||||
isRead: boolean
|
||||
type: 'info' | 'warning' | 'success' | 'error'
|
||||
}
|
||||
import { MessageApi, type SystemMessage, type BackendMessageItem } from '@/api'
|
||||
|
||||
// 响应式数据
|
||||
const messages = ref<SystemMessage[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '你的会员已专属权利已更新,保来享取今日内障,仅此一天!',
|
||||
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 messages = ref<SystemMessage[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(29)
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
|
||||
|
||||
// 计算显示的页码
|
||||
const visiblePages = computed(() => {
|
||||
@ -150,14 +95,117 @@ const showEllipsis = computed(() => {
|
||||
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(() => {
|
||||
loadMessages()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadMessages = () => {
|
||||
// TODO: 调用API加载系统消息数据
|
||||
const loadMessages = async () => {
|
||||
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) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user