import { useCallback, useEffect, useMemo, useState } from "react"; import { Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Tag, Typography, message, } from "antd"; import type { ColumnsType } from "antd/es/table"; import { PlusOutlined, ReloadOutlined } from "@ant-design/icons"; import EllipsisCell from "./EllipsisCell"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/demand/api/v1"; const getResolvedApiBaseUrl = () => { if (API_BASE_URL.startsWith("http://") || API_BASE_URL.startsWith("https://")) { return API_BASE_URL; } return new URL(API_BASE_URL, window.location.origin).toString(); }; type StrategyConfigItem = { strategy_id: string; name: string; version: string; params: Record; active: boolean; daily_write_limit: number; priority: number; registered: boolean; create_time: string | null; updated_time: string | null; }; type StrategyConfigResponse = { items: StrategyConfigItem[]; }; type AvailableStrategyItem = { strategy_id: string; name: string; version: string; }; type AvailableStrategyResponse = { items: AvailableStrategyItem[]; }; function formatParamsPreview(params: Record): string { const text = JSON.stringify(params ?? {}, null, 0); if (!text || text === "{}") { return "-"; } return text.length > 80 ? `${text.slice(0, 80)}…` : text; } function formatExperimentNumber(value: number | null | undefined): string { if (value === undefined || value === null) { return "-"; } return String(value); } const PARAMS_HINT = "策略运行阈值等参数,JSON 格式"; const DAILY_LIMIT_HINT = "0 表示不限制,直到 staging 全部写入"; const PRIORITY_HINT = "数值越小同批次越先选取;同名需求一旦写入 Hive,其他 priority 不可再写,仅同 priority 可重复"; function parseParamsJson(raw: string): Record { const trimmed = raw.trim(); if (!trimmed) { return {}; } const parsed: unknown = JSON.parse(trimmed); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("params 必须是 JSON 对象"); } return parsed as Record; } async function readErrorDetail(response: Response): Promise { const text = await response.text(); if (!text) { return `HTTP ${response.status}`; } try { const payload = JSON.parse(text) as { detail?: unknown }; if (typeof payload.detail === "string") { return payload.detail; } } catch { /* 非 JSON 响应 */ } return text; } function compareStrategyConfigs(a: StrategyConfigItem, b: StrategyConfigItem): number { if (a.active !== b.active) { return a.active ? -1 : 1; } const priorityA = a.priority ?? 0; const priorityB = b.priority ?? 0; if (priorityA !== priorityB) { return priorityA - priorityB; } return a.strategy_id.localeCompare(b.strategy_id); } export default function StrategyConfigApp() { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [items, setItems] = useState([]); const [availableItems, setAvailableItems] = useState([]); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [createForm] = Form.useForm(); const [editForm] = Form.useForm(); const [submitting, setSubmitting] = useState(false); const [togglingId, setTogglingId] = useState(null); const fetchConfigs = useCallback(async () => { setLoading(true); setError(""); try { const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL("strategy-configs", baseWithSlash); const response = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(await readErrorDetail(response)); } const payload = (await response.json()) as StrategyConfigResponse; setItems(payload.items ?? []); } catch (queryError) { setError( queryError instanceof Error ? queryError.message : "加载策略配置失败", ); } finally { setLoading(false); } }, []); const fetchAvailable = useCallback(async () => { try { const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL("strategy-configs/available", baseWithSlash); const response = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(await readErrorDetail(response)); } const payload = (await response.json()) as AvailableStrategyResponse; setAvailableItems(payload.items ?? []); } catch (queryError) { message.error( queryError instanceof Error ? queryError.message : "加载可添加策略失败", ); } }, []); useEffect(() => { void fetchConfigs(); }, [fetchConfigs]); const sortedItems = useMemo( () => [...items].sort(compareStrategyConfigs), [items], ); const openCreateModal = () => { createForm.resetFields(); createForm.setFieldsValue({ paramsText: "{}", daily_write_limit: 0, priority: 0, active: false, }); setCreateOpen(true); void fetchAvailable(); }; const openEditModal = (item: StrategyConfigItem) => { setEditingItem(item); editForm.setFieldsValue({ paramsText: JSON.stringify(item.params ?? {}, null, 2), daily_write_limit: item.daily_write_limit ?? 0, priority: item.priority ?? 0, active: item.active, }); setEditOpen(true); }; const handleCreate = async () => { try { const values = await createForm.validateFields(); setSubmitting(true); const params = parseParamsJson(String(values.paramsText ?? "")); const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL("strategy-configs", baseWithSlash); const response = await fetch(url.toString(), { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ strategy_id: values.strategy_id, params, daily_write_limit: Number(values.daily_write_limit ?? 0), priority: Number(values.priority ?? 0), active: Boolean(values.active), }), }); if (!response.ok) { throw new Error(await readErrorDetail(response)); } message.success("策略配置已添加"); setCreateOpen(false); await fetchConfigs(); } catch (submitError) { if (submitError instanceof Error) { message.error(submitError.message); } } finally { setSubmitting(false); } }; const handleEdit = async () => { if (!editingItem) { return; } try { const values = await editForm.validateFields(); setSubmitting(true); const params = parseParamsJson(String(values.paramsText ?? "")); const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL(`strategy-configs/${encodeURIComponent(editingItem.strategy_id)}`, baseWithSlash); const response = await fetch(url.toString(), { method: "PUT", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ params, daily_write_limit: Number(values.daily_write_limit ?? 0), priority: Number(values.priority ?? 0), active: Boolean(values.active), }), }); if (!response.ok) { throw new Error(await readErrorDetail(response)); } message.success("策略配置已更新"); setEditOpen(false); setEditingItem(null); await fetchConfigs(); } catch (submitError) { if (submitError instanceof Error) { message.error(submitError.message); } } finally { setSubmitting(false); } }; const handleToggleActive = async (item: StrategyConfigItem) => { setTogglingId(item.strategy_id); try { const resolvedBase = getResolvedApiBaseUrl(); const baseWithSlash = resolvedBase.endsWith("/") ? resolvedBase : `${resolvedBase}/`; const url = new URL( `strategy-configs/${encodeURIComponent(item.strategy_id)}/active`, baseWithSlash, ); const response = await fetch(url.toString(), { method: "PATCH", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ active: !item.active }), }); if (!response.ok) { throw new Error(await readErrorDetail(response)); } message.success(item.active ? "策略已暂停" : "策略已开始"); await fetchConfigs(); } catch (toggleError) { message.error( toggleError instanceof Error ? toggleError.message : "操作失败", ); } finally { setTogglingId(null); } }; const columns: ColumnsType = useMemo( () => [ { title: "策略 ID", dataIndex: "strategy_id", width: 220, render: (value) => , }, { title: "策略名称", dataIndex: "name", width: 160, render: (value) => value ?? "-", }, { title: "版本", dataIndex: "version", width: 100, render: (value) => value ?? "-", }, { title: "状态", dataIndex: "active", width: 100, render: (active: boolean) => ( {active ? "运行中" : "已暂停"} ), }, { title: "代码注册", dataIndex: "registered", width: 110, render: (registered: boolean) => ( {registered ? "已注册" : "未注册"} ), }, { title: "日写入上限", dataIndex: "daily_write_limit", width: 110, render: (value: number) => formatExperimentNumber(value), }, { title: "优先级", dataIndex: "priority", width: 90, render: (value: number) => formatExperimentNumber(value), }, { title: "运行参数", dataIndex: "params", render: (params: Record) => ( ), }, { title: "更新时间", dataIndex: "updated_time", width: 180, render: (value) => value ?? "-", }, { title: "操作", key: "actions", width: 220, fixed: "right", render: (_, record) => ( ), }, ], [togglingId], ); return (
需求策略配置
策略管理 参数配置 启停控制 实验写入 ODPS
{error ? ( ) : null} rowKey="strategy_id" loading={loading} columns={columns} dataSource={sortedItems} pagination={false} scroll={{ x: 1200 }} locale={{ emptyText: "暂无策略配置,点击「添加策略」创建" }} />
setCreateOpen(false)} onOk={() => void handleCreate()} confirmLoading={submitting} okText="添加" cancelText="取消" destroyOnClose width={640} >