feat: 添加HR访问量弹窗和日历事项样式优化

- 新增HR访问详情弹窗组件,支持左右切换查看不同HR信息
- 优化日历事项样式系统,基于事件类型匹配样式配置
- 完善侧边栏HR访问量组件,添加重叠头像和点击交互
- 增加班级排名弹窗组件
- 更新专家支持页面布局和样式

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-11 18:40:40 +08:00
parent c969677ef6
commit 4f64941d85
32 changed files with 7335 additions and 1722 deletions

View File

@@ -0,0 +1,65 @@
.form-modal-content {
width: 536px;
height: 542px;
border-radius: 16px;
background-color: #fff;
box-sizing: border-box;
padding: 20px;
position: relative;
.close-icon {
position: absolute;
right: 20px;
top: 20px;
content: "";
width: 24px;
height: 24px;
background-image: url("@/assets/images/Common/close.png");
background-size: 100% 100%;
z-index: 1;
cursor: pointer;
}
&::after {
position: absolute;
right: 0;
top: 0;
content: "";
width: 438px;
height: 138px;
background-image: url("@/assets/images/ExpertSupportPage/modal_bg.png");
background-size: 100% 100%;
}
.form-modal-title {
width: 100%;
height: 138px;
box-sizing: border-box;
padding-top: 20px;
.title {
height: 30px;
line-height: 30px;
font-size: 20px;
color: #1d2129;
font-weight: 800;
margin-bottom: 10px;
}
.sub-title {
line-height: 22px;
font-size: 14px;
color: #4e5969;
font-weight: 400;
}
}
.arco-form-item-control-children {
display: flex;
justify-content: center;
align-items: center;
.submit-btn {
width: 320px;
height: 36px;
background-color: #0077ff;
color: #fff;
}
}
}

View File

