feat:ai对话和我们的视频播放
This commit is contained in:
parent
f152497cab
commit
2e3b6a6cf7
396
src/api/modules/ai.ts
Normal file
396
src/api/modules/ai.ts
Normal file
@ -0,0 +1,396 @@
|
||||
import { ApiRequest } from '../request'
|
||||
import type { ApiResponse } from '../types'
|
||||
|
||||
// AI聊天请求接口
|
||||
export interface AIChatRequest {
|
||||
appId: string
|
||||
content: string
|
||||
responseMode: string
|
||||
[property: string]: any
|
||||
}
|
||||
|
||||
// AI聊天响应接口
|
||||
export interface AIChatResponse extends ApiResponse {
|
||||
data: {
|
||||
content: string
|
||||
messageId?: string
|
||||
conversationId?: string
|
||||
[property: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
// AI聊天API类
|
||||
export class AIApi {
|
||||
/**
|
||||
* 发送AI聊天消息 - 测试连接
|
||||
* @param content 用户输入的消息内容
|
||||
* @returns Promise<any>
|
||||
*/
|
||||
static async sendChatMessage(content: string): Promise<any> {
|
||||
const request: AIChatRequest = {
|
||||
appId: "1970031066993217537",
|
||||
content: content,
|
||||
responseMode: "streaming"
|
||||
}
|
||||
|
||||
console.log('测试AI接口连接:', request)
|
||||
|
||||
try {
|
||||
const response = await ApiRequest.post('/airag/chat/send', request)
|
||||
console.log('AI接口响应:', response)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('AI聊天接口调用失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的fetch测试方法
|
||||
* @param content 用户输入的消息内容
|
||||
*/
|
||||
static async testFetchConnection(content: string): Promise<void> {
|
||||
const request: AIChatRequest = {
|
||||
appId: "1970031066993217537",
|
||||
content: content,
|
||||
responseMode: "streaming"
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('X-Access-Token') || localStorage.getItem('token') || ''
|
||||
|
||||
// 在开发环境使用代理路径,生产环境使用完整URL
|
||||
const apiUrl = import.meta.env.DEV
|
||||
? '/jeecgboot/airag/chat/send' // 开发环境使用代理
|
||||
: `${import.meta.env.VITE_API_BASE_URL}/airag/chat/send` // 生产环境使用完整URL
|
||||
|
||||
console.log('测试fetch连接:', {
|
||||
url: apiUrl,
|
||||
request,
|
||||
hasToken: !!token,
|
||||
baseUrl: import.meta.env.VITE_API_BASE_URL,
|
||||
isDev: import.meta.env.DEV
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-Time': Date.now().toString(),
|
||||
...(token && {
|
||||
'X-Access-Token': token
|
||||
})
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
console.log('Fetch响应:', {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Fetch错误响应:', errorText)
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
console.log('响应内容:', responseText)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch测试失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送AI聊天消息 - 流式响应版本 (处理EventStream)
|
||||
* @param content 用户输入的消息内容
|
||||
* @param onMessage 接收流式消息的回调函数
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
static async sendChatMessageStream(
|
||||
content: string,
|
||||
onMessage: (chunk: string) => void,
|
||||
onComplete?: () => void,
|
||||
onError?: (error: any) => void
|
||||
): Promise<void> {
|
||||
const request: AIChatRequest = {
|
||||
appId: "1970031066993217537",
|
||||
content: content,
|
||||
responseMode: "streaming"
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取token - 使用项目标准的token获取方式
|
||||
const token = localStorage.getItem('X-Access-Token') || localStorage.getItem('token') || ''
|
||||
|
||||
// 在开发环境使用代理路径,生产环境使用完整URL
|
||||
const apiUrl = import.meta.env.DEV
|
||||
? '/jeecgboot/airag/chat/send' // 开发环境使用代理
|
||||
: `${import.meta.env.VITE_API_BASE_URL}/airag/chat/send` // 生产环境使用完整URL
|
||||
|
||||
console.log('准备发送AI请求:', {
|
||||
url: apiUrl,
|
||||
request,
|
||||
hasToken: !!token,
|
||||
isDev: import.meta.env.DEV
|
||||
})
|
||||
|
||||
// 使用fetch进行SSE流式请求
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Request-Time': Date.now().toString(),
|
||||
// 如果需要认证,添加token
|
||||
...(token && {
|
||||
'X-Access-Token': token
|
||||
})
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
console.log('AI请求响应状态:', response.status, response.statusText)
|
||||
console.log('响应头:', Object.fromEntries(response.headers.entries()))
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => '无法读取错误信息')
|
||||
console.error('AI请求失败:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorText
|
||||
})
|
||||
throw new Error(`HTTP error! status: ${response.status} - ${response.statusText}. ${errorText}`)
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error('无法获取响应流')
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let messageEndReceived = false
|
||||
let endTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
console.log('SSE流结束')
|
||||
// 清除可能存在的超时
|
||||
if (endTimeout) {
|
||||
clearTimeout(endTimeout)
|
||||
}
|
||||
onComplete?.()
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log('收到原始数据块:', chunk)
|
||||
|
||||
// 如果收到新数据,清除结束超时
|
||||
if (endTimeout) {
|
||||
clearTimeout(endTimeout)
|
||||
endTimeout = null
|
||||
}
|
||||
|
||||
buffer += chunk
|
||||
|
||||
// 按行分割处理SSE数据
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || '' // 保留最后一个可能不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
if (trimmedLine === '') {
|
||||
// 空行,跳过
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理SSE数据行 - 支持 "data:" 和 "data: " 两种格式
|
||||
let jsonStr = ''
|
||||
let isDataLine = false
|
||||
|
||||
if (trimmedLine.startsWith('data: ')) {
|
||||
// 标准格式:data: {"message":"内容"}
|
||||
jsonStr = trimmedLine.slice(6) // 去掉 "data: " 前缀
|
||||
isDataLine = true
|
||||
} else if (trimmedLine.startsWith('data:')) {
|
||||
// 非标准格式:data:{"message":"内容"}
|
||||
jsonStr = trimmedLine.slice(5) // 去掉 "data:" 前缀
|
||||
isDataLine = true
|
||||
}
|
||||
|
||||
if (isDataLine) {
|
||||
if (jsonStr === '[DONE]') {
|
||||
console.log('收到结束标志')
|
||||
onComplete?.()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
console.log('收到SSE数据:', data)
|
||||
|
||||
// 处理不同的事件类型
|
||||
console.log('处理事件:', data.event, '消息内容:', data.data?.message || '无消息')
|
||||
|
||||
if (data.event === 'MESSAGE_END') {
|
||||
console.log('收到消息结束标志,设置延迟结束')
|
||||
messageEndReceived = true
|
||||
// 清除之前的超时
|
||||
if (endTimeout) {
|
||||
clearTimeout(endTimeout)
|
||||
}
|
||||
// 设置2秒超时,如果没有新数据就结束
|
||||
endTimeout = setTimeout(() => {
|
||||
console.log('超时结束,调用完成回调')
|
||||
onComplete?.()
|
||||
}, 2000)
|
||||
} else if (data.event === 'INIT_REQUEST_ID') {
|
||||
console.log('收到初始化事件,跳过')
|
||||
// 初始化事件,不处理消息内容,但继续处理后续数据
|
||||
// 不要return,让循环继续处理下一行
|
||||
} else if (data.event === 'MESSAGE' && data.data && data.data.message) {
|
||||
// 正常的消息事件
|
||||
console.log('发送消息块到界面:', data.data.message)
|
||||
onMessage(data.data.message)
|
||||
} else if (data.data && data.data.message) {
|
||||
// 兼容其他格式
|
||||
console.log('发送消息块到界面(兼容格式):', data.data.message)
|
||||
onMessage(data.data.message)
|
||||
} else if (data.message) {
|
||||
console.log('发送消息块到界面(直接格式):', data.message)
|
||||
onMessage(data.message)
|
||||
} else if (data.content) {
|
||||
console.log('发送消息块到界面(content格式):', data.content)
|
||||
onMessage(data.content)
|
||||
} else if (typeof data === 'string') {
|
||||
console.log('发送消息块到界面(字符串格式):', data)
|
||||
onMessage(data)
|
||||
} else {
|
||||
console.log('未处理的数据格式:', data)
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('解析SSE JSON数据失败:', parseError, '原始数据:', jsonStr)
|
||||
// 如果解析失败,尝试直接使用原始字符串
|
||||
if (jsonStr && jsonStr !== 'null') {
|
||||
onMessage(jsonStr)
|
||||
}
|
||||
}
|
||||
} else if (trimmedLine.startsWith('event: ') || trimmedLine.startsWith('id: ') || trimmedLine.startsWith('retry: ')) {
|
||||
// SSE元数据行,记录但不处理
|
||||
console.log('SSE元数据:', trimmedLine)
|
||||
} else {
|
||||
// 其他格式的数据,可能是直接的JSON
|
||||
console.log('其他格式数据:', trimmedLine)
|
||||
try {
|
||||
const data = JSON.parse(trimmedLine)
|
||||
|
||||
// 处理不同的事件类型
|
||||
if (data.event === 'MESSAGE_END') {
|
||||
console.log('收到消息结束标志,但继续监听可能的后续数据')
|
||||
// 不要立即return,继续监听可能的后续数据
|
||||
} else if (data.event === 'INIT_REQUEST_ID') {
|
||||
console.log('收到初始化事件,跳过')
|
||||
// 不要return,让循环继续处理下一行
|
||||
} else if (data.event === 'MESSAGE' && data.data && data.data.message) {
|
||||
onMessage(data.data.message)
|
||||
} else if (data.data && data.data.message) {
|
||||
onMessage(data.data.message)
|
||||
} else if (data.message) {
|
||||
onMessage(data.message)
|
||||
}
|
||||
} catch (e) {
|
||||
// 不是JSON格式,可能是其他SSE元数据,忽略
|
||||
console.log('跳过非JSON数据:', trimmedLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI流式聊天接口调用失败:', error)
|
||||
onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用EventSource发送AI聊天消息 - 备用方案
|
||||
* @param content 用户输入的消息内容
|
||||
* @param onMessage 接收流式消息的回调函数
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
static async sendChatMessageWithEventSource(
|
||||
content: string,
|
||||
onMessage: (chunk: string) => void,
|
||||
onComplete?: () => void,
|
||||
onError?: (error: any) => void
|
||||
): Promise<void> {
|
||||
const request: AIChatRequest = {
|
||||
appId: "1970031066993217537",
|
||||
content: content,
|
||||
responseMode: "streaming"
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取token
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// 构建URL参数
|
||||
const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/airag/chat/send`)
|
||||
|
||||
// 创建EventSource连接
|
||||
const eventSource = new EventSource(url.toString())
|
||||
|
||||
// 发送POST请求数据(EventSource只支持GET,所以这里需要特殊处理)
|
||||
// 注意:标准EventSource不支持POST,如果API必须使用POST,需要使用fetch方式
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
console.log('EventSource收到消息:', event.data)
|
||||
|
||||
if (event.data === '[DONE]') {
|
||||
eventSource.close()
|
||||
onComplete?.()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.data && data.data.message) {
|
||||
onMessage(data.data.message)
|
||||
} else if (data.message) {
|
||||
onMessage(data.message)
|
||||
} else if (data.content) {
|
||||
onMessage(data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('EventSource错误:', error)
|
||||
eventSource.close()
|
||||
onError?.(error)
|
||||
}
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('EventSource连接已建立')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('EventSource聊天接口调用失败:', error)
|
||||
onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
@ -1124,6 +1124,11 @@ export class CourseApi {
|
||||
// 解析fileUrl中的多个清晰度URL
|
||||
const qualities = this.parseVideoQualities(video.fileUrl)
|
||||
|
||||
// 选择默认清晰度:优先选择720p,如果没有则选择第一个可用的清晰度
|
||||
const defaultQuality = qualities.find(q => q.value === '720')?.value ||
|
||||
qualities.find(q => q.value === '480')?.value ||
|
||||
qualities[0]?.value || '360'
|
||||
|
||||
return {
|
||||
id: video.id,
|
||||
name: video.name,
|
||||
@ -1133,8 +1138,8 @@ export class CourseApi {
|
||||
duration: video.duration,
|
||||
fileSize: video.fileSize,
|
||||
qualities: qualities,
|
||||
defaultQuality: '360', // 默认360p
|
||||
currentQuality: '360' // 当前选中360p
|
||||
defaultQuality: defaultQuality,
|
||||
currentQuality: defaultQuality
|
||||
}
|
||||
})
|
||||
|
||||
@ -1182,24 +1187,28 @@ export class CourseApi {
|
||||
const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url.length > 0)
|
||||
console.log('🔍 分割后的视频URL:', urls)
|
||||
|
||||
// 支持的清晰度列表(按优先级排序)
|
||||
const supportedQualities = [
|
||||
{ value: '1080', label: '1080p' },
|
||||
{ value: '720', label: '720p' },
|
||||
{ value: '480', label: '480p' },
|
||||
{ value: '360', label: '360p' }
|
||||
]
|
||||
// 根据URL中的清晰度标识来解析
|
||||
urls.forEach((url) => {
|
||||
let quality = { label: '360p', value: '360', url: url }
|
||||
|
||||
// 根据URL数量分配清晰度
|
||||
// 假设URL按清晰度从高到低排列:1080p, 720p, 480p, 360p
|
||||
urls.forEach((url, index) => {
|
||||
if (index < supportedQualities.length) {
|
||||
qualities.push({
|
||||
label: supportedQualities[index].label,
|
||||
value: supportedQualities[index].value,
|
||||
url: url
|
||||
})
|
||||
// 从URL中提取清晰度信息
|
||||
if (url.includes('/1080p/')) {
|
||||
quality = { label: '1080p', value: '1080', url: url }
|
||||
} else if (url.includes('/720p/')) {
|
||||
quality = { label: '720p', value: '720', url: url }
|
||||
} else if (url.includes('/480p/')) {
|
||||
quality = { label: '480p', value: '480', url: url }
|
||||
} else if (url.includes('/360p/')) {
|
||||
quality = { label: '360p', value: '360', url: url }
|
||||
}
|
||||
|
||||
qualities.push(quality)
|
||||
})
|
||||
|
||||
// 按清晰度从高到低排序
|
||||
qualities.sort((a, b) => {
|
||||
const order = { '1080': 4, '720': 3, '480': 2, '360': 1 }
|
||||
return (order[b.value as keyof typeof order] || 0) - (order[a.value as keyof typeof order] || 0)
|
||||
})
|
||||
|
||||
// 如果没有解析到任何清晰度,使用第一个URL作为360p
|
||||
@ -1295,6 +1304,91 @@ export class CourseApi {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取更多课程列表
|
||||
static async getMoreCourses(courseId: string): Promise<ApiResponse<any[]>> {
|
||||
try {
|
||||
console.log('🔍 获取更多课程数据,课程ID:', courseId)
|
||||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/more_courses')
|
||||
|
||||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${courseId}/more_courses`)
|
||||
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 adaptedCourses = response.data.result.map((course: any) => ({
|
||||
id: course.id,
|
||||
title: course.name || '',
|
||||
description: this.stripHtmlTags(course.description || ''),
|
||||
coverImage: course.cover || '',
|
||||
thumbnail: course.cover || '',
|
||||
instructor: course.teacherList?.map((t: any) => t.name).join(', ') || '未知讲师',
|
||||
duration: course.duration || 0,
|
||||
chaptersCount: course.chaptersCount || 0,
|
||||
lessonsCount: course.lessonsCount || 0,
|
||||
enrolledCount: course.enrollCount || 0,
|
||||
price: course.price || 0,
|
||||
originalPrice: course.originalPrice || 0,
|
||||
rating: course.rating || 5,
|
||||
category: course.subject || '',
|
||||
tags: course.tags || [],
|
||||
startTime: course.startTime || '',
|
||||
endTime: course.endTime || '',
|
||||
isEnrolled: course.isEnrolled || false,
|
||||
createdAt: course.createTime ? new Date(course.createTime).getTime() : Date.now(),
|
||||
updatedAt: course.updateTime ? new Date(course.updateTime).getTime() : Date.now()
|
||||
}))
|
||||
|
||||
console.log('✅ 适配后的课程数据:', adaptedCourses)
|
||||
|
||||
return {
|
||||
code: response.data.code,
|
||||
message: response.data.message,
|
||||
data: adaptedCourses
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||||
return {
|
||||
code: 500,
|
||||
message: '数据格式错误',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 更多课程API调用失败:', error)
|
||||
console.error('❌ 错误详情:', {
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
response: (error as any).response?.data,
|
||||
status: (error as any).response?.status,
|
||||
statusText: (error as any).response?.statusText
|
||||
})
|
||||
|
||||
// 重新抛出错误,不使用模拟数据
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 去除HTML标签的辅助方法
|
||||
private static stripHtmlTags(html: string): string {
|
||||
if (!html) return ''
|
||||
|
||||
// 创建一个临时的div元素来解析HTML
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.innerHTML = html
|
||||
|
||||
// 获取纯文本内容
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || ''
|
||||
|
||||
// 清理多余的空白字符
|
||||
return textContent.trim().replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CourseApi
|
||||
|
@ -183,12 +183,16 @@ const initializePlayer = async (videoUrl?: string) => {
|
||||
player = null
|
||||
}
|
||||
|
||||
// 检查是否为HLS流
|
||||
const isHLS = url.includes('.m3u8')
|
||||
console.log('🔍 视频类型检测:', { url, isHLS })
|
||||
|
||||
// 构建DPlayer配置
|
||||
const dplayerConfig: any = {
|
||||
container: dplayerContainer.value,
|
||||
video: {
|
||||
url: url,
|
||||
type: 'auto'
|
||||
type: isHLS ? 'hls' : 'auto'
|
||||
},
|
||||
autoplay: props.autoplay,
|
||||
theme: '#007bff',
|
||||
@ -211,6 +215,29 @@ const initializePlayer = async (videoUrl?: string) => {
|
||||
]
|
||||
}
|
||||
|
||||
// 如果有多个清晰度,添加清晰度切换功能
|
||||
if (props.videoQualities && props.videoQualities.length > 1) {
|
||||
console.log('🔧 配置多清晰度支持:', props.videoQualities)
|
||||
dplayerConfig.quality = {
|
||||
default: props.currentQuality || props.videoQualities[0]?.value,
|
||||
options: props.videoQualities.map(q => ({
|
||||
name: q.label,
|
||||
url: q.url,
|
||||
type: isHLS ? 'hls' : 'auto'
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是HLS流,添加HLS.js支持
|
||||
if (isHLS) {
|
||||
console.log('🔧 配置HLS支持')
|
||||
dplayerConfig.pluginOptions = {
|
||||
hls: {
|
||||
// HLS.js配置选项
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有多个清晰度,启用DPlayer原生清晰度切换
|
||||
console.log('🔍 检查清晰度配置:', {
|
||||
videoQualities: props.videoQualities,
|
||||
@ -224,31 +251,56 @@ const initializePlayer = async (videoUrl?: string) => {
|
||||
availableQualities: props.videoQualities?.length || 0
|
||||
})
|
||||
|
||||
console.log('🔨 创建DPlayer实例,配置:', {
|
||||
...dplayerConfig,
|
||||
quality: dplayerConfig.quality ? '已配置多清晰度' : '单一清晰度'
|
||||
})
|
||||
player = new DPlayer(dplayerConfig)
|
||||
|
||||
// 检查DPlayer是否正确加载了quality配置
|
||||
console.log('🔍 DPlayer实例创建完成:', {
|
||||
hasQuality: !!player.quality,
|
||||
qualityOptions: player.quality?.options,
|
||||
playerConfig: dplayerConfig
|
||||
qualityDefault: player.quality?.default,
|
||||
videoElement: player.video
|
||||
})
|
||||
|
||||
// 事件监听
|
||||
player.on('play', () => {
|
||||
console.log('🎬 视频开始播放')
|
||||
isPlaying.value = true
|
||||
emit('play')
|
||||
})
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('⏸️ 视频暂停')
|
||||
isPlaying.value = false
|
||||
emit('pause')
|
||||
})
|
||||
|
||||
player.on('ended', () => {
|
||||
console.log('🏁 视频播放结束')
|
||||
isPlaying.value = false
|
||||
emit('ended')
|
||||
})
|
||||
|
||||
player.on('error', (error: any) => {
|
||||
console.error('❌ DPlayer播放错误:', error)
|
||||
emit('error', error)
|
||||
})
|
||||
|
||||
player.on('loadstart', () => {
|
||||
console.log('📥 开始加载视频')
|
||||
})
|
||||
|
||||
player.on('canplay', () => {
|
||||
console.log('✅ 视频可以播放')
|
||||
})
|
||||
|
||||
player.on('loadedmetadata', () => {
|
||||
console.log('📊 视频元数据加载完成')
|
||||
})
|
||||
|
||||
player.on('error', (error: any) => {
|
||||
// 检查是否为无害的错误
|
||||
const isHarmlessError = (
|
||||
|
@ -269,8 +269,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放器区域 - 已兑换状态,讨论模式下隐藏 -->
|
||||
<div v-else-if="!discussionMode" class="video-player-section">
|
||||
<!-- 视频播放器区域 - 已兑换状态,练习模式和讨论模式下隐藏 -->
|
||||
<div v-else-if="!practiceMode && !discussionMode" class="video-player-section">
|
||||
<div class="video-player enrolled">
|
||||
<div class="video-container">
|
||||
<!-- DPlayer 播放器 -->
|
||||
@ -739,18 +739,9 @@
|
||||
<div class="sidebar">
|
||||
|
||||
|
||||
<!-- 学期选择器 -->
|
||||
<div class="semester-selector">
|
||||
<select class="semester-dropdown">
|
||||
<option value="2025-spring">2025年上学期</option>
|
||||
<option value="2025-fall">2025年下学期</option>
|
||||
<option value="2024-spring">2024年上学期</option>
|
||||
</select>
|
||||
<div class="dropdown-arrow">
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none">
|
||||
<path d="M1 1L6 6L11 1" stroke="#0088D1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 学期显示 -->
|
||||
<div class="semester-display">
|
||||
<span class="semester-text">2025年上学期</span>
|
||||
</div>
|
||||
|
||||
<!-- 开课时间 -->
|
||||
@ -956,56 +947,43 @@
|
||||
<div class="more-courses-header">
|
||||
<h3>更多课程</h3>
|
||||
</div>
|
||||
<div class="more-courses-list">
|
||||
<div class="course-card">
|
||||
<div v-if="moreCoursesLoading" class="more-courses-loading">
|
||||
<p>正在加载更多课程...</p>
|
||||
</div>
|
||||
<div v-else-if="moreCoursesError" class="more-courses-error">
|
||||
<p>{{ moreCoursesError }}</p>
|
||||
<button @click="loadMoreCourses" class="retry-btn">重试</button>
|
||||
</div>
|
||||
<div v-else-if="moreCourses.length > 0" class="more-courses-list">
|
||||
<div v-for="course in moreCourses" :key="course.id" class="course-card">
|
||||
<div class="course-cover">
|
||||
<div class="course-image computer-bg">
|
||||
<img src="/images/courses/course-activities1.png" alt="">
|
||||
<img :src="course.coverImage || course.thumbnail || '/images/courses/course-activities1.png'"
|
||||
:alt="course.title"
|
||||
@error="handleImageError">
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div class="course-desc">暑期名师领学,提高班级教学质量!高效冲分指南</div>
|
||||
<div class="course-desc">{{ course.description || course.title }}</div>
|
||||
<div class="course-stats">
|
||||
<span class="stats-item">
|
||||
<i class="icon-chapters"></i>
|
||||
共9章54节
|
||||
共{{ course.chaptersCount || 0 }}章{{ course.lessonsCount || 0 }}节
|
||||
</span>
|
||||
<span class="stats-item">
|
||||
<i class="icon-duration"></i>
|
||||
12小时43分钟
|
||||
{{ formatDuration(course.duration) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="course-footer">
|
||||
<span class="enrolled-count">324人已报名</span>
|
||||
<button class="btn-enroll-course">去报名</button>
|
||||
<span class="enrolled-count">{{ course.enrolledCount || 0 }}人已报名</span>
|
||||
<button class="btn-enroll-course" @click="handleEnrollCourse(course)">去报名</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-card">
|
||||
<div class="course-cover">
|
||||
<div class="course-image computer-bg">
|
||||
<img src="/images/courses/course-activities2.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div class="course-desc">暑期名师领学,提高班级教学质量!高效冲分指南</div>
|
||||
<div class="course-stats">
|
||||
<span class="stats-item">
|
||||
<i class="icon-chapters"></i>
|
||||
共9章54节
|
||||
</span>
|
||||
<span class="stats-item">
|
||||
<i class="icon-duration"></i>
|
||||
12小时43分钟
|
||||
</span>
|
||||
</div>
|
||||
<div class="course-footer">
|
||||
<span class="enrolled-count">324人已报名</span>
|
||||
<button class="btn-enroll-course">去报名</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-more-courses">
|
||||
<p>暂无更多课程</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1054,63 +1032,35 @@
|
||||
<!-- AI小助手聊天界面 -->
|
||||
<div v-if="aiActiveTab === 'assistant'" class="ai-chat-interface">
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="chat-messages">
|
||||
<!-- AI欢迎消息 -->
|
||||
<div class="message ai-message">
|
||||
<div class="message-avatar">
|
||||
<img src="/images/aiCompanion/AI小助手@2x.png" alt="AI小助手">
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-bubble">
|
||||
<p>您好!我是您的AI学习助手,很高兴为您服务!</p>
|
||||
<p>我可以帮助您:</p>
|
||||
<ul>
|
||||
<li>解答课程相关问题</li>
|
||||
<li>提供学习建议</li>
|
||||
<li>协助完成作业</li>
|
||||
<li>生成学习笔记</li>
|
||||
</ul>
|
||||
<p>请随时向我提问!</p>
|
||||
</div>
|
||||
<div class="message-time">刚刚</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户消息示例 -->
|
||||
<div class="message user-message">
|
||||
<div class="chat-messages" ref="chatMessagesContainer">
|
||||
<!-- 动态聊天消息 -->
|
||||
<div
|
||||
v-for="msg in chatMessages"
|
||||
:key="msg.id"
|
||||
class="message"
|
||||
:class="msg.type === 'ai' ? 'ai-message' : 'user-message'"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80"
|
||||
alt="用户">
|
||||
:src="msg.type === 'ai' ? '/images/aiCompanion/AI小助手@2x.png' : 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80'"
|
||||
:alt="msg.type === 'ai' ? 'AI小助手' : '用户'"
|
||||
>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-bubble">
|
||||
<p>你好,我想了解一下这个课程的主要内容</p>
|
||||
<div v-if="msg.content" class="message-text" v-html="formatMessageContent(msg.content)"></div>
|
||||
<div v-if="msg.isStreaming" class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="message-time">2分钟前</div>
|
||||
</div>
|
||||
<div class="message-time">{{ msg.timestamp }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI回复消息 -->
|
||||
<div class="message ai-message">
|
||||
<div class="message-avatar">
|
||||
<img src="/images/aiCompanion/AI小助手@2x.png" alt="AI小助手">
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-bubble">
|
||||
<p>这个课程主要包含以下几个部分:</p>
|
||||
<ol>
|
||||
<li><strong>DeepSeek大语言模型介绍</strong> - 了解模型的基本原理和特点</li>
|
||||
<li><strong>模型应用场景分析</strong> - 学习如何在实际工作中应用</li>
|
||||
<li><strong>实战应用技巧</strong> - 掌握具体的操作方法和技巧</li>
|
||||
</ol>
|
||||
<p>课程总时长约12小时,包含54节课程内容。您想从哪个部分开始学习呢?</p>
|
||||
</div>
|
||||
<div class="message-time">1分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-suggestion">
|
||||
<!-- 只在没有对话时显示AI建议 -->
|
||||
<div v-if="chatMessages.length === 0" class="ai-suggestion">
|
||||
<div class="ai-suggestion-title">你可以尝试与AI进行以下对话:</div>
|
||||
|
||||
<div class="ai-suggestion-item">
|
||||
@ -1155,12 +1105,24 @@
|
||||
<!-- 聊天输入区域 -->
|
||||
<div class="chat-input-area">
|
||||
<div class="input-container">
|
||||
<textarea type="text" v-model="chatMessage" placeholder="请输入您的问题..." class="chat-input"
|
||||
@keyup.enter="sendMessage">
|
||||
|
||||
</textarea>
|
||||
<button class="send-button" @click="sendMessage">
|
||||
<img src="/images/aiCompanion/发送@2x.png" alt="发送" class="send-icon">
|
||||
<textarea
|
||||
v-model="chatMessage"
|
||||
placeholder="请输入您的问题..."
|
||||
class="chat-input"
|
||||
:disabled="isAISending"
|
||||
@keyup.enter="!isAISending && sendMessage()"
|
||||
></textarea>
|
||||
<button
|
||||
class="send-button"
|
||||
:disabled="isAISending || !chatMessage.trim()"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<img
|
||||
src="/images/aiCompanion/发送@2x.png"
|
||||
alt="发送"
|
||||
class="send-icon"
|
||||
:class="{ 'disabled': isAISending || !chatMessage.trim() }"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1347,6 +1309,7 @@ import { useMessage } from 'naive-ui'
|
||||
// import { useUserStore } from '@/stores/user'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import { CommentApi } from '@/api/modules/comment'
|
||||
import { AIApi } from '@/api/modules/ai'
|
||||
import type { Course, CourseSection, CourseComment } from '@/api/types'
|
||||
import QuillEditor from '@/components/common/QuillEditor.vue'
|
||||
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
|
||||
@ -1428,10 +1391,8 @@ const isRefreshing = ref(false)
|
||||
|
||||
// AI建议相关
|
||||
const sendPrompt = (prompt: string) => {
|
||||
// 这里可以添加发送提示词到AI的逻辑
|
||||
console.log('发送AI提示词:', prompt)
|
||||
// 可以将提示词设置到输入框中
|
||||
// 或者直接发送给AI处理
|
||||
chatMessage.value = prompt
|
||||
sendMessage()
|
||||
}
|
||||
|
||||
// 隐藏区域的函数
|
||||
@ -1593,6 +1554,11 @@ const courseActiveTab = ref('summary')
|
||||
const showTipSection = ref(true)
|
||||
const showAiAssistant = ref(true)
|
||||
|
||||
// 更多课程相关状态
|
||||
const moreCourses = ref<any[]>([])
|
||||
const moreCoursesLoading = ref(false)
|
||||
const moreCoursesError = ref('')
|
||||
|
||||
// 讲师数据
|
||||
// const instructors = ref([
|
||||
// {
|
||||
@ -1856,7 +1822,28 @@ const replyToUsername = ref('')
|
||||
|
||||
// 处理视频播放
|
||||
const handleVideoPlay = async (section: CourseSection) => {
|
||||
console.log('播放视频:', section.name)
|
||||
console.log('🎬 点击视频播放按钮:', section.name)
|
||||
console.log('🔍 当前状态:', {
|
||||
practiceMode: practiceMode.value,
|
||||
discussionMode: discussionMode.value,
|
||||
sectionId: section.id
|
||||
})
|
||||
|
||||
// 如果当前在练习模式或讨论模式,先退出这些模式
|
||||
if (practiceMode.value) {
|
||||
console.log('🔄 退出练习模式,切换到视频播放')
|
||||
exitPractice()
|
||||
}
|
||||
|
||||
if (discussionMode.value) {
|
||||
console.log('🔄 退出讨论模式,切换到视频播放')
|
||||
exitDiscussion()
|
||||
}
|
||||
|
||||
console.log('🔍 模式切换后状态:', {
|
||||
practiceMode: practiceMode.value,
|
||||
discussionMode: discussionMode.value
|
||||
})
|
||||
|
||||
// 加载章节视频数据
|
||||
await loadSectionVideo(section)
|
||||
@ -1905,7 +1892,15 @@ const loadSectionVideo = async (section: CourseSection) => {
|
||||
currentVideoSection.value = section
|
||||
console.log('✅ 设置视频URL:', currentVideoUrl.value)
|
||||
console.log('✅ 可用清晰度:', video.qualities)
|
||||
console.log('✅ 默认清晰度:', video.defaultQuality)
|
||||
console.log('✅ 传递给DPlayer的清晰度:', videoQualities.value)
|
||||
} else {
|
||||
// 如果找不到默认清晰度,使用第一个可用的清晰度
|
||||
if (video.qualities && video.qualities.length > 0) {
|
||||
currentVideoUrl.value = video.qualities[0].url
|
||||
currentVideoSection.value = section
|
||||
console.log('✅ 使用第一个可用清晰度:', video.qualities[0])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 没有找到视频数据')
|
||||
@ -2473,12 +2468,13 @@ const currentPracticeQuestion = computed(() => {
|
||||
|
||||
// 退出练习
|
||||
const exitPractice = () => {
|
||||
console.log('🚪 正在退出练习模式...')
|
||||
practiceMode.value = false
|
||||
practiceStarted.value = false
|
||||
practiceFinished.value = false
|
||||
currentPracticeSection.value = null
|
||||
practiceQuestions.value = []
|
||||
console.log('✅ 已退出练习模式')
|
||||
console.log('✅ 已退出练习模式,practiceMode:', practiceMode.value)
|
||||
}
|
||||
|
||||
|
||||
@ -2742,12 +2738,13 @@ const loadDiscussionData = async (section: CourseSection) => {
|
||||
}
|
||||
|
||||
const exitDiscussion = () => {
|
||||
console.log('🚪 正在退出讨论模式...')
|
||||
discussionMode.value = false
|
||||
currentDiscussionSection.value = null
|
||||
discussionList.value = []
|
||||
newComment.value = ''
|
||||
replyingTo.value = null
|
||||
console.log('✅ 已退出讨论模式')
|
||||
console.log('✅ 已退出讨论模式,discussionMode:', discussionMode.value)
|
||||
}
|
||||
|
||||
const submitDiscussionComment = () => {
|
||||
@ -2797,10 +2794,10 @@ const handleSectionClick = (section: CourseSection) => {
|
||||
name: section.name
|
||||
})
|
||||
|
||||
// 如果是视频课程,加载视频数据
|
||||
// 如果是视频课程,调用视频播放处理方法
|
||||
if (isVideo) {
|
||||
console.log('✅ 识别为视频课程,开始加载视频数据')
|
||||
loadSectionVideo(section)
|
||||
console.log('✅ 识别为视频课程,调用视频播放方法')
|
||||
handleVideoPlay(section)
|
||||
} else if (isResource) {
|
||||
console.log('✅ 识别为资料课程')
|
||||
handleDownload(section)
|
||||
@ -2872,6 +2869,58 @@ const closePreviewModal = () => {
|
||||
previewModalType.value = ''
|
||||
}
|
||||
|
||||
// 加载更多课程
|
||||
const loadMoreCourses = async () => {
|
||||
try {
|
||||
moreCoursesLoading.value = true
|
||||
moreCoursesError.value = ''
|
||||
console.log('🔍 开始加载更多课程,课程ID:', courseId.value)
|
||||
|
||||
const response = await CourseApi.getMoreCourses(courseId.value)
|
||||
console.log('🔍 更多课程API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
moreCourses.value = response.data || []
|
||||
console.log('✅ 更多课程加载成功,数量:', moreCourses.value.length)
|
||||
} else {
|
||||
moreCoursesError.value = response.message || '加载更多课程失败'
|
||||
console.error('❌ 更多课程加载失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载更多课程失败:', error)
|
||||
moreCoursesError.value = '加载更多课程失败,请稍后重试'
|
||||
} finally {
|
||||
moreCoursesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (duration: number): string => {
|
||||
if (!duration || duration <= 0) return '0分钟'
|
||||
|
||||
const hours = Math.floor(duration / 3600)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
} else {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片加载错误
|
||||
const handleImageError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.src = '/images/courses/course-activities1.png'
|
||||
}
|
||||
|
||||
// 处理课程报名
|
||||
const handleEnrollCourse = (course: any) => {
|
||||
console.log('点击报名课程:', course)
|
||||
// 这里可以添加报名逻辑,比如跳转到课程详情页
|
||||
window.open(`/course/${course.id}`, '_blank')
|
||||
}
|
||||
|
||||
// 处理课程报名
|
||||
// const handleEnrollCourse = () => {
|
||||
// if (!userStore.isLoggedIn) {
|
||||
@ -2965,6 +3014,16 @@ const switchTab = (tab: string) => {
|
||||
|
||||
// AI聊天相关状态
|
||||
const chatMessage = ref('')
|
||||
const chatMessages = ref<Array<{
|
||||
id: string
|
||||
type: 'user' | 'ai'
|
||||
content: string
|
||||
timestamp: string
|
||||
isStreaming?: boolean
|
||||
}>>([
|
||||
// 移除初始化欢迎消息,让用户直接开始对话
|
||||
])
|
||||
const isAISending = ref(false)
|
||||
|
||||
// 笔记相关状态
|
||||
const showNoteEditor = ref(false)
|
||||
@ -3106,11 +3165,96 @@ const calculateProgress = () => {
|
||||
}
|
||||
|
||||
// 发送聊天消息
|
||||
const sendMessage = () => {
|
||||
if (chatMessage.value.trim()) {
|
||||
console.log('发送消息:', chatMessage.value)
|
||||
// 这里可以添加发送消息的逻辑
|
||||
const sendMessage = async () => {
|
||||
if (!chatMessage.value.trim() || isAISending.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const userMessage = chatMessage.value.trim()
|
||||
const messageId = Date.now().toString()
|
||||
|
||||
// 添加用户消息
|
||||
chatMessages.value.push({
|
||||
id: messageId,
|
||||
type: 'user',
|
||||
content: userMessage,
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
})
|
||||
|
||||
// 清空输入框
|
||||
chatMessage.value = ''
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom()
|
||||
|
||||
// 添加AI消息占位符
|
||||
const aiMessageId = (Date.now() + 1).toString()
|
||||
chatMessages.value.push({
|
||||
id: aiMessageId,
|
||||
type: 'ai',
|
||||
content: '',
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
isStreaming: true
|
||||
})
|
||||
|
||||
isAISending.value = true
|
||||
|
||||
try {
|
||||
// 调用AI接口 - 流式响应
|
||||
console.log('开始AI流式请求...')
|
||||
await AIApi.sendChatMessageStream(
|
||||
userMessage,
|
||||
// 接收流式消息的回调
|
||||
(chunk: string) => {
|
||||
console.log('收到AI消息块:', chunk)
|
||||
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
|
||||
if (aiMessage) {
|
||||
aiMessage.content += chunk
|
||||
scrollToBottom()
|
||||
}
|
||||
},
|
||||
// 完成回调
|
||||
() => {
|
||||
console.log('AI消息流完成')
|
||||
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
|
||||
if (aiMessage) {
|
||||
aiMessage.isStreaming = false
|
||||
console.log('最终AI消息内容:', aiMessage.content)
|
||||
// 如果没有收到任何内容,显示默认消息
|
||||
if (!aiMessage.content.trim()) {
|
||||
aiMessage.content = '抱歉,我没有收到完整的回复,请重新提问。'
|
||||
console.log('AI回复为空,显示默认消息')
|
||||
}
|
||||
}
|
||||
isAISending.value = false
|
||||
},
|
||||
// 错误回调
|
||||
(error) => {
|
||||
console.error('AI聊天失败:', error)
|
||||
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
|
||||
if (aiMessage) {
|
||||
aiMessage.content = `抱歉,AI助手暂时无法回复:${error.message || '网络连接异常'},请稍后再试。`
|
||||
aiMessage.isStreaming = false
|
||||
}
|
||||
isAISending.value = false
|
||||
message.error(`AI助手暂时无法回复:${error.message || '请检查网络连接'}`)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
const aiMessage = chatMessages.value.find(msg => msg.id === aiMessageId)
|
||||
if (aiMessage) {
|
||||
aiMessage.content = '抱歉,AI助手暂时无法回复,请稍后再试。'
|
||||
aiMessage.isStreaming = false
|
||||
}
|
||||
isAISending.value = false
|
||||
message.error('发送消息失败,请检查网络连接')
|
||||
}
|
||||
}
|
||||
|
||||
@ -3165,6 +3309,36 @@ const deleteNote = (index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天消息容器引用
|
||||
const chatMessagesContainer = ref<HTMLElement>()
|
||||
|
||||
// 格式化消息内容
|
||||
const formatMessageContent = (content: string): string => {
|
||||
if (!content) return ''
|
||||
|
||||
// 将换行符转换为HTML换行
|
||||
let formatted = content.replace(/\n/g, '<br>')
|
||||
|
||||
// 处理列表项(以•开头的行)
|
||||
formatted = formatted.replace(/^• (.+)$/gm, '<li>$1</li>')
|
||||
|
||||
// 如果有列表项,包装在ul标签中
|
||||
if (formatted.includes('<li>')) {
|
||||
formatted = formatted.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
// 滚动到消息底部
|
||||
const scrollToBottom = () => {
|
||||
if (chatMessagesContainer.value) {
|
||||
setTimeout(() => {
|
||||
chatMessagesContainer.value!.scrollTop = chatMessagesContainer.value!.scrollHeight
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送快捷消息
|
||||
// const sendQuickMessage = (message: string) => {
|
||||
// chatMessage.value = message
|
||||
@ -3198,6 +3372,7 @@ onMounted(() => {
|
||||
loadCourseDetail()
|
||||
loadCourseSections()
|
||||
loadCourseComments() // 加载评论
|
||||
loadMoreCourses() // 加载更多课程
|
||||
|
||||
// 检查是否需要刷新
|
||||
const shouldRefresh = sessionStorage.getItem('refreshCourseExchanged')
|
||||
@ -3422,49 +3597,33 @@ onActivated(() => {
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
/* 学期选择器 */
|
||||
.semester-selector {
|
||||
position: relative;
|
||||
/* 学期显示 */
|
||||
.semester-display {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
margin: 20px 0 -3px 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.semester-dropdown {
|
||||
.semester-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #E2F5FF;
|
||||
border: 1px solid #0088D1;
|
||||
border-radius: 4px;
|
||||
padding: 0 40px 0 16px;
|
||||
padding: 0 16px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0088D1;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
line-height: 40px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.semester-dropdown:focus {
|
||||
border-color: #0088D1;
|
||||
box-shadow: 0 0 0 2px rgba(0, 136, 209, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 开课时间信息 */
|
||||
.course-time-info {
|
||||
width: 100%;
|
||||
@ -5705,6 +5864,57 @@ onActivated(() => {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 消息文本样式 */
|
||||
.message-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-text ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.message-text li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 打字指示器样式 */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #0088D1;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -5760,11 +5970,27 @@ onActivated(() => {
|
||||
justify-content: center;
|
||||
border: none;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.send-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.send-icon.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.chat-input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
|
Loading…
x
Reference in New Issue
Block a user