init
This commit is contained in:
847
src/pages/ExpertSupportPage/index.css
Normal file
847
src/pages/ExpertSupportPage/index.css
Normal file
@@ -0,0 +1,847 @@
|
||||
/* 专家支持中心页面样式 - 重新设计 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 对话分组 */
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
587
src/pages/ExpertSupportPage/index.jsx
Normal file
587
src/pages/ExpertSupportPage/index.jsx
Normal file
@@ -0,0 +1,587 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
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({
|
||||
今天: [
|
||||
{
|
||||
id: 1,
|
||||
title: "TypeScript类型定义疑问",
|
||||
lastMessage: "我需要了解更多关于泛型的使用",
|
||||
time: "16:45",
|
||||
status: "进行中",
|
||||
statusType: "in-progress",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 15 * 60 * 1000, // 15分钟前(模拟超时)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "数据库设计方案讨论",
|
||||
lastMessage: "当前的表结构设计是否合理?",
|
||||
time: "15:30",
|
||||
status: "进行中",
|
||||
statusType: "in-progress",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 5 * 60 * 1000, // 5分钟前
|
||||
},
|
||||
],
|
||||
"7天内": [
|
||||
{
|
||||
id: 3,
|
||||
title: "前端开发环境配置问题",
|
||||
lastMessage: "问题已解决,感谢专家指导",
|
||||
time: "01-07",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "React组件性能优化咨询",
|
||||
lastMessage: "优化方案已实施,性能提升明显",
|
||||
time: "01-06",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 2 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Vue3升级迁移指导",
|
||||
lastMessage: "迁移过程中遇到的兼容性问题",
|
||||
time: "01-05",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 3 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Node.js性能调优建议",
|
||||
lastMessage: "内存泄漏排查和解决方案",
|
||||
time: "01-04",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 4 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Git分支管理策略",
|
||||
lastMessage: "多人协作开发的最佳实践",
|
||||
time: "01-03",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 5 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
"30天内": [
|
||||
{
|
||||
id: 8,
|
||||
title: "微服务架构设计咨询",
|
||||
lastMessage: "希望专家能提供详细的实施方案",
|
||||
time: "12-20",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 18 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Docker容器化部署问题",
|
||||
lastMessage: "生产环境部署的最佳配置",
|
||||
time: "12-18",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "Redis缓存策略优化",
|
||||
lastMessage: "缓存穿透和雪崩的防护措施",
|
||||
time: "12-15",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 23 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: "MySQL索引优化指导",
|
||||
lastMessage: "慢查询分析和索引设计建议",
|
||||
time: "12-12",
|
||||
status: "已解决",
|
||||
statusType: "resolved",
|
||||
hasInactivityAlert: false,
|
||||
lastActivityTime: Date.now() - 26 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 当前活跃对话的消息记录
|
||||
const conversationMessages = [
|
||||
{
|
||||
id: 1,
|
||||
type: "user",
|
||||
content: "您好,我在使用TypeScript时遇到了一些泛型定义的问题",
|
||||
time: "2025-01-08 16:45",
|
||||
avatar: "👤",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "expert",
|
||||
content:
|
||||
"您好!我是王开发专家,很高兴为您解答TypeScript相关问题。请详细描述一下您遇到的具体问题。",
|
||||
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);
|
||||
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);
|
||||
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="sidebar-title">对话记录</h2>
|
||||
<button
|
||||
className="new-conversation-btn"
|
||||
onClick={createNewConversation}
|
||||
>
|
||||
<span className="btn-icon">💬</span>
|
||||
<span className="btn-text">新建对话</span>
|
||||
</button>
|
||||
</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;
|
||||
Reference in New Issue
Block a user