diff --git a/server/internal/handler/contact_extract.go b/server/internal/handler/contact_extract.go index 226d357..e5bbbfd 100644 --- a/server/internal/handler/contact_extract.go +++ b/server/internal/handler/contact_extract.go @@ -137,6 +137,14 @@ func mergeCustomerContacts(tenantID, customerID uint, contacts []extractedContac updates["email"] = c.Value customer.Email = c.Value } + if c.Kind == "wechat" && strings.TrimSpace(customer.Wechat) == "" && updates["wechat"] == nil { + updates["wechat"] = c.Value + customer.Wechat = c.Value + } + if c.Kind == "qq" && strings.TrimSpace(customer.QQ) == "" && updates["qq"] == nil { + updates["qq"] = c.Value + customer.QQ = c.Value + } } if len(updates) > 0 { model.DB.Model(&model.Customer{}).Where("id = ?", customerID).Updates(updates) diff --git a/server/internal/handler/customer.go b/server/internal/handler/customer.go index 3d0e9c2..3ae20f3 100644 --- a/server/internal/handler/customer.go +++ b/server/internal/handler/customer.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "strings" "github.com/gin-gonic/gin" "kefu-cloud/server/internal/middleware" @@ -135,12 +136,23 @@ func (h *CustomerHandler) Update(c *gin.Context) { return } - allowed := map[string]bool{"name": true, "phone": true, "email": true, "tags": true, "source": true, "status": true} + allowed := map[string]bool{ + "name": true, "phone": true, "email": true, "wechat": true, "qq": true, + "tags": true, "source": true, "status": true, + } for key := range updates { if !allowed[key] { delete(updates, key) } } + // 字符串字段统一 trim + for _, k := range []string{"name", "phone", "email", "wechat", "qq", "source", "status"} { + if v, ok := updates[k]; ok { + if s, ok := v.(string); ok { + updates[k] = strings.TrimSpace(s) + } + } + } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "没有可更新字段"}) return diff --git a/server/internal/model/models.go b/server/internal/model/models.go index af44fd5..f8798a2 100644 --- a/server/internal/model/models.go +++ b/server/internal/model/models.go @@ -52,6 +52,8 @@ type Customer struct { Name string `gorm:"size:50;not null" json:"name"` Phone string `gorm:"size:20" json:"phone"` Email string `gorm:"size:100" json:"email"` + Wechat string `gorm:"size:50" json:"wechat"` // 微信号,默认可空 + QQ string `gorm:"size:20;column:qq" json:"qq"` Tags string `gorm:"type:text" json:"tags"` Source string `gorm:"size:30" json:"source"` Status string `gorm:"size:20;default:online" json:"status"` diff --git a/web/src/pages/agent/Customers.tsx b/web/src/pages/agent/Customers.tsx index 9858ef3..9ddea76 100644 --- a/web/src/pages/agent/Customers.tsx +++ b/web/src/pages/agent/Customers.tsx @@ -232,6 +232,8 @@ const Customers = () => { name: customer.name, phone: customer.phone, email: customer.email, + wechat: customer.wechat || '', + qq: customer.qq || '', source: customer.source, status: customer.status, tags: parseTags(customer.tags), @@ -240,7 +242,8 @@ const Customers = () => { } const handleSave = async (values: { - name: string; phone?: string; email?: string; source?: string; status?: string; tags?: string[] + name: string; phone?: string; email?: string; wechat?: string; qq?: string + source?: string; status?: string; tags?: string[] }) => { setSaving(true) try { @@ -248,6 +251,8 @@ const Customers = () => { name: values.name.trim(), phone: values.phone?.trim() || '', email: values.email?.trim() || '', + wechat: values.wechat?.trim() || '', + qq: values.qq?.trim() || '', source: values.source?.trim() || '手动录入', status: values.status || 'offline', tags: JSON.stringify(values.tags || []), @@ -572,6 +577,18 @@ const Customers = () => { {selectedCustomer.email || '—'} +
+ + + {selectedCustomer.wechat || '—'} + +
+
+ Q + + {selectedCustomer.qq || '—'} + +
@@ -692,6 +709,14 @@ const Customers = () => { +
+ + + + + + +
diff --git a/web/src/pages/agent/Dashboard.tsx b/web/src/pages/agent/Dashboard.tsx index 88fd597..d95902c 100644 --- a/web/src/pages/agent/Dashboard.tsx +++ b/web/src/pages/agent/Dashboard.tsx @@ -12,11 +12,95 @@ import { useAuth } from '@/stores/auth' import { addSessionNote, claimSession, endSession, getAvailableAgents, getCustomer, getCustomers, getKnowledgeEntries, getQuickReplies, getSession, getSessionMessages, getSessions, markSessionRead, sendSessionMessage, - suggestQuickReplies, transferSession, updateSessionPriority, uploadImage, useQuickReply, + suggestQuickReplies, transferSession, updateCustomer, updateSessionPriority, uploadImage, useQuickReply, type AvailableAgent, type Customer, type CustomerContact, type KnowledgeEntry, type Message, type QuickReply, type Session, type SessionEvent, type VisitorPageView, } from '@/services/api' +type CustomerEditableField = 'phone' | 'email' | 'wechat' | 'qq' + +/** 客户侧栏基本信息:点击输入,失焦/回车保存 */ +function CustomerInfoField({ + label, + value, + placeholder, + disabled, + saving, + onSave, +}: { + label: string + value: string + placeholder?: string + disabled?: boolean + saving?: boolean + onSave: (next: string) => Promise | void +}) { + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState(value) + const inputRef = useRef(null) + + useEffect(() => { + if (!editing) setDraft(value) + }, [value, editing]) + + useEffect(() => { + if (editing) { + requestAnimationFrame(() => { + inputRef.current?.focus() + inputRef.current?.select() + }) + } + }, [editing]) + + const commit = async () => { + const next = draft.trim() + setEditing(false) + if (next === (value || '').trim()) return + await onSave(next) + } + + return ( +
+ {label} + {editing ? ( + setDraft(e.target.value)} + onBlur={() => { void commit() }} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault() + void commit() + } + if (e.key === 'Escape') { + setDraft(value) + setEditing(false) + } + }} + /> + ) : ( + + )} +
+ ) +} + /** 按 id 合并消息,再按 seq / id 排序 */ function mergeMessagesBySeq(existing: Message[], incoming: Message[]): Message[] { const map = new Map() @@ -185,6 +269,7 @@ const Dashboard = () => { /** 访客输入框未发送草稿(实时) */ const [visitorDraft, setVisitorDraft] = useState('') const [customerContacts, setCustomerContacts] = useState([]) + const [savingCustomerField, setSavingCustomerField] = useState(null) /** 驱动在线读秒每秒刷新 */ const [clockTick, setClockTick] = useState(0) const initialLoad = useRef(true) @@ -714,6 +799,22 @@ const Dashboard = () => { return () => { cancelled = true } }, [selectedCustomer?.id, selectedId]) + const saveCustomerField = useCallback(async (customerId: number, field: CustomerEditableField, value: string) => { + setSavingCustomerField(field) + try { + const res = await updateCustomer(customerId, { [field]: value }) + const updated = res.data + setCustomers(prev => ({ + ...prev, + [customerId]: { ...prev[customerId], ...updated }, + })) + } catch (e) { + antMsg.error(e instanceof Error ? e.message : '保存失败') + } finally { + setSavingCustomerField(null) + } + }, []) + const filterActive = statusFilter !== 'all' || priorityFilter !== 'all' const filteredSessions = sessions .filter(session => { @@ -1493,29 +1594,52 @@ const Dashboard = () => {
基本信息 + 点击可编辑
-
-
- 手机 - {selectedCustomer.phone || '—'} -
-
- 邮箱 - {selectedCustomer.email || '—'} -
+
+ saveCustomerField(selectedCustomer.id, 'phone', v)} + /> + saveCustomerField(selectedCustomer.id, 'email', v)} + /> + saveCustomerField(selectedCustomer.id, 'wechat', v)} + /> + saveCustomerField(selectedCustomer.id, 'qq', v)} + /> {(() => { const kindLabel = (k: string) => k === 'phone' ? '手机' : k === 'wechat' ? '微信' : k === 'email' ? '邮箱' : k === 'qq' ? 'QQ' : k - // 主字段已展示的手机/邮箱不再重复;微信/QQ 等一律展示 + // 主字段已展示的联系方式不重复 const extra = customerContacts.filter(c => { if (c.kind === 'phone' && selectedCustomer.phone && c.value === selectedCustomer.phone) return false if (c.kind === 'email' && selectedCustomer.email && c.value === selectedCustomer.email) return false + if (c.kind === 'wechat' && selectedCustomer.wechat && c.value === selectedCustomer.wechat) return false + if (c.kind === 'qq' && selectedCustomer.qq && c.value === selectedCustomer.qq) return false return true }) if (extra.length === 0) return null return ( -
- 更多 +
+ 更多
{extra.map(c => ( @@ -1530,7 +1654,7 @@ const Dashboard = () => {
) })()} -
+
来源 {selectedCustomer.source || '—'}
diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 5bb0da9..bdb50ba 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -61,7 +61,10 @@ export interface SessionEvent { export interface AvailableAgent { id: number; nickname: string; status: string } export interface Customer { - id: number; tenant_id: number; name: string; phone: string; email: string; tags: string + id: number; tenant_id: number; name: string; phone: string; email: string + wechat?: string + qq?: string + tags: string source: string; status: string; conversation_count: number; last_contact_at: string contacts?: CustomerContact[] }