新增客户标签库:管理员维护、员工选择
租户级标签 CRUD 与颜色预设;客户打标仅能从库中选择,设置页提供管理入口,列表筛选随标签库动态生成。
This commit is contained in:
@@ -8,8 +8,8 @@ import {
|
||||
DeleteOutlined, ExportOutlined, CloseOutlined, GlobalOutlined, MessageOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
createCustomer, deleteCustomer, exportCustomersCSV, getCustomer, getCustomers, updateCustomer,
|
||||
type Customer, type Session,
|
||||
createCustomer, deleteCustomer, exportCustomersCSV, getCustomer, getCustomerTags, getCustomers, updateCustomer,
|
||||
type Customer, type CustomerTag, type Session,
|
||||
} from '@/services/api'
|
||||
import { useAuth } from '@/stores/auth'
|
||||
|
||||
@@ -19,23 +19,22 @@ const statusMap: Record<string, { color: string; text: string; dot: string }> =
|
||||
busy: { color: '#d97706', text: '忙碌', dot: '#d97706' },
|
||||
}
|
||||
|
||||
const tagOptions = ['VIP客户', '新客户', '活跃', '沉默', '企业客户']
|
||||
const filterChips = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'VIP客户', label: 'VIP' },
|
||||
{ key: '新客户', label: '新客户' },
|
||||
{ key: '活跃', label: '活跃' },
|
||||
{ key: '沉默', label: '沉默' },
|
||||
]
|
||||
|
||||
const tagStyle: Record<string, { bg: string; color: string }> = {
|
||||
const tagColorMap: Record<string, { bg: string; color: string }> = {
|
||||
amber: { bg: '#fef3c7', color: '#92400e' },
|
||||
green: { bg: '#f0fdf4', color: '#16a34a' },
|
||||
blue: { bg: '#dbeafe', color: '#2563eb' },
|
||||
cyan: { bg: '#ecfeff', color: '#0891b2' },
|
||||
violet: { bg: '#f3e8ff', color: '#7c3aed' },
|
||||
rose: { bg: '#fff1f2', color: '#e11d48' },
|
||||
orange: { bg: '#fffbeb', color: '#d97706' },
|
||||
slate: { bg: '#f1f5f9', color: '#475569' },
|
||||
// 兼容历史写死名称
|
||||
'VIP客户': { bg: '#fef3c7', color: '#92400e' },
|
||||
'VIP': { bg: '#fef3c7', color: '#92400e' },
|
||||
'新客户': { bg: '#f0fdf4', color: '#16a34a' },
|
||||
'活跃': { bg: '#dbeafe', color: '#2563eb' },
|
||||
'沉默': { bg: '#fffbeb', color: '#d97706' },
|
||||
'企业客户': { bg: '#ecfeff', color: '#0891b2' },
|
||||
'高价值': { bg: '#f1f5f9', color: '#475569' },
|
||||
}
|
||||
|
||||
/** 浅底深字头像色,对齐效果图 */
|
||||
@@ -107,8 +106,10 @@ function sessionTopic(s: Session) {
|
||||
return map[s.status] || `会话 #${s.id}`
|
||||
}
|
||||
|
||||
const TagPill = ({ tag }: { tag: string }) => {
|
||||
const s = tagStyle[tag] || { bg: '#f1f5f9', color: '#475569' }
|
||||
const TagPill = ({ tag, catalog }: { tag: string; catalog?: CustomerTag[] }) => {
|
||||
const meta = catalog?.find(t => t.name === tag)
|
||||
const byColor = meta?.color ? tagColorMap[meta.color] : undefined
|
||||
const s = byColor || tagColorMap[tag] || { bg: '#f1f5f9', color: '#475569' }
|
||||
const label = tag === 'VIP客户' ? 'VIP' : tag
|
||||
return (
|
||||
<span
|
||||
@@ -150,11 +151,34 @@ const Customers = () => {
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
const [tagCatalog, setTagCatalog] = useState<CustomerTag[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadCustomers()
|
||||
}, [page, pageSize, search])
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerTags()
|
||||
.then(res => setTagCatalog(Array.isArray(res.data) ? res.data : []))
|
||||
.catch(() => setTagCatalog([]))
|
||||
}, [])
|
||||
|
||||
const filterChips = useMemo(() => {
|
||||
const chips = [{ key: 'all', label: '全部' }]
|
||||
tagCatalog.forEach(t => {
|
||||
chips.push({
|
||||
key: t.name,
|
||||
label: t.name === 'VIP客户' ? 'VIP' : t.name,
|
||||
})
|
||||
})
|
||||
return chips
|
||||
}, [tagCatalog])
|
||||
|
||||
const tagSelectOptions = useMemo(
|
||||
() => tagCatalog.map(t => ({ value: t.name, label: t.name })),
|
||||
[tagCatalog],
|
||||
)
|
||||
|
||||
const loadCustomers = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -422,7 +446,7 @@ const Customers = () => {
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{parseTags(record.tags).length === 0
|
||||
? <span className="text-neutral-300 text-xs">—</span>
|
||||
: parseTags(record.tags).map(t => <TagPill key={t} tag={t} />)}
|
||||
: parseTags(record.tags).map(t => <TagPill key={t} tag={t} catalog={tagCatalog} />)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-3 text-[13px] text-neutral-500 whitespace-nowrap">
|
||||
@@ -528,7 +552,7 @@ const Customers = () => {
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{parseTags(selectedCustomer.tags).length === 0 ? (
|
||||
<span className="text-xs text-neutral-400">暂无标签</span>
|
||||
) : parseTags(selectedCustomer.tags).map(t => <TagPill key={t} tag={t} />)}
|
||||
) : parseTags(selectedCustomer.tags).map(t => <TagPill key={t} tag={t} catalog={tagCatalog} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -671,8 +695,24 @@ const Customers = () => {
|
||||
<Form.Item name="source" label="来源渠道">
|
||||
<Input maxLength={30} placeholder="如:官网咨询、微信、APP" />
|
||||
</Form.Item>
|
||||
<Form.Item name="tags" label="标签">
|
||||
<Select mode="tags" maxCount={10} options={tagOptions.map(t => ({ value: t, label: t }))} placeholder="选择或输入标签" />
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label="标签"
|
||||
extra={
|
||||
tagSelectOptions.length === 0
|
||||
? '暂无标签库,请管理员在「系统设置 → 客户标签」中创建'
|
||||
: '仅可从标签库中选择(由管理员维护)'
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
maxCount={10}
|
||||
allowClear
|
||||
options={tagSelectOptions}
|
||||
placeholder={tagSelectOptions.length === 0 ? '请先配置标签库' : '选择标签'}
|
||||
disabled={tagSelectOptions.length === 0}
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user