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[]
}