feat: 完善专家支持中心和项目库单元导航功能
- 添加真实的文旅产业Q&A数据到专家支持中心 - 实现项目库到课程直播间的单元导航 - 新增CourseList组件的expandUnitByName方法 - 优化项目详情模态框的单元显示和交互 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -144,12 +144,24 @@
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
> li {
|
||||
border: 1px solid #e5e6eb;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 8px;
|
||||
padding: 5px 12px;
|
||||
background-color: #fff;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f3f5;
|
||||
border-color: #0077ff;
|
||||
color: #0077ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,9 @@ const init = [
|
||||
|
||||
const MyIM = React.forwardRef((props, ref) => {
|
||||
const { hanldeClickOpenModalBtn, initialMessages } = props;
|
||||
const [currentMessages, setCurrentMessages] = useState(init);
|
||||
// 使用ChatUI的消息管理hook
|
||||
const { messages, appendMsg } = useMessages(initialMessages || init);
|
||||
const { messages, appendMsg, setMessages } = useMessages(currentMessages);
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
const id = useRef(undefined);
|
||||
const childRef = useRef();
|
||||
@@ -65,6 +66,59 @@ const MyIM = React.forwardRef((props, ref) => {
|
||||
[studentInfo]
|
||||
);
|
||||
|
||||
// 处理对话内容变化
|
||||
useEffect(() => {
|
||||
if (initialMessages && initialMessages.length > 0) {
|
||||
// 构建新的消息数组
|
||||
const newMessages = [];
|
||||
|
||||
initialMessages.forEach((msg) => {
|
||||
if (msg.type === "user") {
|
||||
newMessages.push({
|
||||
type: "text",
|
||||
content: { text: msg.content },
|
||||
position: "right",
|
||||
user: {
|
||||
avatar: studentInfo?.avatar,
|
||||
name: userAvatarDom,
|
||||
},
|
||||
});
|
||||
} else if (msg.type === "assistant") {
|
||||
const mentorName = msg.mentor ? `${msg.mentor}` : "专家";
|
||||
const assistantAvatarDom = (
|
||||
<div className="user-avatar-wrapper">
|
||||
<span className="user-avatar-name">{mentorName}</span>
|
||||
<div className="user-avatar-tag">专家</div>
|
||||
<span className="user-avatar-time">
|
||||
{dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
newMessages.push({
|
||||
type: "text",
|
||||
content: { text: msg.content },
|
||||
position: "left",
|
||||
user: {
|
||||
avatar: ICONURL,
|
||||
name: assistantAvatarDom,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置新消息
|
||||
if (setMessages) {
|
||||
setMessages(newMessages);
|
||||
} else {
|
||||
// 如果setMessages不存在,重新创建组件实例
|
||||
setCurrentMessages(newMessages);
|
||||
}
|
||||
|
||||
setIsInit(false);
|
||||
}
|
||||
}, [initialMessages, studentInfo, userAvatarDom]);
|
||||
|
||||
// 处理发送消息
|
||||
const handleSend = async (type, val, showBtn = false) => {
|
||||
setIsInit(false);
|
||||
|
||||
@@ -91,6 +91,17 @@
|
||||
background-color: #f4f7f9;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e8ecf0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #e8f0ff;
|
||||
border-left: 3px solid #2c7aff;
|
||||
}
|
||||
|
||||
> P {
|
||||
width: 70%;
|
||||
@@ -101,6 +112,9 @@
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.support-list-content-item-status {
|
||||
width: 52px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import expertSupportData from "@/data/expertSupportData";
|
||||
import "./index.css";
|
||||
|
||||
const STATUS = {
|
||||
@@ -8,11 +9,25 @@ const STATUS = {
|
||||
finish: { key: "finish", text: "已解决" },
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const Index = ({ onSelectConversation }) => {
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
|
||||
const handleClickNew = () => {
|
||||
console.log("点击了新对话");
|
||||
};
|
||||
|
||||
const handleSelectConversation = (conversation) => {
|
||||
setSelectedId(conversation.id);
|
||||
if (onSelectConversation) {
|
||||
onSelectConversation(conversation);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据日期分组对话
|
||||
const todayConversations = expertSupportData.conversations.filter(c => c.date === "今天");
|
||||
const weekConversations = expertSupportData.conversations.filter(c => c.date === "7天内");
|
||||
const monthConversations = expertSupportData.conversations.filter(c => c.date === "30天内");
|
||||
|
||||
return (
|
||||
<div className="support-list-wrapper">
|
||||
<div className="support-list-title">
|
||||
@@ -26,114 +41,66 @@ const Index = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="support-list">
|
||||
<>
|
||||
<p className="support-list-date">今天</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-waiting">
|
||||
{STATUS.waiting.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-processing">
|
||||
{STATUS.processing.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
<>
|
||||
<p className="support-list-date">7天内</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
<>
|
||||
<p className="support-list-date">30天内</p>
|
||||
<ul className="support-list-content">
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
<li className="support-list-content-item">
|
||||
<p>这里是对话名称</p>
|
||||
<div className="support-list-content-item-status status-finish">
|
||||
{STATUS.finish.text}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
{todayConversations.length > 0 && (
|
||||
<>
|
||||
<p className="support-list-date">今天</p>
|
||||
<ul className="support-list-content">
|
||||
{todayConversations.map(conversation => (
|
||||
<li
|
||||
key={conversation.id}
|
||||
className={`support-list-content-item ${selectedId === conversation.id ? 'selected' : ''}`}
|
||||
onClick={() => handleSelectConversation(conversation)}
|
||||
>
|
||||
<p>{conversation.title}</p>
|
||||
<div className={`support-list-content-item-status status-${conversation.status}`}>
|
||||
{STATUS[conversation.status].text}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{weekConversations.length > 0 && (
|
||||
<>
|
||||
<p className="support-list-date">7天内</p>
|
||||
<ul className="support-list-content">
|
||||
{weekConversations.map(conversation => (
|
||||
<li
|
||||
key={conversation.id}
|
||||
className={`support-list-content-item ${selectedId === conversation.id ? 'selected' : ''}`}
|
||||
onClick={() => handleSelectConversation(conversation)}
|
||||
>
|
||||
<p>{conversation.title}</p>
|
||||
<div className={`support-list-content-item-status status-${conversation.status}`}>
|
||||
{STATUS[conversation.status].text}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{monthConversations.length > 0 && (
|
||||
<>
|
||||
<p className="support-list-date">30天内</p>
|
||||
<ul className="support-list-content">
|
||||
{monthConversations.map(conversation => (
|
||||
<li
|
||||
key={conversation.id}
|
||||
className={`support-list-content-item ${selectedId === conversation.id ? 'selected' : ''}`}
|
||||
onClick={() => handleSelectConversation(conversation)}
|
||||
>
|
||||
<p>{conversation.title}</p>
|
||||
<div className={`support-list-content-item-status status-${conversation.status}`}>
|
||||
{STATUS[conversation.status].text}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default Index;
|
||||
@@ -24,6 +24,7 @@ const ExpertSupportPage = () => {
|
||||
const IMRef = useRef(null);
|
||||
const [formVisible, setFormVisible] = useState(false);
|
||||
const [initialMessages, setInitialMessages] = useState(null); // 设置消息
|
||||
const [selectedConversation, setSelectedConversation] = useState(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setFormVisible(false);
|
||||
@@ -33,6 +34,12 @@ const ExpertSupportPage = () => {
|
||||
setFormVisible(true);
|
||||
};
|
||||
|
||||
// 处理对话选择
|
||||
const handleSelectConversation = (conversation) => {
|
||||
setSelectedConversation(conversation);
|
||||
setInitialMessages(conversation.messages);
|
||||
};
|
||||
|
||||
// 调用子组件的方法
|
||||
const handleSend = (type, val, showBtn) => {
|
||||
if (IMRef.current) {
|
||||
@@ -44,7 +51,7 @@ const ExpertSupportPage = () => {
|
||||
<>
|
||||
<div className="expert-support-page">
|
||||
<div className="expert-support-left-wrapper">
|
||||
<SupportList />
|
||||
<SupportList onSelectConversation={handleSelectConversation} />
|
||||
</div>
|
||||
<div className="expert-support-right-wrapper">
|
||||
<div className="expert-support-right-title-wrapper">
|
||||
|
||||
@@ -10,19 +10,27 @@ const LivePage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const courseListRef = useRef(null);
|
||||
|
||||
// 检查URL参数,如果有courseId或courseTitle则自动打开对应课程
|
||||
// 检查URL参数,如果有courseId或courseTitle则自动打开对应课程或单元
|
||||
useEffect(() => {
|
||||
const courseId = searchParams.get('courseId');
|
||||
const courseTitle = searchParams.get('courseTitle');
|
||||
const courseType = searchParams.get('courseType');
|
||||
|
||||
console.log('LivePage - URL params:', { courseId, courseTitle });
|
||||
console.log('LivePage - URL params:', { courseId, courseTitle, courseType });
|
||||
|
||||
if (courseId || courseTitle) {
|
||||
// 需要给组件时间加载数据
|
||||
const timer = setTimeout(() => {
|
||||
if (courseListRef.current) {
|
||||
console.log('LivePage - Calling selectCourse via ref');
|
||||
courseListRef.current.selectCourse(courseId, courseTitle);
|
||||
// 如果courseTitle是单元名称(从项目库跳转过来),则展开对应的单元
|
||||
if (courseType && (courseType === 'compound-skill' || courseType === 'vertical-skill')) {
|
||||
console.log('LivePage - Calling expandUnitByName for unit:', courseTitle);
|
||||
courseListRef.current.expandUnitByName(courseTitle);
|
||||
} else {
|
||||
// 否则按原来的逻辑选择课程
|
||||
console.log('LivePage - Calling selectCourse via ref');
|
||||
courseListRef.current.selectCourse(courseId, courseTitle);
|
||||
}
|
||||
}
|
||||
}, 500); // 等待数据加载
|
||||
|
||||
|
||||
@@ -510,3 +510,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 可点击单元样式 */
|
||||
.clickable-unit {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.clickable-unit:hover {
|
||||
background-color: #e8f4ff !important;
|
||||
border-color: #4080ff !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 128, 255, 0.15);
|
||||
}
|
||||
|
||||
.clickable-unit:active {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import PDFICON from "@/assets/images/Common/pdf_icon.png";
|
||||
import FileIcon from "@/components/FileIcon";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import ImagePreviewModal from "../ImagePreviewModal";
|
||||
import { getCompoundUnits, getVerticalUnits } from "@/data/projectUnitsMapping";
|
||||
import "./index.css";
|
||||
|
||||
export default ({ visible, onClose, data }) => {
|
||||
@@ -30,6 +31,30 @@ export default ({ visible, onClose, data }) => {
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 处理单元点击跳转到课程直播间
|
||||
const handleUnitClick = (unitName, isCompound) => {
|
||||
console.log('Unit clicked:', unitName, 'isCompound:', isCompound);
|
||||
|
||||
// 关闭当前模态框
|
||||
onClose();
|
||||
|
||||
// 构建URL参数
|
||||
const params = new URLSearchParams();
|
||||
params.append('courseTitle', unitName);
|
||||
|
||||
// 根据单元类型设置课程类型
|
||||
if (isCompound) {
|
||||
params.append('courseType', 'compound-skill');
|
||||
} else {
|
||||
params.append('courseType', 'vertical-skill');
|
||||
}
|
||||
|
||||
console.log('Navigate to live with params:', params.toString());
|
||||
|
||||
// 跳转到课程直播间
|
||||
navigate(`/live?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 将换行符转换为Markdown格式
|
||||
const formatMarkdownContent = (content) => {
|
||||
if (!content) return "";
|
||||
@@ -37,6 +62,7 @@ export default ({ visible, onClose, data }) => {
|
||||
return content.replace(/\\n/g, '\n');
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="project-cases-modal">
|
||||
@@ -128,25 +154,20 @@ export default ({ visible, onClose, data }) => {
|
||||
<div className="unit-category-section">
|
||||
<p className="unit-category-subtitle">复合能力课</p>
|
||||
<ul className="project-cases-modal-horizontal-list">
|
||||
{data?.units?.filter(unit =>
|
||||
unit.includes('商业活动') ||
|
||||
unit.includes('营销') ||
|
||||
unit.includes('品牌') ||
|
||||
unit.includes('项目全周期')
|
||||
).map((unit, index) => (
|
||||
<li key={`compound-${index}`} className="class-list-item">
|
||||
{getCompoundUnits(data?.title).map((unit, index) => (
|
||||
<li
|
||||
key={`compound-${index}`}
|
||||
className="class-list-item clickable-unit"
|
||||
onClick={() => handleUnitClick(unit, true)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>{unit}</span>
|
||||
</div>
|
||||
</li>
|
||||
)) || null}
|
||||
{(!data?.units || data?.units?.filter(unit =>
|
||||
unit.includes('商业活动') ||
|
||||
unit.includes('营销') ||
|
||||
unit.includes('品牌') ||
|
||||
unit.includes('项目全周期')
|
||||
).length === 0) && (
|
||||
))}
|
||||
{getCompoundUnits(data?.title).length === 0 && (
|
||||
<li className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
@@ -161,25 +182,20 @@ export default ({ visible, onClose, data }) => {
|
||||
<div className="unit-category-section">
|
||||
<p className="unit-category-subtitle">垂直能力课</p>
|
||||
<ul className="project-cases-modal-horizontal-list">
|
||||
{data?.units?.filter(unit =>
|
||||
!unit.includes('商业活动') &&
|
||||
!unit.includes('营销') &&
|
||||
!unit.includes('品牌') &&
|
||||
!unit.includes('项目全周期')
|
||||
).map((unit, index) => (
|
||||
<li key={`vertical-${index}`} className="class-list-item">
|
||||
{getVerticalUnits(data?.title).map((unit, index) => (
|
||||
<li
|
||||
key={`vertical-${index}`}
|
||||
className="class-list-item clickable-unit"
|
||||
onClick={() => handleUnitClick(unit, false)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>{unit}</span>
|
||||
</div>
|
||||
</li>
|
||||
)) || null}
|
||||
{(!data?.units || data?.units?.filter(unit =>
|
||||
!unit.includes('商业活动') &&
|
||||
!unit.includes('营销') &&
|
||||
!unit.includes('品牌') &&
|
||||
!unit.includes('项目全周期')
|
||||
).length === 0) && (
|
||||
))}
|
||||
{getVerticalUnits(data?.title).length === 0 && (
|
||||
<li className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
|
||||
Reference in New Issue
Block a user