refactor(sidebar): rewrite summary display to support structured summary parsing
重构了侧边栏的每日总结组件,新增了结构化解析能力,可以识别核心诉求、处理结果、待办等区块和标签,优化了卡片样式和展示细节,提升了总结内容的可读性和美观度。
This commit is contained in:
parent
4ba2109f8d
commit
51310683c7
@ -19,6 +19,17 @@ interface SummaryItem {
|
||||
staffName?: string
|
||||
}
|
||||
|
||||
interface SummarySection {
|
||||
title: string
|
||||
items: string[]
|
||||
}
|
||||
|
||||
interface ParsedSummary {
|
||||
sections: SummarySection[]
|
||||
tags: string[]
|
||||
raw: string
|
||||
}
|
||||
|
||||
function SummaryApp() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [list, setList] = useState<SummaryItem[]>([])
|
||||
@ -281,11 +292,11 @@ function SummaryApp() {
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: '12px 16px',
|
||||
marginBottom: 12,
|
||||
padding: '14px 16px',
|
||||
marginBottom: 14,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
|
||||
}}>
|
||||
<div style={{ fontSize: 16, fontWeight: 500, color: '#333' }}>
|
||||
<div style={{ fontSize: 17, fontWeight: 600, color: '#333' }}>
|
||||
📋 每日总结
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||
@ -294,30 +305,352 @@ function SummaryApp() {
|
||||
</div>
|
||||
|
||||
{list.map((item, index) => (
|
||||
<div key={item.id || index} style={{
|
||||
<SummaryCard key={item.id || index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SummaryCard({ item }: { item: SummaryItem }) {
|
||||
const parsed = parseSummary(item.summary)
|
||||
const hasStructure = parsed.sections.length > 0 || parsed.tags.length > 0
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: '14px 16px',
|
||||
marginBottom: 12,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
|
||||
borderRadius: 14,
|
||||
padding: '18px 16px',
|
||||
marginBottom: 14,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.05)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 13, color: '#1677ff', fontWeight: 500 }}>
|
||||
{/* 头部 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 14,
|
||||
marginBottom: 16,
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CalendarIcon />
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: '#1677ff' }}>
|
||||
{item.msgTime || formatDate(item.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<UserIcon />
|
||||
<span style={{ fontSize: 14, color: '#666' }}>
|
||||
{item.staffName || item.staffId || '未知老师'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 14, lineHeight: 1.7, color: '#333', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||
|
||||
{hasStructure ? (
|
||||
<>
|
||||
{parsed.sections.map((section, idx) => (
|
||||
<SectionBlock key={idx} section={section} />
|
||||
))}
|
||||
{parsed.tags.length > 0 && <TagList tags={parsed.tags} />}
|
||||
</>
|
||||
) : (
|
||||
<div style={{ fontSize: 14, lineHeight: 1.8, color: '#333', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||
{item.summary || item.messageContext || '无内容'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionBlock({ section }: { section: SummarySection }) {
|
||||
const config = getSectionConfig(section.title)
|
||||
const showTitle = displaySectionTitle(section.title)
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 18 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: config.titleColor
|
||||
}}>
|
||||
<span style={{ fontSize: 16 }}>{config.icon}</span>
|
||||
{showTitle}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{section.items.map((text, idx) => (
|
||||
<div key={idx} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
||||
<div style={{ flexShrink: 0, marginTop: 2 }}>
|
||||
{section.title.includes('核心诉求') ? (
|
||||
<NumberBadge n={idx + 1} />
|
||||
) : section.title.includes('处理结果') ? (
|
||||
<GreenCheck />
|
||||
) : section.title.includes('待办') ? (
|
||||
<OrangeClock />
|
||||
) : (
|
||||
<Dot color={config.titleColor} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 14,
|
||||
lineHeight: 1.7,
|
||||
color: '#333',
|
||||
wordBreak: 'break-all',
|
||||
flex: 1
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TagList({ tags }: { tags: string[] }) {
|
||||
return (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 10,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: '#1677ff'
|
||||
}}>
|
||||
<span style={{ fontSize: 16 }}>🏷️</span>
|
||||
关键标签
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{tags.map((tag, idx) => (
|
||||
<span key={idx} style={{
|
||||
display: 'inline-block',
|
||||
padding: '5px 12px',
|
||||
background: '#e6f4ff',
|
||||
color: '#1677ff',
|
||||
borderRadius: 16,
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
border: '1px solid #bae0ff'
|
||||
}}>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getSectionConfig(title: string) {
|
||||
if (title.includes('核心诉求')) {
|
||||
return { icon: '📝', titleColor: '#1677ff' }
|
||||
}
|
||||
if (title.includes('处理结果')) {
|
||||
return { icon: '✅', titleColor: '#52c41a' }
|
||||
}
|
||||
if (title.includes('待办') || title.includes('后续')) {
|
||||
return { icon: '⏰', titleColor: '#fa8c16' }
|
||||
}
|
||||
return { icon: '•', titleColor: '#666' }
|
||||
}
|
||||
|
||||
function displaySectionTitle(title: string) {
|
||||
if (title.includes('核心诉求')) return '学员咨询'
|
||||
if (title.includes('处理结果')) return '老师解答'
|
||||
if (title.includes('待办')) return '跟进建议'
|
||||
return title
|
||||
}
|
||||
|
||||
function NumberBadge({ n }: { n: number }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: '#1677ff',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
{n}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GreenCheck() {
|
||||
return (
|
||||
<div style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
color: '#52c41a',
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
✓
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OrangeClock() {
|
||||
return (
|
||||
<div style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
color: '#fa8c16',
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
🕐
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Dot({ color }: { color: string }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
marginTop: 6
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#1677ff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function UserIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#999" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function parseSummary(text?: string): ParsedSummary {
|
||||
if (!text) return { sections: [], tags: [], raw: '' }
|
||||
|
||||
const result: ParsedSummary = { sections: [], tags: [], raw: text }
|
||||
let content = text
|
||||
|
||||
// 提取关键标签:支持 "**关键标签**:#a#b" 或 "关键标签:#a#b" 或单独行
|
||||
const tagPatterns = [
|
||||
/(?:\*\*)?关键标签(?:\*\*)?[::]\s*(.+?)(?=\n\s*\*\*|\n\s*$|$)/s,
|
||||
/(?:\*\*)?标签(?:\*\*)?[::]\s*(.+?)(?=\n\s*\*\*|\n\s*$|$)/s
|
||||
]
|
||||
for (const pattern of tagPatterns) {
|
||||
const tagMatch = content.match(pattern)
|
||||
if (tagMatch) {
|
||||
result.tags = parseTags(tagMatch[1])
|
||||
content = content.replace(tagMatch[0], '')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 按 **标题** 分隔区块,标题可能有空格
|
||||
const sectionRegex = /\*\*\s*(核心诉求|处理结果|待办事项|待处理|后续跟进|关键标签)\s*\*\*/g
|
||||
const parts: { title: string; text: string }[] = []
|
||||
let lastIndex = 0
|
||||
let lastTitle = ''
|
||||
let match
|
||||
|
||||
while ((match = sectionRegex.exec(content)) !== null) {
|
||||
if (lastTitle) {
|
||||
parts.push({ title: lastTitle, text: content.slice(lastIndex, match.index) })
|
||||
}
|
||||
lastTitle = match[1]
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
if (lastTitle) {
|
||||
parts.push({ title: lastTitle, text: content.slice(lastIndex) })
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.title === '关键标签') {
|
||||
const tags = parseTags(part.text)
|
||||
if (tags.length) result.tags = tags
|
||||
continue
|
||||
}
|
||||
const items = parseItems(part.text)
|
||||
if (items.length > 0) {
|
||||
result.sections.push({ title: part.title, items })
|
||||
}
|
||||
}
|
||||
|
||||
// 没有任何结构化区块时,尝试整段按行解析列表
|
||||
if (result.sections.length === 0 && result.tags.length === 0) {
|
||||
const items = parseItems(content)
|
||||
if (items.length > 1) {
|
||||
result.sections.push({ title: '总结', items })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function parseItems(text: string): string[] {
|
||||
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
||||
const items: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// 匹配 1. / 1. / - / * / • 开头的列表项
|
||||
const match = line.match(/^(?:\d+[..]\s*|[\-*•]\s+)(.+)$/)
|
||||
if (match) {
|
||||
items.push(cleanText(match[1]))
|
||||
} else if (items.length > 0 && !line.match(/^\*\*/)) {
|
||||
// 续行(非标题)
|
||||
items[items.length - 1] += ' ' + cleanText(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果整段没有列表项,把段落本身作为一条内容(兼容纯文本区块)
|
||||
if (items.length === 0 && text.trim().length > 0) {
|
||||
items.push(cleanText(text.trim()))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function parseTags(text: string): string[] {
|
||||
const s = text.trim()
|
||||
if (!s) return []
|
||||
// 支持 #标签1#标签2 或 标签1#标签2 或 #标签1 #标签2
|
||||
return s.split(/[#\s]+/)
|
||||
.map(t => t.trim())
|
||||
.filter(t => t.length > 0 && t !== '、' && t !== ',')
|
||||
}
|
||||
|
||||
function cleanText(s: string): string {
|
||||
return s.replace(/\*\*/g, '').trim()
|
||||
}
|
||||
|
||||
function formatDate(timestamp?: number) {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user