agent_9art/frontend/sidebar/src/SummarySearchApp.tsx
jqb 6a13b47328 feat: 新增运营端客户总结查询功能,优化现有总结页面
1.  新增客户联系方式查询客户每日总结的完整功能,包括前端页面、后端接口和数据库字段支持
2.  优化原有每日总结页面,新增绑定客户联系方式功能和无总结时的聊天记录兜底展示
3.  修复消息查询时的to_user数组匹配问题,新增客户微信号存储字段
4.  补充批量修复客户昵称的脚本和部署文档更新
2026-08-04 09:49:10 +08:00

297 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import { SpinLoading, Empty, Toast, SearchBar } from 'antd-mobile'
import { SummaryCard, SummaryItem } from './components/SummaryCard'
import { ChatMessageList, ChatMessage } from './components/ChatMessageList'
import { noCacheFetch } from './services/http'
const CORPID = 'wwd483c2fba24ae30a'
const AGENTID = '1000021'
const REDIRECT_URI = encodeURIComponent('https://ai.9artedu.com/sidebar/summary-search.html')
interface CustomerSummaryGroup {
customerId?: string
customerName?: string
phone?: string
wechatId?: string
list?: SummaryItem[]
}
function SummarySearchApp() {
const [loading, setLoading] = useState(true)
const [searching, setSearching] = useState(false)
const [keyword, setKeyword] = useState('')
const [groups, setGroups] = useState<CustomerSummaryGroup[] | null>(null)
const [needLogin, setNeedLogin] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const init = async () => {
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
// 1. URL 中有 code → 先登录
if (code) {
const cleanUrl = window.location.pathname + window.location.hash
window.history.replaceState({}, document.title, cleanUrl)
const ok = await tryCodeLogin(code)
if (!ok) {
setLoading(false)
setNeedLogin(true)
setError('登录失败,请重试')
return
}
}
// 2. 检查员工登录态
const staffId = urlParams.get('staffId') || localStorage.getItem('staff_user_id') || ''
if (!staffId) {
setLoading(false)
setNeedLogin(true)
setError('未获取到员工ID,请先登录')
return
}
setLoading(false)
}
init()
}, [])
const tryCodeLogin = async (code: string): Promise<boolean> => {
try {
const res = await noCacheFetch(`/api/v1/auth/login?code=${encodeURIComponent(code)}`, { method: 'POST' })
const data = await res.json()
if (data.code === 0 && data.data) {
const info = data.data
localStorage.setItem('staff_token', info.token)
localStorage.setItem('staff_user_id', info.userId)
localStorage.setItem('staff_user_name', info.userName || '')
localStorage.setItem('staff_avatar', info.avatar || '')
localStorage.setItem('staff_role', info.role || 'SEAT')
console.log('OAuth 登录成功:', info.userId, info.userName)
return true
}
console.error('OAuth login failed:', data.message)
} catch (e) {
console.error('Code login error:', e)
}
return false
}
const redirectToWeComOAuth = () => {
const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${CORPID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=snsapi_base&agentid=${AGENTID}&state=summarySearch#wechat_redirect`
console.log('跳转企微 OAuth:', oauthUrl)
window.location.href = oauthUrl
}
const doSearch = async () => {
const kw = keyword.trim()
if (!kw) {
Toast.show({ content: '请输入手机号或微信号', icon: 'fail' })
return
}
setSearching(true)
try {
const res = await noCacheFetch(`/api/v1/archive/daily-summary/by-contact?keyword=${encodeURIComponent(kw)}`)
const data = await res.json()
if (data.code === 0 && Array.isArray(data.data)) {
setGroups(data.data)
if (data.data.length === 0) {
Toast.show({ content: '未找到绑定该联系方式的客户', icon: 'fail' })
}
} else {
const msg = data.message || '查询失败'
Toast.show({ content: msg, icon: 'fail' })
}
} catch (e) {
console.error('查询每日总结失败:', e)
Toast.show({ content: '网络错误', icon: 'fail' })
} finally {
setSearching(false)
}
}
if (loading) {
return (
<div style={{ padding: 60, textAlign: 'center' }}>
<SpinLoading style={{ '--size': '32px' } as any} />
<div style={{ marginTop: 16, color: '#999', fontSize: 14 }}>加载中...</div>
</div>
)
}
if (needLogin) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh', padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 24 }}>{error || '未获取到员工ID,请先登录'}</div>
<button
onClick={redirectToWeComOAuth}
style={{
padding: '10px 32px',
background: '#1677ff',
color: '#fff',
border: 'none',
borderRadius: 6,
fontSize: 15,
cursor: 'pointer',
fontWeight: 500
}}
>
企微一键登录
</button>
</div>
)
}
const totalCount = (groups || []).reduce((sum, g) => sum + (g.list?.length || 0), 0)
// 昵称缺失(仍是企微外部联系人ID)时,显示 ID 尾号而不是整串 ID
const displayName = (g: CustomerSummaryGroup) => {
const name = (g.customerName || '').trim()
if (name && name !== g.customerId) return name
const id = g.customerId || ''
return id ? `客户 …${id.slice(-6)}` : '未命名客户'
}
return (
<div style={{ padding: 12, background: '#f5f5f5', minHeight: '100vh' }}>
<div style={{
background: '#fff',
borderRadius: 12,
padding: '14px 16px',
marginBottom: 14,
boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
}}>
<div style={{ fontSize: 17, fontWeight: 600, color: '#333', marginBottom: 12 }}>
🔍 客户总结查询
</div>
<SearchBar
value={keyword}
onChange={setKeyword}
placeholder="输入手机号或微信号"
onSearch={doSearch}
showCancelButton={() => false}
style={{ '--border-radius': '8px', padding: 0 } as any}
/>
<button
onClick={doSearch}
disabled={searching}
style={{
marginTop: 12,
width: '100%',
padding: '10px 0',
background: searching ? '#91caff' : '#1677ff',
color: '#fff',
border: 'none',
borderRadius: 8,
fontSize: 15,
fontWeight: 500,
cursor: searching ? 'default' : 'pointer'
}}
>
{searching ? '查询中...' : '查询'}
</button>
{groups !== null && groups.length > 0 && (
<div style={{ fontSize: 12, color: '#999', marginTop: 10 }}>
共 {groups.length} 个客户 · {totalCount} 条总结
</div>
)}
</div>
{groups !== null && groups.length === 0 && (
<Empty description="未找到绑定该联系方式的客户" style={{ padding: 40 }} />
)}
{(groups || []).map((group, gi) => (
<div key={group.customerId || gi}>
<div style={{
background: '#fff',
borderRadius: 12,
padding: '12px 16px',
marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ fontSize: 15, fontWeight: 600, color: '#333' }}>
👤 {displayName(group)}
</div>
<div style={{ fontSize: 12, color: '#999', textAlign: 'right' }}>
{group.phone && <div>📱 {group.phone}</div>}
{group.wechatId && <div>💬 {group.wechatId}</div>}
</div>
</div>
{(group.list || []).length === 0 ? (
<RecentChatFallback customerId={group.customerId || ''} />
) : (
(group.list || []).map((item, index) => (
<SummaryCard key={item.id || index} item={item} />
))
)}
</div>
))}
</div>
)
}
export default SummarySearchApp
/**
* 客户暂无每日总结时的兜底:加载最近 10 条单聊消息
*/
function RecentChatFallback({ customerId }: { customerId: string }) {
const [chatMessages, setChatMessages] = useState<ChatMessage[] | null>(null)
useEffect(() => {
if (!customerId) {
setChatMessages([])
return
}
const load = async () => {
try {
const res = await noCacheFetch(
`/api/v1/archive/messages/recent-chat?customerId=${encodeURIComponent(customerId)}&limit=10`
)
const data = await res.json()
setChatMessages(data.code === 0 && Array.isArray(data.data) ? data.data : [])
} catch (e) {
console.error('加载最近聊天记录失败:', e)
setChatMessages([])
}
}
load()
}, [customerId])
if (chatMessages === null) {
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<SpinLoading style={{ '--size': '20px' } as any} />
</div>
)
}
if (chatMessages.length === 0) {
return <Empty description="该客户暂无每日总结" style={{ padding: 24 }} />
}
return (
<>
<div style={{
background: '#fff',
borderRadius: 12,
padding: '10px 16px',
marginBottom: 12,
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
fontSize: 13,
color: '#999'
}}>
暂无每日总结,展示最近 {chatMessages.length} 条聊天
</div>
<ChatMessageList messages={chatMessages} />
</>
)
}