import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Card, Descriptions, Tag, Badge, Button, Space, Typography, Timeline, Input, message, Spin, Select, Row, Col, Modal, Form, } from 'antd' import { ArrowLeftOutlined, EditOutlined, LinkOutlined, TeamOutlined, UserSwitchOutlined, SyncOutlined, } from '@ant-design/icons' import { getMerchant, getMemberGroups, getMerchantNotes, addMerchantNote, updateFollowStatus, getLevelMap, getAssignableUsers, assignMerchant, updateMerchantClean, startTask, recheckMerchant, type MerchantClean, type GroupMember, type MerchantNote, type AssignableUser, } from '../api' import { useAppStore } from '../store' const { Text, Title } = Typography const { Option } = Select const defaultLevelColor: Record = { Hot: 'red', Warm: 'orange', Cold: 'blue' } const followStatusLabels: Record = { pending: '待跟进', contacted: '已联系', cooperating: '已合作', rejected: '已拒绝', } const followStatusBadge: Record = { pending: 'default', contacted: 'processing', cooperating: 'success', rejected: 'error', } const statusBadgeMap: Record = { valid: 'success', invalid: 'error', bot: 'warning', duplicate: 'default', } const sourceTypeColor: Record = { web: 'blue', tg_channel: 'orange', github: 'geekblue' } interface SourceInfo { source_type: string source_name: string source_url: string } function formatDateTime(dateStr: string) { return new Date(dateStr).toLocaleString('zh-CN') } export default function MerchantDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { isOperator, hasAction } = useAppStore() const [merchant, setMerchant] = useState(null) const [loading, setLoading] = useState(true) const [notes, setNotes] = useState([]) const [noteInput, setNoteInput] = useState('') const [noteLoading, setNoteLoading] = useState(false) const [groups, setGroups] = useState([]) const [levelMap, setLevelMap] = useState>({}) const [assignableUsers, setAssignableUsers] = useState([]) // Edit modal const [editModalOpen, setEditModalOpen] = useState(false) const [editForm] = Form.useForm() const [editLoading, setEditLoading] = useState(false) useEffect(() => { getLevelMap().then(r => setLevelMap(r.data)).catch(() => {}) getAssignableUsers().then(r => setAssignableUsers(r.data || [])).catch(() => {}) }, []) const loadMerchant = (merchantId: number) => { getMerchant(merchantId).then(res => { const d = res.data as { source: string; data: MerchantClean } setMerchant(d.data) if (d.data.tg_username) { getMemberGroups(d.data.tg_username).then(r => setGroups(r.data || [])).catch(() => {}) } getMerchantNotes(merchantId).then(r => setNotes(r.data || [])).catch(() => {}) }).catch(() => { message.error('商户不存在') navigate('/merchants') }) } useEffect(() => { if (!id) return setLoading(true) getMerchant(Number(id)).then(res => { const d = res.data as { source: string; data: MerchantClean } setMerchant(d.data) if (d.data.tg_username) { getMemberGroups(d.data.tg_username).then(r => setGroups(r.data || [])).catch(() => {}) } getMerchantNotes(Number(id)).then(r => setNotes(r.data || [])).catch(() => {}) }).catch(() => { message.error('商户不存在') navigate('/merchants') }).finally(() => setLoading(false)) }, [id, navigate]) const getLevelLabel = (key: string) => levelMap[key]?.label || key const getLevelColor = (key: string) => levelMap[key]?.color || defaultLevelColor[key] || 'default' const parseSources = (): SourceInfo[] => { if (!merchant) return [] try { if (Array.isArray(merchant.all_sources)) return merchant.all_sources as SourceInfo[] if (typeof merchant.all_sources === 'string') return JSON.parse(merchant.all_sources) } catch { /* ignore */ } return [] } const handleFollowStatusChange = async (val: string) => { if (!merchant) return try { await updateFollowStatus(merchant.id, val) setMerchant({ ...merchant, follow_status: val }) message.success('跟进状态已更新') } catch { message.error('更新失败') } } const handleAssign = async (val: string) => { if (!merchant) return try { const res = await assignMerchant(merchant.id, val) setMerchant(res.data) message.success('分配成功') } catch { message.error('分配失败') } } const handleAddNote = async () => { if (!merchant || !noteInput.trim()) return setNoteLoading(true) try { await addMerchantNote(merchant.id, noteInput.trim()) setNoteInput('') message.success('备注已添加') const res = await getMerchantNotes(merchant.id) setNotes(res.data || []) } catch { message.error('添加失败') } finally { setNoteLoading(false) } } const openEditModal = () => { if (!merchant) return editForm.setFieldsValue({ merchant_name: merchant.merchant_name, industry_tag: merchant.industry_tag, website: merchant.website, email: merchant.email, phone: merchant.phone, remark: merchant.remark || '', }) setEditModalOpen(true) } const handleEditSave = async () => { if (!merchant) return try { const values = await editForm.validateFields() setEditLoading(true) const res = await updateMerchantClean(merchant.id, values) setMerchant(res.data) message.success('商户信息已更新') setEditModalOpen(false) } catch { message.error('更新失败') } finally { setEditLoading(false) } } const handleCollectGroup = (username: string) => { Modal.confirm({ title: `采集群 @${username}`, content: `将使用 TG 采集器采集群 @${username} 中的成员和联系方式。`, okText: '开始采集', onOk: async () => { try { await startTask({ plugin_name: 'tg_collector', target_group: username }) message.success(`已启动对 @${username} 的采集任务`) } catch { message.error('启动采集失败') } }, }) } if (loading) return
if (!merchant) return null const sources = parseSources() return (
{/* Header */}
<a href={`https://t.me/${merchant.tg_username}`} target="_blank" rel="noreferrer"> @{merchant.tg_username} </a> {merchant.merchant_name && {merchant.merchant_name}} {getLevelLabel(merchant.level)} {hasAction('merchant_edit') && ( )} {hasAction('merchant_edit') && ( )}
{/* Left column */} {/* Basic info */} {merchant.tg_link || `https://t.me/${merchant.tg_username}`} {merchant.industry_tag || '-'} {merchant.website ? {merchant.website} : '-'} {merchant.email || '-'} {merchant.phone || '-'} {merchant.is_alive ? 存活 : 失效} {merchant.last_checked_at ? formatDateTime(merchant.last_checked_at) : '-'} {formatDateTime(merchant.created_at)} {formatDateTime(merchant.updated_at)} {merchant.remark && ( {merchant.remark} )} {/* Sources */} {sources.length === 0 ? 无来源记录 : sources.map((src, idx) => (
{src.source_type} {src.source_name || '未知来源'} {src.source_url && ( )}
))}
{/* Groups */} {groups.length > 0 && ( 所属群/频道 ({groups.length})} size="small" style={{ marginBottom: 16 }}> {groups.map((gm, idx) => (
@{gm.group_username} {gm.group_title && {gm.group_title}} {gm.source_type}
{hasAction('task_start') && ( )}
))}
)} {/* TG Preview */}