@@ -0,0 +1,98 @@
import { useRef } from "react";
import { Form, Input, Select, Button } from "@arco-design/web-react";
import Modal from "@/components/Modal";
import "./index.css";
const FormItem = Form.Item;
const TextArea = Input.TextArea;
export default ({ visible, onClose, handleSend }) => {
const formRef = useRef();
const handleSubmit = async (values) => {
console.log(values);
handleSend(
"system",
"正在为您自动匹配适合的专家2小时内将给您答复请耐心等待...",
false
);
onClose();
};
return (
<Modal visible={visible} onClose={onClose}>
<div className="form-modal-content">
<i className="close-icon" onClick={onClose} />
<div className="form-modal-title">
<p className="title">请填写表单</p>
<p className="sub-title">
这有助于更快地为你匹配专家以及 <br />
解答疑问哦
</p>
</div>
<Form
ref={formRef}
autoComplete="off"
layout="vertical"
scrollToFirstError
onSubmit={handleSubmit}
>
<FormItem label="问题类型" field="post" rules={[{ required: true }]}>
<Select
placeholder="请选择问题类型"
options={[
{
label: "one",
value: 0,
},
{
label: "two",
value: 1,
},
{
label: "three",
value: 2,
},
]}
allowClear
/>
</FormItem>
<FormItem
label="具体问题描述"
field="name"
rules={[{ required: true }]}
>
<TextArea
placeholder="请输入您的问题描述"
autoSize={{ minRows: 4, maxRows: 7 }}
/>
</FormItem>
<FormItem label="紧急程度" field="status">
<Select
placeholder="请选择紧急程度"
options={[
{
label: "one",
value: 0,
},
{
label: "two",
value: 1,
},
{
label: "three",
value: 2,
},
]}
allowClear
/>
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit" className="submit-btn">
提交
</Button>
</FormItem>
</Form>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,167 @@
.my-im-content {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
}
/* ChatUI 容器样式 */
.my-im-content .chatui-chat {
height: 100%;
display: flex;
flex-direction: column;
}
/* 消息容器样式 */
.my-im-content .chatui-message-container {
flex: 1;
overflow-y: auto;
}
.ChatApp {
.user-avatar-wrapper {
height: 22px;
display: flex;
align-items: center;
.user-avatar-name {
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: #1d2129;
}
.user-avatar-tag {
width: 52px;
height: 20px;
background-color: rgba(0, 119, 255, 0.1);
color: #0077ff;
text-align: center;
border-radius: 4px;
margin-left: 5px;
}
.user-avatar-time {
margin-left: 5px;
color: #86909c;
font-size: 12px;
font-weight: 400;
}
}
.SystemMessage {
.SystemMessage-inner {
background-color: #e5f1ff;
.system-message-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 10px 5px;
> p {
display: flex;
justify-content: center;
align-items: center;
> span {
font-size: 14px;
font-weight: 400;
color: #1d2129;
}
.system-message-icon {
width: 24px;
height: 24px;
margin-right: 5px;
}
}
.system-message-btn {
width: 72px;
height: 24px;
background-color: #0077ff;
line-height: 24px;
text-align: center;
border-radius: 4px;
margin-top: 20px;
color: #fff;
}
}
}
}
.right {
.user-avatar-wrapper {
justify-content: flex-end;
.user-avatar-time {
margin-right: 5px;
}
}
.content-item {
.content-item-text {
color: #fff;
font-weight: 600 !important;
}
}
.content-item-tip,
.question-tags-title,
.question-tags {
display: none;
}
}
.left {
.user-avatar-wrapper {
justify-content: flex-start;
}
.Message-author {
line-height: 22px;
font-size: 14px;
font-weight: 600;
}
.content-item {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-direction: column;
.content-item-text {
font-size: 14px;
font-weight: 400;
}
.content-item-tip {
color: #86909c;
> span {
font-size: 14px;
font-weight: 400;
color: var(--color-text-1);
}
}
.question-tags-title {
width: 100%;
height: 22px;
line-height: 22px;
color: #1d2129;
font-size: 14px;
font-weight: 400;
margin: 20px 0;
}
.question-tags {
display: flex;
justify-content: flex-start;
align-items: center;
> li {
border: 1px solid #e5e6eb;
box-sizing: border-box;
padding: 3px 8px;
background-color: #fff;
margin-right: 5px;
}
}
}
}
}
/* 自定义样式预留区域 - 用户可以在这里添加自定义样式 */
:root {
--app-bg: #fff;
--footer-bg: #fff;
--color-fill-1: #f4f7f9;
--color-text-1: #1d2129;
--brand-3: #0077ff;
--btn-primary-bg: #0077ff;
}

View File

@@ -0,0 +1,191 @@
import React, { useState, useEffect, useRef, useImperativeHandle } from "react";
import Chat, { Bubble, useMessages, SystemMessage } from "@chatui/core";
import { useSelector } from "react-redux";
import dayjs from "dayjs";
import IconFont from "@/components/IconFont";
import "@chatui/core/dist/index.css";
import "./index.css";
import { useMemo } from "react";
const ICONURL =
"https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuWmDuekBTlr.png";
const questionTags = [
{ text: "专业知识", type: "text", val: "专业知识" },
{ text: "求职策略", type: "text", val: "求职策略" },
{ text: "面试模拟", type: "text", val: "面试模拟" },
{ text: "企业内推岗位", type: "text", val: "企业内推岗位" },
{
text: "转专家服务",
type: "system",
val: "进入专家服务前,请先填写询前表单,这有助于更快地为你匹配专家以及解答疑问哦。",
},
];
const RobotAvatarDom = (
<div className="user-avatar-wrapper">
<span className="user-avatar-name">多多畅职专家服务台</span>
<div className="user-avatar-tag">机器人</div>
<span className="user-avatar-time">
{dayjs().format("YYYY-MM-DD HH:mm")}
</span>
</div>
);
const init = [
{
type: "text",
content: { text: "您好!我是您的专属专家顾问,有什么可以帮助您的吗?" },
position: "left",
user: {
avatar: ICONURL,
name: RobotAvatarDom,
},
},
];
const MyIM = React.forwardRef((props, ref) => {
const { hanldeClickOpenModalBtn, initialMessages } = props;
// 使用ChatUI的消息管理hook
const { messages, appendMsg } = useMessages(initialMessages || init);
const [isInit, setIsInit] = useState(true);
const id = useRef(undefined);
const childRef = useRef();
const studentInfo = useSelector((state) => state.student.studentInfo);
const userAvatarDom = useMemo(
() => (
<div className="user-avatar-wrapper">
<span className="user-avatar-time">
{dayjs().format("YYYY-MM-DD HH:mm")}
</span>
<span className="user-avatar-name">{studentInfo?.realName}</span>
</div>
),
[studentInfo]
);
// 处理发送消息
const handleSend = async (type, val, showBtn = false) => {
setIsInit(false);
switch (type) {
case "text":
// 添加用户消息
appendMsg({
type,
content: { text: val.trim() },
position: "right",
user: {
avatar: studentInfo?.avatar,
name: userAvatarDom,
},
});
// todo
appendMsg({
type: "text",
content: { text: "收到您的消息,专家正在回复中..." },
position: "left",
user: {
avatar: ICONURL,
name: RobotAvatarDom,
},
});
break;
case "system":
appendMsg({
type,
content: {
text: (
<div className="system-message-wrapper">
<p>
{!showBtn && (
<IconFont
className="system-message-icon"
src="recuWi8Aq62JUo"
/>
)}
<span style={{ color: !showBtn ? "#0077FF" : "#1D2129" }}>
{val.trim()}
</span>
</p>
{showBtn && (
<div
className="system-message-btn"
onClick={hanldeClickOpenModalBtn}
>
填写表单
</div>
)}
</div>
),
},
});
break;
case "image":
break;
case "file":
break;
default:
break;
}
};
// 自定义消息渲染器
const renderMessageContent = (MessageProps) => {
const { content, _id, type } = MessageProps;
if (isInit) {
id.current = _id;
}
return (
<Bubble>
<div className="content-item">
<span className="content-item-text">{content.text}</span>
{id.current === _id && (
<>
<div className="content-item-tip">
机器人在线时间:<span>7*24</span>
</div>
<div className="content-item-tip">
专家在线时间:<span>工作日13:00-20:00UTC+8</span>
</div>
<div className="question-tags-title">
请选择分类查看常见问题
</div>
<ul className="question-tags">
{questionTags.map((item, index) => (
<li
key={index}
onClick={() =>
handleSend(item.type, item.val, item.type === "system")
}
>
{item.text}
</li>
))}
</ul>
</>
)}
</div>
</Bubble>
);
};
useImperativeHandle(ref, () => ({
handleSend,
}));
return (
<div className="my-im-content">
<Chat
messages={messages}
renderMessageContent={renderMessageContent}
onSend={handleSend}
locale="zh-CN"
placeholder="发送消息Shift + Enter换行"
/>
</div>
);
});
export default MyIM;

View File

@@ -0,0 +1,135 @@
.support-list-wrapper {
height: 100%;
width: 100%;
position: relative;
background-color: #fff;
border-radius: 16px;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
box-sizing: border-box;
padding-bottom: 20px;
.support-list-title {
width: 100%;
height: 60px;
background-image: url("@/assets/images/ExpertSupportPage/list_bg.png");
background-size: 100% 100%;
box-sizing: border-box;
padding: 0 20px;
color: #1d2129;
font-size: 20px;
font-weight: 700;
.support-list-title-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
border-bottom: 1px solid #e4ecf2;
position: relative;
.support-list-title-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
.support-list-title-new-btn {
width: 80px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
border-radius: 4px;
background-color: #0077ff;
cursor: pointer;
> span {
font-size: 14px;
font-weight: 600;
color: #fff;
margin: 0 2px;
}
}
}
}
.support-list {
width: 100%;
overflow-y: auto;
box-sizing: border-box;
padding: 0 20px;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
.support-list-date {
width: 100%;
height: 20px;
line-height: 20px;
color: #86909c;
font-size: 12px;
font-weight: 400;
margin-top: 20px;
}
.support-list-content {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
.support-list-content-item {
width: 100%;
height: 44px;
border-radius: 4px;
background-color: #f4f7f9;
margin-top: 5px;
position: relative;
> P {
width: 70%;
height: 100%;
line-height: 44px;
box-sizing: border-box;
padding-left: 20px;
color: #1d2129;
font-size: 14px;
font-weight: 600;
}
.support-list-content-item-status {
width: 52px;
height: 20px;
border-radius: 4px;
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: 400;
text-align: center;
box-sizing: border-box;
}
.status-finish {
color: #86909c;
background-color: #ffffff;
}
.status-waiting {
color: #ff3f43;
background-color: #ffe0e1;
border: 1px solid #ff3f43;
}
.status-processing {
color: #ff6600;
background-color: #ffedda;
border: 1px solid #ff6600;
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
import React from "react";
import IconFont from "@/components/IconFont";
import "./index.css";
const STATUS = {
waiting: { key: "waiting", text: "待分配" },
processing: { key: "processing", text: "进行中" },
finish: { key: "finish", text: "已解决" },
};
const Index = () => {
const handleClickNew = () => {
console.log("点击了新对话");
};
return (
<div className="support-list-wrapper">
<div className="support-list-title">
<div className="support-list-title-content">
<IconFont className="support-list-title-icon" src="recuUY5vFY3hVP" />
<span>咨询对话</span>
<div className="support-list-title-new-btn" onClick={handleClickNew}>
<span>+</span>
<span>新对话</span>
</div>
</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>
</>
</div>
</div>
);
};
export default Index;

File diff suppressed because it is too large Load Diff

View File

@@ -1,543 +1,85 @@
import React, { useState, useEffect, useRef } from "react";
import { expertQAData, expertInfo } from "@/data/expertQAData";
import { useState, useRef } from "react";
import IconFont from "@/components/IconFont";
import SupportList from "./components/SupportList";
import MyIM from "./components/MyIM";
import FormModal from "./components/FormModal";
import "./index.css";
const titleList = [
{
title: "专家客服",
icon: "recuWi8z90DYHn",
},
{
title: "快速响应",
icon: "recuWi8zBJn42J",
},
{
title: "24小时服务",
icon: "recuWi8A1CNtCo",
},
];
const ExpertSupportPage = () => {
// 对话管理
const [selectedConversation, setSelectedConversation] = useState(null);
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState("");
const IMRef = useRef(null);
const [formVisible, setFormVisible] = useState(false);
const [initialMessages, setInitialMessages] = useState(null); // 设置消息
const [showResolutionModal, setShowResolutionModal] = useState(null);
const [isNewConversation, setIsNewConversation] = useState(false);
// 用于跟踪最后活动时间
// 初始对话数据 - 整合文旅产业问答内容
const [conversationGroups, setConversationGroups] = useState({
今天: [], // 清空今天板块的对话记录
"7天内": [
// 导入文旅产业问答数据中的最近记录
...expertQAData.slice(0, 3).map(qa => ({
...qa,
lastActivityTime: Date.now() - (Math.floor(Math.random() * 6) + 1) * 24 * 60 * 60 * 1000
})),
{
id: 7,
title: "文创产品开发咨询",
lastMessage: "如何设计有文化内涵的旅游纪念品",
time: "01-03",
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastActivityTime: Date.now() - 5 * 24 * 60 * 60 * 1000,
messages: []
},
],
"30天内": [
// 导入文旅产业问答数据中的其余记录
...expertQAData.slice(3, 6).map(qa => ({
...qa,
lastActivityTime: Date.now() - (Math.floor(Math.random() * 10) + 15) * 24 * 60 * 60 * 1000
})),
{
id: 11,
title: "智慧旅游技术应用",
lastMessage: "如何利用数字技术提升游客体验",
time: "12-12",
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastActivityTime: Date.now() - 26 * 24 * 60 * 60 * 1000,
messages: []
},
],
});
// 当前活跃对话的消息记录
const conversationMessages = [
{
id: 1,
type: "user",
content: "您好,我遇到了一些问题",
time: "2025-01-08 16:45",
avatar: "👤",
},
{
id: 2,
type: "expert",
content:
"你好我是多多ai智能问答小助手有什么问题就来问我吧",
time: "2025-01-08 16:46",
avatar: "👨‍💻",
expertName: "多多机器人",
},
{
id: 3,
type: "user",
content: "我需要了解更多关于泛型的使用,特别是在复杂数据结构中的应用",
time: "2025-01-08 16:48",
avatar: "👤",
},
];
// 创建新对话
const createNewConversation = () => {
const newConversation = {
id: Date.now(),
title: "新的问题咨询",
lastMessage: "",
time: new Date().toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
}),
status: "进行中",
statusType: "in-progress",
hasInactivityAlert: false,
lastActivityTime: Date.now(),
};
setConversationGroups((prev) => ({
...prev,
今天: [newConversation, ...prev["今天"]],
}));
setSelectedConversation(newConversation);
setMessages([]);
setIsNewConversation(true);
const handleClose = () => {
setFormVisible(false);
};
// 检查非活跃状态
const checkInactivityStatus = () => {
const currentTime = Date.now();
const INACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10分钟
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.statusType === "in-progress") {
const timeSinceLastActivity =
currentTime - conversation.lastActivityTime;
const shouldShowAlert =
timeSinceLastActivity >= INACTIVITY_THRESHOLD;
if (shouldShowAlert !== conversation.hasInactivityAlert) {
return { ...conversation, hasInactivityAlert: shouldShowAlert };
}
}
return conversation;
});
});
return updated;
});
const hanldeClickOpenModal = () => {
setFormVisible(true);
};
// 处理对话选择
const handleConversationSelect = (conversation) => {
// 如果是带有红点的对话,点击时询问是否已解决
if (
conversation.hasInactivityAlert &&
conversation.statusType === "in-progress"
) {
setShowResolutionModal(conversation);
} else {
setSelectedConversation(conversation);
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
if (conversation.messages && conversation.messages.length > 0) {
setMessages(conversation.messages);
} else {
setMessages(conversationMessages);
}
setIsNewConversation(false);
}
};
// 标记问题为已解决
const markAsResolved = (conversationId) => {
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === conversationId) {
return {
...conversation,
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastMessage: "问题已解决",
};
}
return conversation;
});
});
return updated;
});
setShowResolutionModal(null);
// 如果当前选中的是这个对话,取消选择
if (selectedConversation?.id === conversationId) {
setSelectedConversation(null);
setMessages([]);
}
};
// 继续对话
const continueConversation = (conversation) => {
// 更新最后活动时间
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conv) => {
if (conv.id === conversation.id) {
return {
...conv,
hasInactivityAlert: false,
lastActivityTime: Date.now(),
};
}
return conv;
});
});
return updated;
});
setSelectedConversation(conversation);
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
if (conversation.messages && conversation.messages.length > 0) {
setMessages(conversation.messages);
} else {
setMessages(conversationMessages);
}
setShowResolutionModal(null);
setIsNewConversation(false);
};
// 发送消息
const handleSendMessage = () => {
if (!inputMessage.trim()) return;
const newMessage = {
id: messages.length + 1,
type: "user",
content: inputMessage,
time: new Date().toLocaleString("zh-CN"),
avatar: "👤",
};
setMessages([...messages, newMessage]);
setInputMessage("");
// 更新当前对话的标题和最后消息(如果是新对话)
if (selectedConversation && isNewConversation && messages.length === 0) {
const title =
inputMessage.length > 20
? inputMessage.substring(0, 20) + "..."
: inputMessage;
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === selectedConversation.id) {
return {
...conversation,
title: title,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
};
}
return conversation;
});
});
return updated;
});
setSelectedConversation((prev) => ({
...prev,
title: title,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
}));
setIsNewConversation(false);
} else if (selectedConversation) {
// 更新现有对话的最后活动时间
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === selectedConversation.id) {
return {
...conversation,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
hasInactivityAlert: false,
};
}
return conversation;
});
});
return updated;
});
}
};
// 组件生命周期
useEffect(() => {
// 页面加载时自动创建新对话
createNewConversation();
// 定时检查非活跃状态
const interval = setInterval(checkInactivityStatus, 30000); // 每30秒检查一次
return () => clearInterval(interval);
}, []);
// 组件挂载后进行初始检查
useEffect(() => {
const timeoutId = setTimeout(checkInactivityStatus, 1000);
return () => clearTimeout(timeoutId);
}, []);
// 转专家服务
const handleTransferToExpert = () => {
if (
selectedConversation &&
selectedConversation.statusType === "in-progress"
) {
// 更新对话状态逻辑
alert("已转交人工专家,将有专业老师为您服务");
// 调用子组件的方法
const handleSend = (type, val, showBtn) => {
if (IMRef.current) {
IMRef.current.handleSend(type, val, showBtn);
}
};
return (
<div className="expert-support-page">
{/* 左侧对话记录区域 */}
<div className="conversation-sidebar">
<div className="sidebar-header">
<h2 className="expert-support-sidebar-title">对话记录</h2>
<div className="new-conversation-btn-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
<button
className="new-conversation-btn disabled"
onClick={(e) => e.preventDefault()}
disabled
style={{
cursor: 'not-allowed',
opacity: 0.6,
backgroundColor: '#f5f5f5'
}}
>
<span className="btn-icon">💬</span>
<span className="btn-text">新建对话</span>
</button>
<div className="hover-tooltip" style={{
position: 'absolute',
top: '-35px',
left: '50%',
transform: 'translateX(-50%)',
padding: '6px 12px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
borderRadius: '4px',
fontSize: '12px',
whiteSpace: 'nowrap',
display: 'none',
zIndex: 1000
}}>
非学员无权限
</div>
</div>
<>
<div className="expert-support-page">
<div className="expert-support-left-wrapper">
<SupportList />
</div>
{/* 按时间分组的对话列表 */}
<div className="conversation-groups">
{Object.entries(conversationGroups).map(
([groupName, conversations]) => (
<div key={groupName} className="conversation-group">
<div className="group-header">
<h3 className="group-title">{groupName}</h3>
</div>
<div className="conversation-list">
{conversations.map((conversation) => (
<div
key={conversation.id}
className={`conversation-item ${
selectedConversation?.id === conversation.id
? "selected"
: ""
} ${conversation.hasInactivityAlert ? "has-alert" : ""}`}
onClick={() => handleConversationSelect(conversation)}
>
<div className="conversation-content">
<div className="conversation-title-row">
<h4 className="conversation-title">
{conversation.title}
</h4>
{conversation.hasInactivityAlert && (
<div className="inactivity-alert">
<span className="alert-dot"></span>
</div>
)}
</div>
<p className="conversation-preview">
{conversation.lastMessage}
</p>
</div>
<div className="conversation-meta">
<span className="conversation-time">
{conversation.time}
</span>
<span
className={`conversation-status status-${conversation.statusType}`}
>
{conversation.status}
</span>
</div>
</div>
))}
</div>
<div className="expert-support-right-wrapper">
<div className="expert-support-right-title-wrapper">
<div className="title-wrapper">
<IconFont className="right-title-icon" src="recuWi8ARUkXzm" />
<div className="right-title-text">
<p>专家支持中心</p>
<span>Expert Tutor Support Center</span>
</div>
)
)}
</div>
<ul>
{titleList.map((item) => (
<li key={item.title}>
<IconFont src={item.icon} className="item-icon" />
<span>{item.title}</span>
</li>
))}
</ul>
</div>
<div className="expert-support-im-wrapper">
<MyIM
ref={IMRef}
initialMessages={initialMessages}
hanldeClickOpenModalBtn={hanldeClickOpenModal}
/>
</div>
</div>
</div>
{/* 右侧问答界面 */}
<div className="chat-interface">
{/* 顶部信息栏 */}
<div className="chat-header">
<div className="header-info">
<div className="service-badge">
<span className="badge-icon">🎓</span>
<span className="badge-text">专家客服</span>
</div>
<div className="service-title">学有所问向必有答</div>
<div className="service-subtitle">
我们有创新学习习惯养成方案
</div>
</div>
<div className="service-actions">
<div className="service-tag">专家答疑</div>
<div className="service-tag">快速响应</div>
<div className="service-tag">24小时服务</div>
</div>
</div>
{/* 对话内容区域 */}
<div className="chat-content">
{selectedConversation ? (
<>
<div className="chat-title-bar">
<h3 className="active-conversation-title">
{selectedConversation.title}
</h3>
{selectedConversation.statusType === "in-progress" && (
<button
className="transfer-expert-btn"
onClick={handleTransferToExpert}
>
转专家服务
</button>
)}
</div>
<div className="messages-container">
{messages.map((message) => (
<div
key={message.id}
className={`message ${message.type}-message`}
>
<div className="message-avatar">{message.avatar}</div>
<div className="message-bubble">
{message.type === "expert" && (
<div className="expert-name">{message.expertName}</div>
)}
<div className="message-text">{message.content}</div>
<div className="message-time">{message.time}</div>
</div>
</div>
))}
</div>
</>
) : (
<div className="no-conversation-selected">
<div className="placeholder-icon">💬</div>
<div className="placeholder-text">请选择一个对话开始</div>
<div className="placeholder-subtitle">
从左侧列表中选择对话记录
</div>
</div>
)}
</div>
{/* 输入区域 */}
{selectedConversation && (
<div className="chat-input-area">
<div className="input-container">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder="请输入您的问题..."
className="message-input"
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
/>
<button
className="send-button"
onClick={handleSendMessage}
disabled={!inputMessage.trim()}
>
发送
</button>
</div>
<div className="input-tips">
<span className="tip-text">
Shift + Enter 可以换行Enter 发送消息
</span>
</div>
</div>
)}
</div>
{/* 问题解决确认模态框 */}
{showResolutionModal && (
<div className="resolution-modal-overlay">
<div className="resolution-modal">
<div className="modal-header">
<h3 className="modal-title">问题解决确认</h3>
<button
className="modal-close"
onClick={() => setShowResolutionModal(null)}
>
×
</button>
</div>
<div className="modal-content">
<div className="modal-icon"></div>
<p className="modal-text">
您在{showResolutionModal.title}中已经超过10分钟没有活动
请问这个问题是否已经得到解决
</p>
</div>
<div className="modal-actions">
<button
className="btn-secondary"
onClick={() => continueConversation(showResolutionModal)}
>
继续讨论
</button>
<button
className="btn-primary"
onClick={() => markAsResolved(showResolutionModal.id)}
>
已解决
</button>
</div>
</div>
</div>
)}
</div>
<FormModal
visible={formVisible}
onClose={handleClose}
handleSend={handleSend}
/>
</>
);
};
export default ExpertSupportPage;
export default ExpertSupportPage;

View File

@@ -0,0 +1,899 @@
/* 专家支持中心页面样式 - 重新设计 */
.expert-support-page {
display: grid;
grid-template-columns: 360px 1fr;
height: calc(100vh - 80px);
background: #f5f7fa;
gap: 1px;
overflow: hidden;
}
/* ===================
左侧对话记录区域
=================== */
.conversation-sidebar {
background: white;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 24px 20px;
border-bottom: 1px solid #f0f1f3;
background: #fafbfc;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.expert-support-sidebar-title {
font-size: 18px;
font-weight: 600;
color: #111827;
margin: 0;
line-height: 1.4;
flex: 1;
}
/* 新建对话按钮 */
.new-conversation-btn {
display: flex;
align-items: center;
gap: 6px;
background: #3b82f6;
color: white;
border: none;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
flex-shrink: 0;
}
.new-conversation-btn:hover {
background: #2563eb;
transform: translateY(-1px);
}
.new-conversation-btn .btn-icon {
font-size: 14px;
}
.new-conversation-btn .btn-text {
white-space: nowrap;
}
/* 禁用状态的新建对话按钮 */
.new-conversation-btn.disabled {
cursor: not-allowed !important;
opacity: 0.6;
background-color: #e5e7eb !important;
}
.new-conversation-btn.disabled:hover {
transform: none !important;
box-shadow: none !important;
background-color: #e5e7eb !important;
}
/* hover提示容器 */
.new-conversation-btn-wrapper {
position: relative;
display: inline-block;
}
/* hover提示气泡 */
.new-conversation-btn-wrapper:hover .hover-tooltip {
display: block !important;
}
.hover-tooltip {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
padding: 6px 12px;
background-color: rgba(0, 0, 0, 0.85);
color: #fff;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
display: none;
z-index: 1000;
pointer-events: none;
}
/* 小三角箭头 */
.hover-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.85);
}
/* 对话分组 */
.conversation-groups {
flex: 1;
overflow-y: auto;
}
.conversation-group {
border-bottom: 1px solid #f3f4f6;
}
.conversation-group:last-child {
border-bottom: none;
}
.group-header {
padding: 16px 20px 8px 20px;
background: #fafbfc;
border-bottom: 1px solid #f0f1f3;
}
.group-title {
font-size: 14px;
font-weight: 600;
color: #6b7280;
margin: 0;
line-height: 1.4;
}
/* 对话列表 */
.conversation-list {
padding: 0;
}
.conversation-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
cursor: pointer;
transition: all 150ms ease;
border-bottom: 1px solid #f3f4f6;
position: relative;
}
.conversation-item:hover {
background: #f8fafc;
}
.conversation-item.selected {
background: #eff6ff;
border-left: 4px solid #3b82f6;
}
.conversation-content {
flex: 1;
min-width: 0;
}
/* 对话标题行 */
.conversation-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.conversation-title {
font-size: 14px;
font-weight: 600;
color: #111827;
margin: 0;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
/* 非活跃提醒 */
.inactivity-alert {
flex-shrink: 0;
}
.alert-dot {
display: inline-block;
width: 8px;
height: 8px;
background: #ef4444;
border-radius: 50%;
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
.conversation-item.has-alert {
border-left: 4px solid #ef4444;
}
.conversation-item.has-alert:hover {
background: #fef2f2;
}
.conversation-preview {
font-size: 13px;
color: #6b7280;
margin: 0 0 8px 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.conversation-time {
font-size: 11px;
color: #9ca3af;
flex-shrink: 0;
}
.conversation-status {
font-size: 10px;
padding: 4px 8px;
border-radius: 12px;
font-weight: 500;
flex-shrink: 0;
text-align: center;
}
.status-resolved {
background: #f0fdf4;
color: #166534;
}
.status-in-progress {
background: #fffbeb;
color: #92400e;
}
.status-pending {
background: #fef2f2;
color: #991b1b;
}
/* ===================
右侧问答界面
=================== */
.chat-interface {
background: white;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部信息栏 */
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px 32px;
flex-shrink: 0;
}
.header-info {
margin-bottom: 16px;
}
.service-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 20px;
margin-bottom: 12px;
backdrop-filter: blur(8px);
}
.badge-icon {
font-size: 16px;
}
.badge-text {
font-size: 13px;
font-weight: 500;
}
.service-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
line-height: 1.3;
}
.service-subtitle {
font-size: 14px;
opacity: 0.9;
line-height: 1.4;
}
.service-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.service-tag {
background: rgba(255, 255, 255, 0.15);
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* 对话内容区域 */
.chat-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
border-bottom: 1px solid #e5e7eb;
background: #fafbfc;
flex-shrink: 0;
}
.active-conversation-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin: 0;
line-height: 1.4;
}
.transfer-expert-btn {
background: #3b82f6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
}
.transfer-expert-btn:hover {
background: #2563eb;
transform: translateY(-1px);
}
/* 消息容器 */
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px 32px;
display: flex;
flex-direction: column;
gap: 20px;
}
.message {
display: flex;
align-items: flex-start;
gap: 12px;
}
.user-message {
flex-direction: row-reverse;
}
.message-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: #f3f4f6;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.message-bubble {
max-width: 70%;
padding: 12px 16px;
border-radius: 16px;
position: relative;
}
.user-message .message-bubble {
background: #3b82f6;
color: white;
border-bottom-right-radius: 4px;
}
.expert-message .message-bubble {
background: #f8fafc;
color: #111827;
border: 1px solid #e5e7eb;
border-bottom-left-radius: 4px;
}
.expert-name {
font-size: 12px;
font-weight: 600;
color: #6b7280;
margin-bottom: 4px;
}
.user-message .expert-name {
color: rgba(255, 255, 255, 0.8);
}
.message-text {
font-size: 14px;
line-height: 1.5;
margin-bottom: 4px;
}
.user-message .message-text {
color: white;
}
.message-time {
font-size: 11px;
opacity: 0.7;
text-align: right;
}
.user-message .message-time {
color: rgba(255, 255, 255, 0.8);
}
.expert-message .message-time {
color: #9ca3af;
}
/* 空状态 */
.no-conversation-selected {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #6b7280;
padding: 40px;
}
.placeholder-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.placeholder-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #374151;
}
.placeholder-subtitle {
font-size: 14px;
color: #9ca3af;
}
/* 输入区域 */
.chat-input-area {
border-top: 1px solid #e5e7eb;
background: #fafbfc;
padding: 20px 32px;
flex-shrink: 0;
}
.input-container {
display: flex;
gap: 12px;
align-items: flex-end;
}
.message-input {
flex: 1;
background: white;
border: 1px solid #d1d5db;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
line-height: 1.5;
resize: none;
transition: all 150ms ease;
min-height: 48px;
}
.message-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.send-button {
background: #3b82f6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 150ms ease;
height: 48px;
flex-shrink: 0;
}
.send-button:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
}
.send-button:disabled {
background: #d1d5db;
color: #9ca3af;
cursor: not-allowed;
transform: none;
}
.input-tips {
margin-top: 8px;
text-align: center;
}
.tip-text {
font-size: 12px;
color: #9ca3af;
}
/* ===================
响应式设计
=================== */
@media (max-width: 1024px) {
.expert-support-page {
grid-template-columns: 300px 1fr;
}
.conversation-sidebar {
width: 300px;
}
.sidebar-header {
padding: 20px 16px;
}
.new-conversation-btn .btn-text {
display: none;
}
.new-conversation-btn {
padding: 8px;
border-radius: 6px;
}
.chat-header {
padding: 20px 24px;
}
.messages-container {
padding: 20px 24px;
}
.chat-input-area {
padding: 16px 24px;
}
}
@media (max-width: 768px) {
.expert-support-page {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.conversation-sidebar {
height: 200px;
width: 100%;
border-right: none;
border-bottom: 1px solid #e5e7eb;
}
.sidebar-header {
padding: 16px;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.new-conversation-btn {
width: 100%;
justify-content: center;
}
.new-conversation-btn .btn-text {
display: inline;
}
.conversation-groups {
overflow-x: auto;
}
.conversation-list {
display: flex;
gap: 8px;
padding: 8px 16px;
}
.conversation-item {
min-width: 280px;
flex-shrink: 0;
border-bottom: none;
border-radius: 8px;
border: 1px solid #e5e7eb;
margin-bottom: 0;
}
.chat-header {
padding: 16px 20px;
}
.service-title {
font-size: 20px;
}
.messages-container {
padding: 16px 20px;
}
.chat-input-area {
padding: 16px 20px;
}
.message-bubble {
max-width: 85%;
}
.chat-title-bar {
padding: 16px 20px;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.chat-header {
padding: 12px 16px;
}
.service-title {
font-size: 18px;
}
.service-subtitle {
font-size: 13px;
}
.messages-container {
padding: 12px 16px;
}
.chat-input-area {
padding: 12px 16px;
}
.input-container {
flex-direction: column;
gap: 8px;
}
.send-button {
width: 100%;
}
}
/* ===================
问题解决确认模态框
=================== */
.resolution-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.resolution-modal {
background: white;
border-radius: 12px;
width: 90%;
max-width: 480px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
overflow: hidden;
animation: modalAppear 0.2s ease-out;
}
@keyframes modalAppear {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
background: #fafbfc;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #111827;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
color: #6b7280;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 150ms ease;
}
.modal-close:hover {
background: #f3f4f6;
color: #374151;
}
.modal-content {
padding: 24px;
text-align: center;
}
.modal-icon {
font-size: 48px;
margin-bottom: 16px;
}
.modal-text {
font-size: 16px;
line-height: 1.6;
color: #374151;
margin: 0;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e5e7eb;
background: #fafbfc;
}
.btn-secondary {
flex: 1;
background: white;
color: #374151;
border: 1px solid #d1d5db;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.btn-primary {
flex: 1;
background: #3b82f6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
}
/* 响应式模态框 */
@media (max-width: 768px) {
.resolution-modal {
width: 95%;
margin: 0 10px;
}
.modal-header {
padding: 16px 20px;
}
.modal-content {
padding: 20px;
}
.modal-actions {
padding: 16px 20px;
flex-direction: column;
}
.btn-secondary,
.btn-primary {
width: 100%;
}
}

View File

@@ -0,0 +1,543 @@
import React, { useState, useEffect, useRef } from "react";
import { expertQAData, expertInfo } from "@/data/expertQAData";
import "./index.css";
const ExpertSupportPage = () => {
// 对话管理
const [selectedConversation, setSelectedConversation] = useState(null);
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState("");
const [showResolutionModal, setShowResolutionModal] = useState(null);
const [isNewConversation, setIsNewConversation] = useState(false);
// 用于跟踪最后活动时间
// 初始对话数据 - 整合文旅产业问答内容
const [conversationGroups, setConversationGroups] = useState({
今天: [], // 清空今天板块的对话记录
"7天内": [
// 导入文旅产业问答数据中的最近记录
...expertQAData.slice(0, 3).map(qa => ({
...qa,
lastActivityTime: Date.now() - (Math.floor(Math.random() * 6) + 1) * 24 * 60 * 60 * 1000
})),
{
id: 7,
title: "文创产品开发咨询",
lastMessage: "如何设计有文化内涵的旅游纪念品",
time: "01-03",
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastActivityTime: Date.now() - 5 * 24 * 60 * 60 * 1000,
messages: []
},
],
"30天内": [
// 导入文旅产业问答数据中的其余记录
...expertQAData.slice(3, 6).map(qa => ({
...qa,
lastActivityTime: Date.now() - (Math.floor(Math.random() * 10) + 15) * 24 * 60 * 60 * 1000
})),
{
id: 11,
title: "智慧旅游技术应用",
lastMessage: "如何利用数字技术提升游客体验",
time: "12-12",
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastActivityTime: Date.now() - 26 * 24 * 60 * 60 * 1000,
messages: []
},
],
});
// 当前活跃对话的消息记录
const conversationMessages = [
{
id: 1,
type: "user",
content: "您好,我遇到了一些问题",
time: "2025-01-08 16:45",
avatar: "👤",
},
{
id: 2,
type: "expert",
content:
"你好我是多多ai智能问答小助手有什么问题就来问我吧",
time: "2025-01-08 16:46",
avatar: "👨‍💻",
expertName: "多多机器人",
},
{
id: 3,
type: "user",
content: "我需要了解更多关于泛型的使用,特别是在复杂数据结构中的应用",
time: "2025-01-08 16:48",
avatar: "👤",
},
];
// 创建新对话
const createNewConversation = () => {
const newConversation = {
id: Date.now(),
title: "新的问题咨询",
lastMessage: "",
time: new Date().toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
}),
status: "进行中",
statusType: "in-progress",
hasInactivityAlert: false,
lastActivityTime: Date.now(),
};
setConversationGroups((prev) => ({
...prev,
今天: [newConversation, ...prev["今天"]],
}));
setSelectedConversation(newConversation);
setMessages([]);
setIsNewConversation(true);
};
// 检查非活跃状态
const checkInactivityStatus = () => {
const currentTime = Date.now();
const INACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10分钟
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.statusType === "in-progress") {
const timeSinceLastActivity =
currentTime - conversation.lastActivityTime;
const shouldShowAlert =
timeSinceLastActivity >= INACTIVITY_THRESHOLD;
if (shouldShowAlert !== conversation.hasInactivityAlert) {
return { ...conversation, hasInactivityAlert: shouldShowAlert };
}
}
return conversation;
});
});
return updated;
});
};
// 处理对话选择
const handleConversationSelect = (conversation) => {
// 如果是带有红点的对话,点击时询问是否已解决
if (
conversation.hasInactivityAlert &&
conversation.statusType === "in-progress"
) {
setShowResolutionModal(conversation);
} else {
setSelectedConversation(conversation);
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
if (conversation.messages && conversation.messages.length > 0) {
setMessages(conversation.messages);
} else {
setMessages(conversationMessages);
}
setIsNewConversation(false);
}
};
// 标记问题为已解决
const markAsResolved = (conversationId) => {
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === conversationId) {
return {
...conversation,
status: "已解决",
statusType: "resolved",
hasInactivityAlert: false,
lastMessage: "问题已解决",
};
}
return conversation;
});
});
return updated;
});
setShowResolutionModal(null);
// 如果当前选中的是这个对话,取消选择
if (selectedConversation?.id === conversationId) {
setSelectedConversation(null);
setMessages([]);
}
};
// 继续对话
const continueConversation = (conversation) => {
// 更新最后活动时间
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conv) => {
if (conv.id === conversation.id) {
return {
...conv,
hasInactivityAlert: false,
lastActivityTime: Date.now(),
};
}
return conv;
});
});
return updated;
});
setSelectedConversation(conversation);
// 如果对话有预设的消息记录,使用它们;否则使用默认消息
if (conversation.messages && conversation.messages.length > 0) {
setMessages(conversation.messages);
} else {
setMessages(conversationMessages);
}
setShowResolutionModal(null);
setIsNewConversation(false);
};
// 发送消息
const handleSendMessage = () => {
if (!inputMessage.trim()) return;
const newMessage = {
id: messages.length + 1,
type: "user",
content: inputMessage,
time: new Date().toLocaleString("zh-CN"),
avatar: "👤",
};
setMessages([...messages, newMessage]);
setInputMessage("");
// 更新当前对话的标题和最后消息(如果是新对话)
if (selectedConversation && isNewConversation && messages.length === 0) {
const title =
inputMessage.length > 20
? inputMessage.substring(0, 20) + "..."
: inputMessage;
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === selectedConversation.id) {
return {
...conversation,
title: title,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
};
}
return conversation;
});
});
return updated;
});
setSelectedConversation((prev) => ({
...prev,
title: title,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
}));
setIsNewConversation(false);
} else if (selectedConversation) {
// 更新现有对话的最后活动时间
setConversationGroups((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((groupKey) => {
updated[groupKey] = updated[groupKey].map((conversation) => {
if (conversation.id === selectedConversation.id) {
return {
...conversation,
lastMessage: inputMessage,
lastActivityTime: Date.now(),
hasInactivityAlert: false,
};
}
return conversation;
});
});
return updated;
});
}
};
// 组件生命周期
useEffect(() => {
// 页面加载时自动创建新对话
createNewConversation();
// 定时检查非活跃状态
const interval = setInterval(checkInactivityStatus, 30000); // 每30秒检查一次
return () => clearInterval(interval);
}, []);
// 组件挂载后进行初始检查
useEffect(() => {
const timeoutId = setTimeout(checkInactivityStatus, 1000);
return () => clearTimeout(timeoutId);
}, []);
// 转专家服务
const handleTransferToExpert = () => {
if (
selectedConversation &&
selectedConversation.statusType === "in-progress"
) {
// 更新对话状态逻辑
alert("已转交人工专家,将有专业老师为您服务");
}
};
return (
<div className="expert-support-page">
{/* 左侧对话记录区域 */}
<div className="conversation-sidebar">
<div className="sidebar-header">
<h2 className="expert-support-sidebar-title">对话记录</h2>
<div className="new-conversation-btn-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
<button
className="new-conversation-btn disabled"
onClick={(e) => e.preventDefault()}
disabled
style={{
cursor: 'not-allowed',
opacity: 0.6,
backgroundColor: '#f5f5f5'
}}
>
<span className="btn-icon">💬</span>
<span className="btn-text">新建对话</span>
</button>
<div className="hover-tooltip" style={{
position: 'absolute',
top: '-35px',
left: '50%',
transform: 'translateX(-50%)',
padding: '6px 12px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
borderRadius: '4px',
fontSize: '12px',
whiteSpace: 'nowrap',
display: 'none',
zIndex: 1000
}}>
非学员无权限
</div>
</div>
</div>
{/* 按时间分组的对话列表 */}
<div className="conversation-groups">
{Object.entries(conversationGroups).map(
([groupName, conversations]) => (
<div key={groupName} className="conversation-group">
<div className="group-header">
<h3 className="group-title">{groupName}</h3>
</div>
<div className="conversation-list">
{conversations.map((conversation) => (
<div
key={conversation.id}
className={`conversation-item ${
selectedConversation?.id === conversation.id
? "selected"
: ""
} ${conversation.hasInactivityAlert ? "has-alert" : ""}`}
onClick={() => handleConversationSelect(conversation)}
>
<div className="conversation-content">
<div className="conversation-title-row">
<h4 className="conversation-title">
{conversation.title}
</h4>
{conversation.hasInactivityAlert && (
<div className="inactivity-alert">
<span className="alert-dot"></span>
</div>
)}
</div>
<p className="conversation-preview">
{conversation.lastMessage}
</p>
</div>
<div className="conversation-meta">
<span className="conversation-time">
{conversation.time}
</span>
<span
className={`conversation-status status-${conversation.statusType}`}
>
{conversation.status}
</span>
</div>
</div>
))}
</div>
</div>
)
)}
</div>
</div>
{/* 右侧问答界面 */}
<div className="chat-interface">
{/* 顶部信息栏 */}
<div className="chat-header">
<div className="header-info">
<div className="service-badge">
<span className="badge-icon">🎓</span>
<span className="badge-text">专家客服</span>
</div>
<div className="service-title">学有所问向必有答</div>
<div className="service-subtitle">
我们有创新学习习惯养成方案
</div>
</div>
<div className="service-actions">
<div className="service-tag">专家答疑</div>
<div className="service-tag">快速响应</div>
<div className="service-tag">24小时服务</div>
</div>
</div>
{/* 对话内容区域 */}
<div className="chat-content">
{selectedConversation ? (
<>
<div className="chat-title-bar">
<h3 className="active-conversation-title">
{selectedConversation.title}
</h3>
{selectedConversation.statusType === "in-progress" && (
<button
className="transfer-expert-btn"
onClick={handleTransferToExpert}
>
转专家服务
</button>
)}
</div>
<div className="messages-container">
{messages.map((message) => (
<div
key={message.id}
className={`message ${message.type}-message`}
>
<div className="message-avatar">{message.avatar}</div>
<div className="message-bubble">
{message.type === "expert" && (
<div className="expert-name">{message.expertName}</div>
)}
<div className="message-text">{message.content}</div>
<div className="message-time">{message.time}</div>
</div>
</div>
))}
</div>
</>
) : (
<div className="no-conversation-selected">
<div className="placeholder-icon">💬</div>
<div className="placeholder-text">请选择一个对话开始</div>
<div className="placeholder-subtitle">
从左侧列表中选择对话记录
</div>
</div>
)}
</div>
{/* 输入区域 */}
{selectedConversation && (
<div className="chat-input-area">
<div className="input-container">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder="请输入您的问题..."
className="message-input"
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
/>
<button
className="send-button"
onClick={handleSendMessage}
disabled={!inputMessage.trim()}
>
发送
</button>
</div>
<div className="input-tips">
<span className="tip-text">
Shift + Enter 可以换行Enter 发送消息
</span>
</div>
</div>
)}
</div>
{/* 问题解决确认模态框 */}
{showResolutionModal && (
<div className="resolution-modal-overlay">
<div className="resolution-modal">
<div className="modal-header">
<h3 className="modal-title">问题解决确认</h3>
<button
className="modal-close"
onClick={() => setShowResolutionModal(null)}
>
×
</button>
</div>
<div className="modal-content">
<div className="modal-icon"></div>
<p className="modal-text">
您在{showResolutionModal.title}中已经超过10分钟没有活动
请问这个问题是否已经得到解决
</p>
</div>
<div className="modal-actions">
<button
className="btn-secondary"
onClick={() => continueConversation(showResolutionModal)}
>
继续讨论
</button>
<button
className="btn-primary"
onClick={() => markAsResolved(showResolutionModal.id)}
>
已解决
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ExpertSupportPage;