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:
KQL
2025-09-12 19:52:36 +08:00
parent 0e7f98d3fc
commit 4738a8545c
11 changed files with 938 additions and 144 deletions

View File

@@ -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;
}
}
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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">

View File

@@ -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); // 等待数据加载

View File

@@ -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);
}

View File

@@ -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 />