/** * 维客线索管理页 — 客户搜索 + 线索列表 + 编辑/删除/隐藏。 * * - 客户搜索栏(Input.Search),支持姓名模糊/手机号精确搜索 * - 门店筛选器(复用 SiteSelector + useSiteFilter) * - 搜索结果列表:member_id、姓名、手机号脱敏、所属门店 * - 搜索结果为空时展示"未找到匹配客户"提示(Ant Design Empty) * - 选择客户后展示线索列表,支持来源/隐藏状态筛选 * - 线索操作:编辑、删除(二次确认)、隐藏/显示(Switch) * - API:GET /customers/search, GET /customers/{member_id}/clues, * PATCH /clues/{id}, DELETE /clues/{id}, PATCH /clues/{id}/visibility */ import React, { useState, useCallback } from "react"; import { Input, Table, Select, Switch, Button, Space, Tag, Empty, Modal, message, Card, } from "antd"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { tenantApi } from "@/services/api"; import SiteSelector, { useSiteFilter } from "@/components/SiteSelector"; import ClueEditor, { CATEGORY_LABEL } from "@/components/ClueEditor"; import type { ClueData, ClueFormValues } from "@/components/ClueEditor"; import type { AxiosError } from "axios"; /* ------------------------------------------------------------------ */ /* 类型定义 */ /* ------------------------------------------------------------------ */ interface CustomerItem { memberId: number; nickname: string | null; mobileMasked: string | null; siteId: number; } interface ClueItem { id: number; category: string; summary: string; detail: string | null; recordedByName: string | null; source: string; recordedAt: string | null; isHidden: boolean; } /* ------------------------------------------------------------------ */ /* 常量 */ /* ------------------------------------------------------------------ */ const SOURCE_OPTIONS = [ { label: "全部来源", value: "" }, { label: "手动录入", value: "manual" }, { label: "AI-消费分析", value: "ai_consumption" }, { label: "AI-笔记", value: "ai_note" }, ]; const SOURCE_LABEL: Record = { manual: "手动录入", ai_consumption: "AI-消费分析", ai_note: "AI-笔记", }; const HIDDEN_OPTIONS = [ { label: "全部状态", value: "" }, { label: "已隐藏", value: "true" }, { label: "未隐藏", value: "false" }, ]; /* ------------------------------------------------------------------ */ /* 主页面组件 */ /* ------------------------------------------------------------------ */ const RetentionClues: React.FC = () => { const { selectedSiteIds, setSelectedSiteIds, allSiteIds } = useSiteFilter(); // 客户搜索状态 const [customers, setCustomers] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [searched, setSearched] = useState(false); // 选中客户 + 线索列表状态 const [selectedCustomer, setSelectedCustomer] = useState(null); const [clues, setClues] = useState([]); const [cluesLoading, setCluesLoading] = useState(false); const [sourceFilter, setSourceFilter] = useState(""); const [hiddenFilter, setHiddenFilter] = useState(""); // 编辑 Modal 状态 const [editorVisible, setEditorVisible] = useState(false); const [editingClue, setEditingClue] = useState(null); /* ---------------------------------------------------------------- */ /* 客户搜索 */ /* ---------------------------------------------------------------- */ const handleSearch = useCallback(async (keyword: string) => { const trimmed = keyword.trim(); if (!trimmed) { message.warning("请输入搜索关键词"); return; } setSearchLoading(true); setSearched(true); setSelectedCustomer(null); setClues([]); try { const params: Record = { keyword: trimmed }; // 如果选了特定门店,传第一个 site_id(搜索接口支持单门店筛选) if (selectedSiteIds.length === 1) { params.site_id = selectedSiteIds[0]; } const res = await tenantApi.get("/customers/search", { params }); setCustomers(res.data); } catch { message.error("搜索客户失败"); setCustomers([]); } finally { setSearchLoading(false); } }, [selectedSiteIds]); /* ---------------------------------------------------------------- */ /* 线索列表 */ /* ---------------------------------------------------------------- */ const fetchClues = useCallback(async (memberId: number) => { setCluesLoading(true); try { const params: Record = {}; if (sourceFilter) params.source = sourceFilter; if (hiddenFilter) params.is_hidden = hiddenFilter; const res = await tenantApi.get(`/customers/${memberId}/clues`, { params }); setClues(res.data); } catch { message.error("获取线索列表失败"); setClues([]); } finally { setCluesLoading(false); } }, [sourceFilter, hiddenFilter]); const handleSelectCustomer = useCallback((customer: CustomerItem) => { setSelectedCustomer(customer); fetchClues(customer.memberId); }, [fetchClues]); // 筛选条件变化时重新加载线索 React.useEffect(() => { if (selectedCustomer) { fetchClues(selectedCustomer.memberId); } }, [sourceFilter, hiddenFilter]); // eslint-disable-line react-hooks/exhaustive-deps /* ---------------------------------------------------------------- */ /* 线索操作 */ /* ---------------------------------------------------------------- */ /** 编辑线索 */ const handleEdit = (clue: ClueItem) => { setEditingClue({ id: clue.id, category: clue.category, summary: clue.summary, detail: clue.detail, }); setEditorVisible(true); }; const handleEditSubmit = async (values: ClueFormValues) => { if (!editingClue || !selectedCustomer) return; try { await tenantApi.patch(`/clues/${editingClue.id}`, values); message.success("线索更新成功"); setEditorVisible(false); setEditingClue(null); fetchClues(selectedCustomer.memberId); } catch (err) { const axiosErr = err as AxiosError<{ message?: string }>; message.error(axiosErr.response?.data?.message ?? "更新失败"); } }; /** 删除线索 — 二次确认 */ const handleDelete = (clue: ClueItem) => { Modal.confirm({ title: "确认删除", icon: , content: `确定要删除该线索吗?删除后不可恢复。`, okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { try { await tenantApi.delete(`/clues/${clue.id}`); message.success("线索已删除"); if (selectedCustomer) fetchClues(selectedCustomer.memberId); } catch (err) { const axiosErr = err as AxiosError<{ message?: string }>; message.error(axiosErr.response?.data?.message ?? "删除失败"); } }, }); }; /** 隐藏/显示切换 */ const handleToggleVisibility = async (clue: ClueItem, isHidden: boolean) => { try { await tenantApi.patch(`/clues/${clue.id}/visibility`, { isHidden }); message.success(isHidden ? "线索已隐藏" : "线索已显示"); if (selectedCustomer) fetchClues(selectedCustomer.memberId); } catch (err) { const axiosErr = err as AxiosError<{ message?: string }>; message.error(axiosErr.response?.data?.message ?? "操作失败"); } }; /* ---------------------------------------------------------------- */ /* 客户搜索结果列定义 */ /* ---------------------------------------------------------------- */ const customerColumns: ColumnsType = [ { title: "会员 ID", dataIndex: "memberId", key: "memberId", width: 100 }, { title: "姓名", dataIndex: "nickname", key: "nickname", render: (v) => v ?? "-" }, { title: "手机号", dataIndex: "mobileMasked", key: "mobileMasked", render: (v) => v ?? "-" }, { title: "所属门店", dataIndex: "siteId", key: "siteId", render: (v: number) => `门店 ${v}` }, { title: "操作", key: "action", width: 100, render: (_, record) => ( ), }, ]; /* ---------------------------------------------------------------- */ /* 线索列表列定义 */ /* ---------------------------------------------------------------- */ const clueColumns: ColumnsType = [ { title: "大类", dataIndex: "category", key: "category", width: 110, render: (v: string) => CATEGORY_LABEL[v] ?? v, }, { title: "摘要", dataIndex: "summary", key: "summary", ellipsis: true, }, { title: "来源", dataIndex: "source", key: "source", width: 120, render: (v: string) => { const label = SOURCE_LABEL[v] ?? v; const color = v === "manual" ? "blue" : "purple"; return {label}; }, }, { title: "提供人", dataIndex: "recordedByName", key: "recordedByName", width: 100, render: (v) => v ?? "-", }, { title: "记录时间", dataIndex: "recordedAt", key: "recordedAt", width: 170, render: (v) => v ?? "-", }, { title: "隐藏", dataIndex: "isHidden", key: "isHidden", width: 80, render: (isHidden: boolean, record) => ( handleToggleVisibility(record, checked)} /> ), }, { title: "操作", key: "action", width: 120, render: (_, record) => ( ), }, ]; /* ---------------------------------------------------------------- */ /* 渲染 */ /* ---------------------------------------------------------------- */ return ( {/* 搜索栏 + 门店筛选 */} {/* 搜索结果 */} {searched && ( {customers.length === 0 ? ( ) : ( rowKey="memberId" columns={customerColumns} dataSource={customers} loading={searchLoading} pagination={false} size="small" /> )} )} {/* 线索列表 */} {selectedCustomer && ( } > rowKey="id" columns={clueColumns} dataSource={clues} loading={cluesLoading} pagination={{ pageSize: 10 }} size="small" rowClassName={(record) => record.isHidden ? "clue-row-hidden" : "" } /> )} {/* 线索编辑 Modal */} { setEditorVisible(false); setEditingClue(null); }} /> ); }; export default RetentionClues;