feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Pencil, Plus, RefreshCw } from 'lucide-react';
|
||||
import { Pencil, Plus, RefreshCw, Search, Trash2, Unlock } from 'lucide-react';
|
||||
import {
|
||||
adminApi,
|
||||
type EnterpriseApplication,
|
||||
type PhoneFrequencyHit,
|
||||
type PhoneFrequencyWhitelistItem,
|
||||
type RiskRuleItem,
|
||||
} from '@/api/adminApi';
|
||||
import {
|
||||
@@ -10,18 +12,27 @@ import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
Textarea,
|
||||
type TableColumn,
|
||||
} from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
const definitions: Array<{ code: RiskRuleItem['code']; label: string; unit: string }> = [
|
||||
{ code: 'MAX_PHONES_PER_TASK', label: '单任务最大号码数', unit: '个号码' },
|
||||
{ code: 'NON_WORKING_MARKETING_BULK', label: '非工作时间大批量营销发送', unit: '个号码' },
|
||||
{ code: 'TASK_CREATE_FREQUENCY', label: '10分钟客户端任务创建频控', unit: '个任务' },
|
||||
{ code: 'PHONE_FREQUENCY_24H', label: '单号码24小时发送频次', unit: '条业务短信' },
|
||||
{ code: 'PHONE_FREQUENCY_5M', label: '单号码5分钟发送频次', unit: '条业务短信' },
|
||||
];
|
||||
|
||||
function isPhoneFrequencyRule(code: RiskRuleItem['code']) {
|
||||
return code === 'PHONE_FREQUENCY_24H' || code === 'PHONE_FREQUENCY_5M';
|
||||
}
|
||||
|
||||
type EditorState = {
|
||||
id?: string;
|
||||
applicationId: string;
|
||||
@@ -34,6 +45,14 @@ type EditorState = {
|
||||
endTime: string;
|
||||
};
|
||||
|
||||
type WhitelistEditorState = {
|
||||
id?: string;
|
||||
phoneNumber: string;
|
||||
reason: string;
|
||||
remark: string;
|
||||
status: 'active' | 'inactive';
|
||||
};
|
||||
|
||||
function editorFromRule(rule?: RiskRuleItem): EditorState {
|
||||
return {
|
||||
id: rule?.id,
|
||||
@@ -55,6 +74,23 @@ export function AdminRiskRulesPage() {
|
||||
const [editor, setEditor] = useState<EditorState | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [frequencyHits, setFrequencyHits] = useState<PhoneFrequencyHit[]>([]);
|
||||
const [hitPhone, setHitPhone] = useState('');
|
||||
const [hitStatus, setHitStatus] = useState<'active' | 'expired' | 'released' | ''>('active');
|
||||
const [hitPage, setHitPage] = useState(1);
|
||||
const [hitTotal, setHitTotal] = useState(0);
|
||||
const [releaseHit, setReleaseHit] = useState<PhoneFrequencyHit | null>(null);
|
||||
const [releaseReason, setReleaseReason] = useState('');
|
||||
const [releasing, setReleasing] = useState(false);
|
||||
const [whitelist, setWhitelist] = useState<PhoneFrequencyWhitelistItem[]>([]);
|
||||
const [whitelistPhone, setWhitelistPhone] = useState('');
|
||||
const [whitelistStatus, setWhitelistStatus] = useState<'active' | 'inactive' | 'deleted' | ''>('');
|
||||
const [whitelistPage, setWhitelistPage] = useState(1);
|
||||
const [whitelistTotal, setWhitelistTotal] = useState(0);
|
||||
const [whitelistEditor, setWhitelistEditor] = useState<WhitelistEditorState | null>(null);
|
||||
const [whitelistSaving, setWhitelistSaving] = useState(false);
|
||||
const [deletingWhitelist, setDeletingWhitelist] = useState<PhoneFrequencyWhitelistItem | null>(null);
|
||||
const [deleteWhitelistReason, setDeleteWhitelistReason] = useState('');
|
||||
|
||||
function load() {
|
||||
Promise.all([
|
||||
@@ -69,6 +105,42 @@ export function AdminRiskRulesPage() {
|
||||
|
||||
useEffect(load, [applicationId]);
|
||||
|
||||
function loadFrequencyHits(page = hitPage) {
|
||||
adminApi.listPhoneFrequencyHits({
|
||||
applicationId: applicationId || undefined,
|
||||
phoneNumber: hitPhone.trim() || undefined,
|
||||
status: hitStatus || undefined,
|
||||
page,
|
||||
pageSize: 20,
|
||||
}).then((result) => {
|
||||
setFrequencyHits(result.items);
|
||||
setHitTotal(result.total);
|
||||
setHitPage(result.page);
|
||||
}).catch((failure: Error) => setError(failure.message || '号码频次触发记录加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setHitPage(1);
|
||||
loadFrequencyHits(1);
|
||||
}, [applicationId]);
|
||||
|
||||
function loadWhitelist(page = whitelistPage) {
|
||||
adminApi.listPhoneFrequencyWhitelist({
|
||||
phoneNumber: whitelistPhone.trim() || undefined,
|
||||
status: whitelistStatus || undefined,
|
||||
page,
|
||||
pageSize: 20,
|
||||
}).then((result) => {
|
||||
setWhitelist(result.items);
|
||||
setWhitelistTotal(result.total);
|
||||
setWhitelistPage(result.page);
|
||||
}).catch((failure: Error) => setError(failure.message || '号码频控白名单加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadWhitelist(1);
|
||||
}, []);
|
||||
|
||||
const existingCodes = useMemo(
|
||||
() => new Set(rules.filter((rule) => rule.applicationId === editor?.applicationId).map((rule) => rule.code)),
|
||||
[editor?.applicationId, rules],
|
||||
@@ -87,9 +159,10 @@ export function AdminRiskRulesPage() {
|
||||
}
|
||||
setSaving(true);
|
||||
setError('');
|
||||
const phoneFrequencyRule = isPhoneFrequencyRule(editor.code);
|
||||
const body = {
|
||||
thresholdValue,
|
||||
action: editor.action,
|
||||
action: phoneFrequencyRule ? 'block' as const : editor.action,
|
||||
status: editor.status,
|
||||
priority: Number(editor.priority) || 100,
|
||||
config: editor.code === 'NON_WORKING_MARKETING_BULK'
|
||||
@@ -115,6 +188,78 @@ export function AdminRiskRulesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRelease() {
|
||||
if (!releaseHit) return;
|
||||
if (!releaseReason.trim()) {
|
||||
setError('解除并清零时必须填写原因');
|
||||
return;
|
||||
}
|
||||
setReleasing(true);
|
||||
try {
|
||||
await adminApi.releasePhoneFrequencyHit(releaseHit.id, releaseReason.trim());
|
||||
setReleaseHit(null);
|
||||
setReleaseReason('');
|
||||
loadFrequencyHits();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '解除号码频控失败');
|
||||
} finally {
|
||||
setReleasing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWhitelist() {
|
||||
if (!whitelistEditor) return;
|
||||
if (!/^(\+?86)?1\d{10}$/.test(whitelistEditor.phoneNumber.replace(/[\s-]/g, ''))) {
|
||||
setError('请输入有效的中国大陆11位手机号码');
|
||||
return;
|
||||
}
|
||||
if (!whitelistEditor.reason.trim()) {
|
||||
setError('白名单用途说明不能为空');
|
||||
return;
|
||||
}
|
||||
setWhitelistSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const body = {
|
||||
phoneNumber: whitelistEditor.phoneNumber.trim(),
|
||||
reason: whitelistEditor.reason.trim(),
|
||||
remark: whitelistEditor.remark.trim(),
|
||||
status: whitelistEditor.status,
|
||||
};
|
||||
if (whitelistEditor.id) {
|
||||
await adminApi.updatePhoneFrequencyWhitelist(whitelistEditor.id, body);
|
||||
} else {
|
||||
await adminApi.createPhoneFrequencyWhitelist(body);
|
||||
}
|
||||
setWhitelistEditor(null);
|
||||
loadWhitelist(1);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '号码频控白名单保存失败');
|
||||
} finally {
|
||||
setWhitelistSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteWhitelist() {
|
||||
if (!deletingWhitelist) return;
|
||||
if (!deleteWhitelistReason.trim()) {
|
||||
setError('删除白名单时必须填写原因');
|
||||
return;
|
||||
}
|
||||
setWhitelistSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
await adminApi.deletePhoneFrequencyWhitelist(deletingWhitelist.id, deleteWhitelistReason.trim());
|
||||
setDeletingWhitelist(null);
|
||||
setDeleteWhitelistReason('');
|
||||
loadWhitelist(1);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '号码频控白名单删除失败');
|
||||
} finally {
|
||||
setWhitelistSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<RiskRuleItem>> = [
|
||||
{ key: 'name', title: '规则名称', render: (rule) => <div><strong>{rule.name}</strong><small className="table-subline">{rule.description}</small></div> },
|
||||
{ key: 'scope', title: '适用范围', render: (rule) => rule.application ? <div><strong>{rule.application.name}</strong><small className="table-subline">{rule.application.tenant?.name ?? '-'}</small></div> : <Tag tone="info">全局默认</Tag> },
|
||||
@@ -131,6 +276,45 @@ export function AdminRiskRulesPage() {
|
||||
{ key: 'actions', title: '操作', width: '100px', align: 'right', render: (rule) => <Button icon={<Pencil size={15} />} onClick={() => setEditor(editorFromRule(rule))} size="sm" variant="ghost">编辑</Button> },
|
||||
];
|
||||
|
||||
const hitColumns: Array<TableColumn<PhoneFrequencyHit>> = [
|
||||
{ key: 'phone', title: '号码', width: '140px', render: (hit) => <strong>{hit.phoneNumber}</strong> },
|
||||
{ key: 'scope', title: '企业 / 应用', render: (hit) => <div><strong>{hit.tenant.name}</strong><small className="table-subline">{hit.application.name}</small></div> },
|
||||
{ key: 'rule', title: '命中规则', render: (hit) => <div><strong>{hit.ruleName}</strong><small className="table-subline">阈值 {hit.thresholdValue} 条,触发值 {hit.actualValue} 条</small></div> },
|
||||
{ key: 'window', title: '计数周期', width: '250px', render: (hit) => `${formatDateTime(hit.windowStartedAt)} 至 ${formatDateTime(hit.windowEndsAt)}` },
|
||||
{ key: 'status', title: '状态', width: '100px', render: (hit) => hit.releasedAt
|
||||
? <Tag tone="neutral">已解除</Tag>
|
||||
: new Date(hit.windowEndsAt).getTime() <= Date.now()
|
||||
? <Tag tone="warning">已到期</Tag>
|
||||
: <Tag tone="danger">拦截中</Tag> },
|
||||
{ key: 'createdAt', title: '触发时间', width: '170px', render: (hit) => formatDateTime(hit.createdAt) },
|
||||
{ key: 'actions', title: '操作', width: '110px', align: 'right', render: (hit) => hit.releasedAt
|
||||
? <span className="muted">已清零</span>
|
||||
: <Button icon={<Unlock size={15} />} onClick={() => { setReleaseHit(hit); setReleaseReason(''); }} size="sm" variant="ghost">解除</Button> },
|
||||
];
|
||||
|
||||
const whitelistColumns: Array<TableColumn<PhoneFrequencyWhitelistItem>> = [
|
||||
{ key: 'phone', title: '手机号码', width: '145px', render: (item) => <strong>{item.phoneNumber}</strong> },
|
||||
{ key: 'reason', title: '用途说明', render: (item) => <div><strong>{item.reason}</strong>{item.remark ? <small className="table-subline">{item.remark}</small> : null}</div> },
|
||||
{ key: 'status', title: '状态', width: '90px', render: (item) => <Tag tone={item.status === 'active' ? 'success' : item.status === 'deleted' ? 'danger' : 'neutral'}>{item.status === 'active' ? '启用' : item.status === 'deleted' ? '已删除' : '停用'}</Tag> },
|
||||
{ key: 'operator', title: '最后操作人', width: '150px', render: (item) => item.updatedBy.displayName || item.updatedBy.username },
|
||||
{ key: 'updatedAt', title: '更新时间', width: '170px', render: (item) => formatDateTime(item.updatedAt) },
|
||||
{ key: 'actions', title: '操作', width: '175px', align: 'right', render: (item) => item.status === 'deleted'
|
||||
? <span className="muted">历史记录</span>
|
||||
: <div className="table-actions">
|
||||
<Button icon={<Pencil size={15} />} onClick={() => setWhitelistEditor({
|
||||
id: item.id,
|
||||
phoneNumber: item.phoneNumber,
|
||||
reason: item.reason,
|
||||
remark: item.remark ?? '',
|
||||
status: item.status === 'active' ? 'active' : 'inactive',
|
||||
})} size="sm" variant="ghost">编辑</Button>
|
||||
<Button icon={<Trash2 size={15} />} onClick={() => { setDeletingWhitelist(item); setDeleteWhitelistReason(''); }} size="sm" variant="ghost">删除</Button>
|
||||
</div> },
|
||||
];
|
||||
|
||||
const hitTotalPages = Math.max(1, Math.ceil(hitTotal / 20));
|
||||
const whitelistTotalPages = Math.max(1, Math.ceil(whitelistTotal / 20));
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
@@ -156,6 +340,77 @@ export function AdminRiskRulesPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="surface"><Table columns={columns} data={rules} emptyText="暂无风控规则" rowKey="id" /></div>
|
||||
<div className="surface">
|
||||
<div className="section-heading">
|
||||
<div><h2>平台级号码频控白名单</h2><p>启用后,该号码在全平台所有企业应用下均不受24小时和5分钟号码频次限制;其他风控规则仍正常执行。</p></div>
|
||||
<div className="page-heading__actions">
|
||||
<Tag tone="info">{whitelistTotal} 条</Tag>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setWhitelistEditor({ phoneNumber: '', reason: '', remark: '', status: 'active' })}>新增白名单</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sms-audit-filter">
|
||||
<Input label="手机号码" onChange={(event) => setWhitelistPhone(event.target.value)} placeholder="输入完整或部分号码" value={whitelistPhone} />
|
||||
<Select
|
||||
label="白名单状态"
|
||||
onChange={(event) => setWhitelistStatus(event.target.value as typeof whitelistStatus)}
|
||||
options={[
|
||||
{ label: '全部有效记录', value: '' },
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '停用', value: 'inactive' },
|
||||
{ label: '已删除', value: 'deleted' },
|
||||
]}
|
||||
value={whitelistStatus}
|
||||
/>
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => loadWhitelist(1)}>查询</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table columns={whitelistColumns} data={whitelist} emptyText="暂无号码频控白名单" pagination={false} rowKey="id" />
|
||||
<Pagination
|
||||
nextDisabled={whitelistPage >= whitelistTotalPages}
|
||||
onNext={() => loadWhitelist(Math.min(whitelistTotalPages, whitelistPage + 1))}
|
||||
onPageChange={(page) => loadWhitelist(page)}
|
||||
onPrevious={() => loadWhitelist(Math.max(1, whitelistPage - 1))}
|
||||
page={whitelistPage}
|
||||
previousDisabled={whitelistPage <= 1}
|
||||
total={whitelistTotal}
|
||||
totalPages={whitelistTotalPages}
|
||||
/>
|
||||
</div>
|
||||
<div className="surface">
|
||||
<div className="section-heading">
|
||||
<div><h2>号码频次触发记录</h2><p>按企业应用和号码隔离计数;周期到期自动重新计数,人工解除会立即清零当前周期并保留审计记录。</p></div>
|
||||
<Tag tone="warning">{hitTotal} 条</Tag>
|
||||
</div>
|
||||
<div className="sms-audit-filter">
|
||||
<Input label="手机号码" onChange={(event) => setHitPhone(event.target.value)} placeholder="输入完整或部分号码" value={hitPhone} />
|
||||
<Select
|
||||
label="记录状态"
|
||||
onChange={(event) => setHitStatus(event.target.value as typeof hitStatus)}
|
||||
options={[
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '拦截中', value: 'active' },
|
||||
{ label: '周期已到期', value: 'expired' },
|
||||
{ label: '已人工解除', value: 'released' },
|
||||
]}
|
||||
value={hitStatus}
|
||||
/>
|
||||
<div className="admin-task-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => { setHitPage(1); loadFrequencyHits(1); }}>查询</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table columns={hitColumns} data={frequencyHits} emptyText="暂无号码频次触发记录" pagination={false} rowKey="id" />
|
||||
<Pagination
|
||||
nextDisabled={hitPage >= hitTotalPages}
|
||||
onNext={() => loadFrequencyHits(Math.min(hitTotalPages, hitPage + 1))}
|
||||
onPageChange={(page) => loadFrequencyHits(page)}
|
||||
onPrevious={() => loadFrequencyHits(Math.max(1, hitPage - 1))}
|
||||
page={hitPage}
|
||||
previousDisabled={hitPage <= 1}
|
||||
total={hitTotal}
|
||||
totalPages={hitTotalPages}
|
||||
/>
|
||||
</div>
|
||||
{editor ? <Modal
|
||||
footer={<><Button disabled={saving} onClick={() => setEditor(null)} variant="ghost">取消</Button><Button disabled={saving} onClick={() => void save()}>{saving ? '保存中…' : '保存'}</Button></>}
|
||||
onClose={() => setEditor(null)}
|
||||
@@ -180,8 +435,8 @@ export function AdminRiskRulesPage() {
|
||||
options={definitions.filter((item) => !existingCodes.has(item.code) || item.code === editor.code).map((item) => ({ label: item.label, value: item.code }))}
|
||||
value={editor.code}
|
||||
/> : <Input disabled label="规则" value={definitions.find((item) => item.code === editor.code)?.label ?? editor.code} />}
|
||||
<Input label={`阈值(${definitions.find((item) => item.code === editor.code)?.unit ?? ''})`} min="0" onChange={(event) => setEditor({ ...editor, thresholdValue: event.target.value })} type="number" value={editor.thresholdValue} />
|
||||
<Select label="处理动作" onChange={(event) => setEditor({ ...editor, action: event.target.value as RiskRuleItem['action'] })} options={[{ label: '直接拒绝', value: 'block' }, { label: '进入人工审核', value: 'manual_review' }]} value={editor.action} />
|
||||
<Input label={`阈值(${definitions.find((item) => item.code === editor.code)?.unit ?? ''})`} min={isPhoneFrequencyRule(editor.code) ? '1' : '0'} onChange={(event) => setEditor({ ...editor, thresholdValue: event.target.value })} type="number" value={editor.thresholdValue} />
|
||||
<Select disabled={isPhoneFrequencyRule(editor.code)} label="处理动作" onChange={(event) => setEditor({ ...editor, action: event.target.value as RiskRuleItem['action'] })} options={isPhoneFrequencyRule(editor.code) ? [{ label: '直接拒绝(首版固定)', value: 'block' }] : [{ label: '直接拒绝', value: 'block' }, { label: '进入人工审核', value: 'manual_review' }]} value={isPhoneFrequencyRule(editor.code) ? 'block' : editor.action} />
|
||||
<Select label="状态" onChange={(event) => setEditor({ ...editor, status: event.target.value as RiskRuleItem['status'] })} options={[{ label: '启用', value: 'active' }, { label: '停用', value: 'inactive' }]} value={editor.status} />
|
||||
<Input label="优先级" min="1" onChange={(event) => setEditor({ ...editor, priority: event.target.value })} type="number" value={editor.priority} />
|
||||
{editor.code === 'NON_WORKING_MARKETING_BULK' ? <>
|
||||
@@ -190,6 +445,38 @@ export function AdminRiskRulesPage() {
|
||||
</> : null}
|
||||
</div>
|
||||
</Modal> : null}
|
||||
{whitelistEditor ? <Modal
|
||||
footer={<><Button disabled={whitelistSaving} onClick={() => setWhitelistEditor(null)} variant="ghost">取消</Button><Button disabled={whitelistSaving} onClick={() => void saveWhitelist()}>{whitelistSaving ? '保存中…' : '保存'}</Button></>}
|
||||
onClose={() => setWhitelistEditor(null)}
|
||||
open
|
||||
title={whitelistEditor.id ? '编辑号码频控白名单' : '新增号码频控白名单'}
|
||||
>
|
||||
<div className="form-grid">
|
||||
<Input label="手机号码" onChange={(event) => setWhitelistEditor({ ...whitelistEditor, phoneNumber: event.target.value })} placeholder="中国大陆11位手机号码" value={whitelistEditor.phoneNumber} />
|
||||
<Select label="状态" onChange={(event) => setWhitelistEditor({ ...whitelistEditor, status: event.target.value as WhitelistEditorState['status'] })} options={[{ label: '启用', value: 'active' }, { label: '停用', value: 'inactive' }]} value={whitelistEditor.status} />
|
||||
</div>
|
||||
<Textarea label="用途说明" maxLength={200} onChange={(event) => setWhitelistEditor({ ...whitelistEditor, reason: event.target.value })} placeholder="必填,说明该号码为何需要豁免频控" rows={3} value={whitelistEditor.reason} />
|
||||
<Textarea label="备注" maxLength={500} onChange={(event) => setWhitelistEditor({ ...whitelistEditor, remark: event.target.value })} placeholder="选填" rows={3} value={whitelistEditor.remark} />
|
||||
<p className="muted">新增启用、启停切换或修改号码时,会清零相关号码在所有企业应用下的当前频控计数,并解除尚未到期的频控命中。</p>
|
||||
</Modal> : null}
|
||||
{deletingWhitelist ? <Modal
|
||||
footer={<><Button disabled={whitelistSaving} onClick={() => setDeletingWhitelist(null)} variant="ghost">取消</Button><Button disabled={whitelistSaving} onClick={() => void confirmDeleteWhitelist()}>{whitelistSaving ? '处理中…' : '删除并清零'}</Button></>}
|
||||
onClose={() => setDeletingWhitelist(null)}
|
||||
open
|
||||
title="删除号码频控白名单"
|
||||
>
|
||||
<p>删除号码 <strong>{deletingWhitelist.phoneNumber}</strong> 的平台级频控豁免,并清零该号码在所有企业应用下的当前频控计数。历史记录和审计日志会保留。</p>
|
||||
<Textarea label="删除原因" maxLength={500} onChange={(event) => setDeleteWhitelistReason(event.target.value)} placeholder="请填写删除原因" rows={4} value={deleteWhitelistReason} />
|
||||
</Modal> : null}
|
||||
{releaseHit ? <Modal
|
||||
footer={<><Button disabled={releasing} onClick={() => setReleaseHit(null)} variant="ghost">取消</Button><Button disabled={releasing} onClick={() => void confirmRelease()}>{releasing ? '处理中…' : '解除并清零'}</Button></>}
|
||||
onClose={() => setReleaseHit(null)}
|
||||
open
|
||||
title="解除号码频控"
|
||||
>
|
||||
<p>将解除号码 <strong>{releaseHit.phoneNumber}</strong> 在应用“{releaseHit.application.name}”下的当前拦截,并将该规则当前周期计数清零。历史触发记录仍会保留。</p>
|
||||
<Textarea label="解除原因" onChange={(event) => setReleaseReason(event.target.value)} placeholder="请填写人工解除原因" rows={4} value={releaseReason} />
|
||||
</Modal> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user