import { useCallback, useEffect, useState } from 'react'; import { Button, Card, Form, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Typography, message, } from 'antd'; import { API_ACCESS_ERROR_KINDS, API_ACCESS_ERROR_LABELS, API_ACCESS_PERCENT_FEATURES, API_ACCESS_PERCENT_FEATURE_LABELS, type ApiAccessActorHit, type ApiAccessErrorKind, type ApiAccessFormResponse, type ApiAccessPercentFeature, type ApiAccessPolicyDto, } from '@dukang/shared-types'; import { request } from '../lib/api'; const ERROR_OPTIONS = API_ACCESS_ERROR_KINDS.map((value) => ({ value, label: API_ACCESS_ERROR_LABELS[value], })); const FEATURE_OPTIONS = [ { value: '', label: '全部功能' }, ...API_ACCESS_PERCENT_FEATURES.map((value) => ({ value, label: API_ACCESS_PERCENT_FEATURE_LABELS[value], })), ]; type PercentDraft = { featureKey: ApiAccessPercentFeature; featureLabel: string; successPercent: number; errorKind: ApiAccessErrorKind; }; export default function ApiAccessPage() { const [loading, setLoading] = useState(false); const [savingGlobals, setSavingGlobals] = useState(false); const [savingNotifies, setSavingNotifies] = useState(false); const [percents, setPercents] = useState([]); const [overrides, setOverrides] = useState([]); const [notifies, setNotifies] = useState([]); const [open, setOpen] = useState(false); const load = useCallback(async () => { setLoading(true); try { const data = await request('/admin/api-access'); setPercents( data.percents.map((row) => ({ featureKey: row.featureKey as ApiAccessPercentFeature, featureLabel: row.featureLabel, successPercent: row.successPercent ?? 100, errorKind: row.errorKind ?? 'request_error', })), ); setOverrides(data.overrides); setNotifies(data.notifies); } catch (error) { message.error(error instanceof Error ? error.message : '加载失败'); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); async function saveGlobals() { setSavingGlobals(true); try { const data = await request('/admin/api-access/globals', { method: 'PUT', body: JSON.stringify({ items: percents.map((row) => ({ featureKey: row.featureKey, successPercent: row.successPercent, errorKind: row.errorKind, })), }), }); setOverrides(data.overrides); message.success('已保存成功百分比'); } catch (error) { message.error(error instanceof Error ? error.message : '保存失败'); } finally { setSavingGlobals(false); } } async function saveNotifies() { setSavingNotifies(true); try { await request('/admin/api-access/notifies', { method: 'PUT', body: JSON.stringify({ items: notifies.map((row) => ({ featureKey: row.featureKey, enabled: row.enabled })), }), }); message.success('已保存通知开关'); } catch (error) { message.error(error instanceof Error ? error.message : '保存失败'); } finally { setSavingNotifies(false); } } async function removeOverride(id: string) { try { const data = await request(`/admin/api-access/overrides/${id}`, { method: 'DELETE', }); setOverrides(data.overrides); message.success('已删除'); } catch (error) { message.error(error instanceof Error ? error.message : '删除失败'); } } return (
接口访问 成功百分比是放行比例,默认 100。0 为全部拒绝,每次请求单独计算。可按用户或账户覆盖。总部登录不受登录百分比影响。被拒绝的请求只返回所选文案,不发企微。通知开关只停止对应企微消息,不拦截下单、核销、出账、审核和改套餐。
void saveGlobals()}> 保存 } > ( setPercents((list) => list.map((item) => item.featureKey === row.featureKey ? { ...item, successPercent: typeof value === 'number' ? value : 100 } : item, ), ) } /> ), }, { title: '失败文案', render: (_, row) => (
(row.scopeType === 'user' ? '用户' : '账户'), }, { title: '对象', render: (_, row) => row.actorLabel || `${row.actorType} ${row.actorId}`, }, { title: '功能', dataIndex: 'featureLabel', width: 140 }, { title: '成功百分比', dataIndex: 'successPercent', width: 120 }, { title: '失败文案', width: 160, render: (_, row) => (row.errorKind ? API_ACCESS_ERROR_LABELS[row.errorKind] : '请求异常'), }, { title: '操作', width: 100, render: (_, row) => ( void removeOverride(row.id)}> ), }, ]} /> void saveNotifies()}> 保存 } >
( setNotifies((list) => list.map((item) => (item.featureKey === row.featureKey ? { ...item, enabled } : item)), ) } /> ), }, ]} /> setOpen(false)} onCreated={(data) => { setOverrides(data.overrides); setOpen(false); }} /> ); } function OverrideModal({ open, onClose, onCreated, }: { open: boolean; onClose: () => void; onCreated: (data: ApiAccessFormResponse) => void; }) { const [form] = Form.useForm(); const [scope, setScope] = useState<'user' | 'account'>('user'); const [hits, setHits] = useState([]); const [searching, setSearching] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; form.resetFields(); form.setFieldsValue({ scopeType: 'user', featureKey: '', successPercent: 100, errorKind: 'request_error', }); setScope('user'); setHits([]); }, [open, form]); async function onSearch(q: string) { const keyword = q.trim(); if (!keyword) { setHits([]); return; } setSearching(true); try { const rows = await request( `/admin/api-access/actors?scope=${scope}&q=${encodeURIComponent(keyword)}`, ); setHits(rows); } catch (error) { message.error(error instanceof Error ? error.message : '搜索失败'); } finally { setSearching(false); } } async function submit() { const values = await form.validateFields(); const hit = hits.find((item) => `${item.actorType}:${item.actorId}` === values.actor); if (!hit) { message.error('请选择对象'); return; } setSaving(true); try { const data = await request('/admin/api-access/overrides', { method: 'POST', body: JSON.stringify({ scopeType: scope, actorType: hit.actorType, actorId: hit.actorId, featureKey: values.featureKey || null, successPercent: values.successPercent, errorKind: values.errorKind, }), }); message.success('已添加'); onCreated(data); } catch (error) { message.error(error instanceof Error ? error.message : '保存失败'); } finally { setSaving(false); } } return ( void submit()} confirmLoading={saving} destroyOnClose >
void onSearch(q)} options={hits.map((hit) => ({ value: `${hit.actorType}:${hit.actorId}`, label: hit.label, }))} />
); }