This commit is contained in:
超级老白兔
2025-09-26 14:33:43 +08:00
parent 0159a246ac
commit e0feb6dff1
4 changed files with 587 additions and 30 deletions

View File

@@ -11,6 +11,7 @@
align-items: center; align-items: center;
min-height: 64px; min-height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
justify-content: space-between;
.headerLeft { .headerLeft {
display: flex; display: flex;

View File

@@ -13,6 +13,7 @@ export interface PowerNavigationProps {
onBackClick?: () => void; onBackClick?: () => void;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
rightContent?: React.ReactNode;
} }
const PowerNavigation: React.FC<PowerNavigationProps> = ({ const PowerNavigation: React.FC<PowerNavigationProps> = ({
@@ -23,6 +24,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
onBackClick, onBackClick,
className, className,
style, style,
rightContent,
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -57,6 +59,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
{subtitle && <span className={styles.subtitle}>{subtitle}</span>} {subtitle && <span className={styles.subtitle}>{subtitle}</span>}
</div> </div>
</div> </div>
<div className={styles.headerRight}>{rightContent}</div>
</div> </div>
); );
}; };

View File

@@ -5,39 +5,289 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.header { // 导出按钮样式
margin-bottom: 24px; .exportButton {
height: 36px;
h1 { border-radius: 6px;
font-size: 24px; font-size: 14px;
font-weight: 600; font-weight: 500;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
} }
.content { .content {
min-height: 400px; min-height: 400px;
} }
.placeholder { // 顶部搜索和筛选区域
.headerSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.searchBar {
flex: 1;
max-width: 400px;
}
.filterButtons {
display: flex;
gap: 12px;
.ant-btn {
height: 36px;
border-radius: 6px;
font-size: 14px;
}
}
// 导航栏样式
.navigationTabs {
margin-bottom: 24px;
.tabs {
.ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-tab {
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
.anticon {
margin-right: 8px;
}
}
.ant-tabs-tab-active {
color: #1890ff;
.ant-tabs-tab-btn {
color: #1890ff;
}
}
.ant-tabs-ink-bar {
background: #1890ff;
}
}
}
// 记录列表样式
.recordsList {
display: flex;
flex-direction: column;
gap: 16px;
}
.recordCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #d9d9d9;
}
.ant-card-body {
padding: 16px 20px;
}
}
.cardContent {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.cardLeft {
display: flex;
gap: 12px;
flex: 1;
}
.avatar {
width: 40px;
height: 40px;
background: #1890ff;
color: #fff;
font-size: 16px;
font-weight: 600;
flex-shrink: 0;
}
.recordInfo {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.nameAndType {
display: flex;
align-items: center;
gap: 12px;
.name {
font-size: 15px;
font-weight: 600;
color: #262626;
}
.type {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #8c8c8c;
.anticon {
font-size: 13px;
}
}
}
.statusAndTime {
display: flex;
align-items: center;
gap: 12px;
.dateTime {
font-size: 13px;
color: #8c8c8c;
}
.duration {
font-size: 13px;
color: #8c8c8c;
}
}
.directionAndSubject {
display: flex;
align-items: center;
gap: 12px;
.subject {
font-size: 13px;
color: #262626;
font-weight: 500;
}
}
.content {
font-size: 13px;
color: #595959;
line-height: 1.4;
margin: 2px 0;
}
.tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
.tag {
font-size: 11px;
background: #f5f5f5;
border: 1px solid #d9d9d9;
color: #8c8c8c;
border-radius: 3px;
padding: 1px 6px;
margin: 0;
}
}
.attachments {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 6px;
.attachmentsLabel {
font-size: 12px;
color: #8c8c8c;
font-weight: 500;
}
.attachment {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #f0f0f0;
.attachmentIcon {
font-size: 14px;
}
.attachmentName {
font-size: 12px;
color: #262626;
flex: 1;
}
.downloadIcon {
color: #8c8c8c;
cursor: pointer;
transition: color 0.3s ease;
font-size: 12px;
&:hover {
color: #1890ff;
}
}
}
}
.cardRight {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 300px; width: 28px;
background: #fafafa; height: 28px;
border: 1px dashed #d9d9d9;
border-radius: 6px; .viewIcon {
font-size: 14px;
p {
font-size: 16px;
color: #8c8c8c; color: #8c8c8c;
margin: 0; cursor: pointer;
transition: color 0.3s ease;
&:hover {
color: #1890ff;
}
} }
} }
// 响应式设计
@media (max-width: 768px) {
.headerSection {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.searchBar {
max-width: none;
}
.filterButtons {
justify-content: center;
}
.cardContent {
flex-direction: column;
gap: 16px;
}
.cardRight {
align-self: flex-end;
}
}

View File

@@ -1,20 +1,323 @@
import React from "react"; import React, { useState } from "react";
import { Input, Button, Tabs, Tag, Avatar, Card } from "antd";
import {
SearchOutlined,
FilterOutlined,
CalendarOutlined,
MessageOutlined,
PhoneOutlined,
VideoCameraOutlined,
MailOutlined,
EyeOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion"; import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
const { Search } = Input;
interface CommunicationRecord {
id: string;
avatar: string;
name: string;
type: "chat" | "call" | "video" | "email";
status: "completed" | "pending" | "cancelled";
dateTime: string;
duration?: string;
direction: "incoming" | "outgoing";
subject?: string;
content: string;
tags: string[];
attachments?: Array<{
name: string;
type: "pdf" | "xlsx" | "doc" | "other";
}>;
}
const CommunicationRecord: React.FC = () => { const CommunicationRecord: React.FC = () => {
const [activeTab, setActiveTab] = useState("chat");
const [searchValue, setSearchValue] = useState("");
// 模拟数据
const mockData: CommunicationRecord[] = [
{
id: "1",
avatar: "李",
name: "李先生",
type: "chat",
status: "completed",
dateTime: "2024/3/5 14:30:00",
direction: "incoming",
content: "咨询AI营销产品的详细功能和价格",
tags: ["产品咨询", "价格询问"],
},
{
id: "2",
avatar: "张",
name: "张总",
type: "call",
status: "completed",
dateTime: "2024/3/5 10:15:00",
duration: "25分钟",
direction: "outgoing",
subject: "产品演示预约",
content: "与客户确认产品演示时间,讨论具体需求",
tags: ["产品演示", "需求确认"],
},
{
id: "3",
avatar: "王",
name: "王女士",
type: "video",
status: "completed",
dateTime: "2024/3/4 16:45:00",
duration: "45分钟",
direction: "incoming",
subject: "产品功能演示",
content: "详细演示AI客服功能,客户表示很满意",
tags: ["产品演示", "功能介绍"],
attachments: [
{ name: "产品介绍.pdf", type: "pdf" },
{ name: "报价单.xlsx", type: "xlsx" },
],
},
];
const getTypeIcon = (type: string) => {
switch (type) {
case "chat":
return <MessageOutlined />;
case "call":
return <PhoneOutlined />;
case "video":
return <VideoCameraOutlined />;
case "email":
return <MailOutlined />;
default:
return <MessageOutlined />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "success";
case "pending":
return "processing";
case "cancelled":
return "error";
default:
return "default";
}
};
const getDirectionColor = (direction: string) => {
return direction === "incoming" ? "green" : "blue";
};
const getDirectionText = (direction: string) => {
return direction === "incoming" ? "来电/来信" : "去电/去信";
};
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "已完成";
case "pending":
return "进行中";
case "cancelled":
return "已取消";
default:
return "未知";
}
};
const getAttachmentIcon = (type: string) => {
switch (type) {
case "pdf":
return "📄";
case "xlsx":
return "📊";
case "doc":
return "📝";
default:
return "📎";
}
};
const filteredData = mockData.filter(
record =>
record.type === activeTab &&
(searchValue === "" ||
record.name.includes(searchValue) ||
record.content.includes(searchValue) ||
record.tags.some(tag => tag.includes(searchValue))),
);
const tabItems = [
{
key: "chat",
label: (
<span>
<MessageOutlined />
(1)
</span>
),
},
{
key: "call",
label: (
<span>
<PhoneOutlined />
(2)
</span>
),
},
{
key: "video",
label: (
<span>
<VideoCameraOutlined />
(1)
</span>
),
},
{
key: "email",
label: (
<span>
<MailOutlined />
(1)
</span>
),
},
];
// 导出记录处理函数
const handleExportRecords = () => {
console.log("导出记录功能");
// TODO: 实现导出功能
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
<PowerNavigation <PowerNavigation
title="沟通记录" title="沟通记录"
subtitle="完整记录客户沟通历史,支持多维度查询分析" subtitle="查看和管理所有客户沟通记录"
showBackButton={true} showBackButton={true}
backButtonText="返回功能中心" backButtonText="返回功能中心"
rightContent={
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleExportRecords}
className={styles.exportButton}
>
</Button>
}
/> />
<div className={styles.content}> <div className={styles.content}>
{/* 功能内容待开发 */} {/* 顶部搜索和筛选区域 */}
<div className={styles.placeholder}> <div className={styles.headerSection}>
<p>...</p> <div className={styles.searchBar}>
<Search
placeholder="搜索客户、内容或标签..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
</div>
<div className={styles.filterButtons}>
<Button icon={<FilterOutlined />}></Button>
<Button icon={<CalendarOutlined />}></Button>
</div>
</div>
{/* 通信类型导航栏 */}
<div className={styles.navigationTabs}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
className={styles.tabs}
/>
</div>
{/* 通信记录列表 */}
<div className={styles.recordsList}>
{filteredData.map(record => (
<Card key={record.id} className={styles.recordCard}>
<div className={styles.cardContent}>
<div className={styles.cardLeft}>
<Avatar className={styles.avatar}>{record.avatar}</Avatar>
<div className={styles.recordInfo}>
<div className={styles.nameAndType}>
<span className={styles.name}>{record.name}</span>
<span className={styles.type}>
{getTypeIcon(record.type)}
{record.type === "chat"
? "聊天"
: record.type === "call"
? "通话"
: record.type === "video"
? "视频"
: "邮件"}
</span>
</div>
<div className={styles.statusAndTime}>
<Tag color={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
<span className={styles.dateTime}>{record.dateTime}</span>
{record.duration && (
<span className={styles.duration}>
:{record.duration}
</span>
)}
</div>
<div className={styles.directionAndSubject}>
<Tag color={getDirectionColor(record.direction)}>
{getDirectionText(record.direction)}
</Tag>
{record.subject && (
<span className={styles.subject}>{record.subject}</span>
)}
</div>
<div className={styles.content}>{record.content}</div>
<div className={styles.tags}>
{record.tags.map((tag, index) => (
<Tag key={index} className={styles.tag}>
{tag}
</Tag>
))}
</div>
{record.attachments && record.attachments.length > 0 && (
<div className={styles.attachments}>
<span className={styles.attachmentsLabel}>:</span>
{record.attachments.map((attachment, index) => (
<div key={index} className={styles.attachment}>
<span className={styles.attachmentIcon}>
{getAttachmentIcon(attachment.type)}
</span>
<span className={styles.attachmentName}>
{attachment.name}
</span>
<DownloadOutlined className={styles.downloadIcon} />
</div>
))}
</div>
)}
</div>
</div>
<div className={styles.cardRight}>
<EyeOutlined className={styles.viewIcon} />
</div>
</div>
</Card>
))}
</div> </div>
</div> </div>
</div> </div>