1. 新增客户联系方式查询客户每日总结的完整功能,包括前端页面、后端接口和数据库字段支持 2. 优化原有每日总结页面,新增绑定客户联系方式功能和无总结时的聊天记录兜底展示 3. 修复消息查询时的to_user数组匹配问题,新增客户微信号存储字段 4. 补充批量修复客户昵称的脚本和部署文档更新
297 lines
9.4 KiB
TypeScript
297 lines
9.4 KiB
TypeScript
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} />
|
||
</>
|
||
)
|
||
}
|