/** * P18 任务引擎参数管理页面。 * * 展示 biz.cfg_task_generator_params 全局默认 + 门店覆盖参数。 * 超级管理员可编辑/新增/删除;门店管理员只读。 * 权重参数(w_rs/w_ms/w_ml)以卡片形式整体编辑,后端联合校验。 */ import React, { useEffect, useState, useCallback } from "react"; import { Table, Card, Typography, Button, Space, Tag, InputNumber, Modal, Select, Popconfirm, Tooltip, message, } from "antd"; import { ReloadOutlined, SettingOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined, } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import dayjs from "dayjs"; import { fetchConfigParams, updateConfigParam, createConfigParam, deleteConfigParam, type ConfigParam, } from "../api/taskEngine"; import { useAuthStore } from "../store/authStore"; const { Title, Text } = Typography; /** 参数中文描述映射 */ const PARAM_LABELS: Record = { high_priority_recall_threshold: "高优先召回阈值", priority_recall_threshold: "优先召回阈值", rs_min_for_relationship: "关系构建 RS 下限", rs_max_for_relationship: "关系构建 RS 上限", consecutive_recall_fail_cycles: "连续失败触发转移轮数", min_wbi_for_transfer: "触发转移最低 WBI", guard_assistant_coverage_ratio: "助教绑定率保护阈值", guard_new_assistant_days: "新助教入驻保护天数", transfer_score_w_rs: "转移排序 RS 权重", transfer_score_w_ms: "转移排序 MS 权重", transfer_score_w_ml: "转移排序 ML 权重", max_transfer_count: "单客户最大转移次数", follow_up_visit_retention_hours: "回访任务保留时长(h)", }; const WEIGHT_KEYS = ["transfer_score_w_rs", "transfer_score_w_ms", "transfer_score_w_ml"]; const TaskEngineConfig: React.FC = () => { const user = useAuthStore((s) => s.user); const isSuperAdmin = user?.roles?.includes("super_admin") ?? false; const [params, setParams] = useState([]); const [loading, setLoading] = useState(false); // 行内编辑 const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(0); const [saving, setSaving] = useState(false); // 新增弹窗 const [addVisible, setAddVisible] = useState(false); const [addSiteId, setAddSiteId] = useState(null); const [addKey, setAddKey] = useState(""); const [addValue, setAddValue] = useState(0); const [adding, setAdding] = useState(false); // 权重卡片编辑 const [weightVisible, setWeightVisible] = useState(false); const [weightSiteId, setWeightSiteId] = useState(null); const [wRs, setWRs] = useState(0.5); const [wMs, setWMs] = useState(0.3); const [wMl, setWMl] = useState(0.2); const [weightSaving, setWeightSaving] = useState(false); const load = useCallback(async () => { setLoading(true); try { const data = await fetchConfigParams(); setParams(data.params); } catch { message.error("加载参数配置失败"); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const handleSave = async () => { if (editingId == null) return; setSaving(true); try { await updateConfigParam(editingId, editValue); message.success("参数已更新"); setEditingId(null); load(); } catch (err: unknown) { const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail; message.error(msg || "更新失败"); } finally { setSaving(false); } }; const handleAdd = async () => { if (!addSiteId || !addKey) return; setAdding(true); try { await createConfigParam(addSiteId, addKey, addValue); message.success("门店覆盖参数已添加"); setAddVisible(false); setAddKey(""); setAddValue(0); load(); } catch (err: unknown) { const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail; message.error(msg || "添加失败"); } finally { setAdding(false); } }; const handleDelete = async (paramId: number) => { try { await deleteConfigParam(paramId); message.success("门店覆盖已删除"); load(); } catch (err: unknown) { const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail; message.error(msg || "删除失败"); } }; /** 打开权重卡片编辑弹窗 */ const openWeightEditor = (siteId: number | null) => { const siteParams = params.filter( (p) => p.site_id === siteId && WEIGHT_KEYS.includes(p.param_key), ); const findVal = (key: string) => siteParams.find((p) => p.param_key === key)?.param_value ?? 0; setWeightSiteId(siteId); setWRs(findVal("transfer_score_w_rs")); setWMs(findVal("transfer_score_w_ms")); setWMl(findVal("transfer_score_w_ml")); setWeightVisible(true); }; const handleWeightSave = async () => { const sum = wRs + wMs + wMl; if (Math.abs(sum - 1.0) > 0.001) { message.error(`权重之和必须为 1.0,当前为 ${sum.toFixed(4)}`); return; } setWeightSaving(true); try { // 逐个更新三个权重参数 const weightParams = params.filter( (p) => p.site_id === weightSiteId && WEIGHT_KEYS.includes(p.param_key), ); const valMap: Record = { transfer_score_w_rs: wRs, transfer_score_w_ms: wMs, transfer_score_w_ml: wMl, }; for (const wp of weightParams) { await updateConfigParam(wp.id, valMap[wp.param_key]); } message.success("权重配置已更新"); setWeightVisible(false); load(); } catch (err: unknown) { const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail; message.error(msg || "权重更新失败"); } finally { setWeightSaving(false); } }; const columns: ColumnsType = [ { title: "参数", dataIndex: "param_key", key: "param_key", width: 220, render: (v: string) => ( {PARAM_LABELS[v] || v}
{v}
), }, { title: "门店", key: "site", width: 120, render: (_: unknown, r) => r.site_id == null ? 全局默认 : {r.site_name || `#${r.site_id}`}, }, { title: "参数值", key: "value", width: 160, render: (_: unknown, r) => { if (editingId === r.id) { return ( v != null && setEditValue(v)} step={WEIGHT_KEYS.includes(r.param_key) ? 0.01 : 1} /> ); } return {r.param_value}; }, }, { title: "说明", dataIndex: "description", key: "desc", width: 200, render: (v: string | null) => v || "—", }, { title: "更新时间", dataIndex: "updated_at", key: "updated_at", width: 160, render: (v: string) => dayjs(v).format("YYYY-MM-DD HH:mm"), }, ]; if (isSuperAdmin) { columns.push({ title: "操作", key: "action", width: 160, fixed: "right", render: (_: unknown, r) => { // 权重参数用卡片编辑 if (WEIGHT_KEYS.includes(r.param_key)) { return ( ); } return ( {r.site_id != null && ( handleDelete(r.id)}> )} ); }, }); } return (
<SettingOutlined style={{ marginRight: 8 }} /> 任务引擎参数管理 {isSuperAdmin && ( )}
rowKey="id" columns={columns} dataSource={params} loading={loading} size="small" scroll={{ x: 1000 }} pagination={false} /> {/* 新增门店覆盖弹窗 */} setAddVisible(false)} confirmLoading={adding} okButtonProps={{ disabled: !addSiteId || !addKey }} >
门店 ID: setAddSiteId(v)} />
参数名: