新增客户标签库:管理员维护、员工选择

租户级标签 CRUD 与颜色预设;客户打标仅能从库中选择,设置页提供管理入口,列表筛选随标签库动态生成。
This commit is contained in:
yml2213
2026-07-17 22:28:16 +08:00
parent c5f28dca1c
commit 7a08f5f729
11 changed files with 723 additions and 23 deletions
+59 -19
View File
@@ -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>