@@ -1,10 +1,9 @@
import { useCallback , useEffect , useState } from 'react' ;
import { Check , Plus , RefreshCw , Search , Settings2 , ShieldOff , Trash2 } from 'lucide-react' ;
import { Plus , RefreshCw , Search , Settings2 , ShieldOff , Trash2 } from 'lucide-react' ;
import {
adminApi ,
type AdminChannel ,
type EnterpriseApplication ,
type LegacySignatureReportTask ,
type SignatureRetirementMessage ,
type SignatureRetirementRule ,
type SignatureRetirementRuleType ,
@@ -19,11 +18,6 @@ const carrierLabels = { mobile: '移动', unicom: '联通', telecom: '电信' };
const ruleTypeLabels : Record < SignatureRetirementRuleType , string > = {
enterprise_global : '企业全局规则' , enterprise_application : '企业应用特殊规则' , channel_global : '通道全局规则' , channel : '通道特殊规则' ,
} ;
const statusOptions = [
{ value : 'pending' , label : '待处理' } , { value : 'waiting_material' , label : '待材料' } , { value : 'reporting' , label : '报备中' } ,
{ value : 'approved' , label : '已通过' } , { value : 'failed' , label : '失败' } , { value : 'rejected' , label : '驳回' } , { value : 'abandoned' , label : '已放弃' } ,
] ;
type RuleDraft = {
ruleType : SignatureRetirementRuleType ; targetId : string ; enabled : boolean ;
mobileWindowDays : string ; mobileThreshold : string ; unicomWindowDays : string ; unicomThreshold : string ;
@@ -61,7 +55,6 @@ export function AdminSignatureRetirementPage() {
const [ webhooks , setWebhooks ] = useState < SignatureRetirementWebhook [ ] > ( [ ] ) ;
const [ messages , setMessages ] = useState < SignatureRetirementMessage [ ] > ( [ ] ) ;
const [ suppressions , setSuppressions ] = useState < SignatureRetirementSuppression [ ] > ( [ ] ) ;
const [ legacyTasks , setLegacyTasks ] = useState < LegacySignatureReportTask [ ] > ( [ ] ) ;
const [ applications , setApplications ] = useState < EnterpriseApplication [ ] > ( [ ] ) ;
const [ channels , setChannels ] = useState < AdminChannel [ ] > ( [ ] ) ;
const [ tenants , setTenants ] = useState < TenantOption [ ] > ( [ ] ) ;
@@ -75,23 +68,21 @@ export function AdminSignatureRetirementPage() {
const [ ruleDraft , setRuleDraft ] = useState < RuleDraft | null > ( null ) ;
const [ webhookOpen , setWebhookOpen ] = useState ( false ) ;
const [ webhookDraft , setWebhookDraft ] = useState ( { name : '' , platform : 'wecom' as 'wecom' | 'feishu' , url : '' } ) ;
const [ legacyDraft , setLegacyDraft ] = useState < LegacySignatureReportTask | null > ( null ) ;
const [ legacyResults , setLegacyResults ] = useState < Record < string , { status : string ; approvedAt : string } > > ( { } ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ error , setError ] = useState ( '' ) ;
const loadData = useCallback ( async ( targetPage = 1 , filters = defaultMessageFilters ( ) ) = > {
setLoading ( true ) ;
try {
const [ configuration , messageResult , activeSuppressions , history , applicationRows , channelRows , tenantRows ] = await Promise . all ( [
const [ configuration , messageResult , activeSuppressions , applicationRows , channelRows , tenantRows ] = await Promise . all ( [
adminApi . getSignatureRetirementConfiguration ( ) ,
adminApi . listSignatureRetirementMessages ( messageQuery ( targetPage , filters ) ) ,
adminApi . listSignatureRetirementSuppressions ( ) , adminApi . listLegacySignatureReportTasks ( ) ,
adminApi . listSignatureRetirementSuppressions ( ) ,
adminApi . listEnterpriseApplications ( ) , adminApi . listChannels ( ) , adminApi . listTenants ( ) ,
] ) ;
setRules ( configuration . rules ) ; setWebhooks ( configuration . webhooks . filter ( ( item ) = > item . status === 'active' ) ) ;
setMessages ( messageResult . items ) ; setMessageTotal ( messageResult . total ) ; setMessagePage ( messageResult . page ) ;
setSuppressions ( activeSuppressions ) ; setLegacyTasks ( history ) ;
setSuppressions ( activeSuppressions ) ;
setApplications ( applicationRows ) ; setChannels ( channelRows ) ; setTenants ( tenantRows ) ; setError ( '' ) ;
} catch ( failure ) {
setError ( failure instanceof Error ? failure . message : '签名清退预警数据加载失败' ) ;
@@ -133,13 +124,6 @@ export function AdminSignatureRetirementPage() {
{ key : 'reason' , title : '原因' , render : ( item ) = > item . reason || '-' } ,
{ key : 'actions' , title : '操作' , align : 'right' , render : ( item ) = > < Button icon = { < ShieldOff size = { 15 } / > } onClick = { ( ) = > { setActionError ( '' ) ; setCancelSuppressionDraft ( { id : item.id , reason : '' } ) ; } } size = "sm" variant = "ghost" > 取 消 抑 制 < / Button > } ,
] ;
const legacyColumns : Array < TableColumn < LegacySignatureReportTask > > = [
{ key : 'signature' , title : '签名' , render : ( item ) = > < > < strong > { item . signature ? . name } < / strong > < br / > < small > { item . signature ? . tenant ? . name } < / small > < / > } ,
{ key : 'channel' , title : '历史通道' , render : ( item ) = > < > { item . channel ? . name } < br / > < small > { supportedCarriers ( item ) . map ( ( carrier ) = > carrierLabels [ carrier ] ) . join ( '、' ) } < / small > < / > } ,
{ key : 'status' , title : '原状态' , render : ( item ) = > < Tag tone = { item . status === 'approved' ? 'success' : 'neutral' } > { statusLabel ( item . status ) } < / Tag > } ,
{ key : 'actions' , title : '操作' , align : 'right' , render : ( item ) = > < Button icon = { < Check size = { 15 } / > } onClick = { ( ) = > openLegacy ( item ) } size = "sm" > 按 运 营 商 确 认 < / Button > } ,
] ;
async function saveRule() {
if ( ! ruleDraft ) return ;
setLoading ( true ) ;
@@ -188,20 +172,6 @@ export function AdminSignatureRetirementPage() {
}
async function deleteWebhook ( id : string ) { if ( ! window . confirm ( '确认停用该Webhook? ' ) ) return ; await adminApi . deleteSignatureRetirementWebhook ( id ) ; await loadData ( messagePage , appliedMessageFilters ) ; }
async function readAll() { await adminApi . readAllSignatureRetirementMessagesToday ( ) ; await loadMessages ( messagePage , appliedMessageFilters ) ; window . dispatchEvent ( new Event ( 'cmpp-retirement-count-refresh' ) ) ; }
function openLegacy ( task : LegacySignatureReportTask ) {
setLegacyDraft ( task ) ;
// 历史通道级结果不能推导任何运营商状态;留空迫使操作人逐项依据供应商结果确认。
setLegacyResults ( Object . fromEntries ( supportedCarriers ( task ) . map ( ( carrier ) = > [ carrier , { status : '' , approvedAt : '' } ] ) ) ) ;
}
async function confirmLegacy() {
if ( ! legacyDraft ) return ;
await adminApi . confirmLegacySignatureReportTask ( legacyDraft . id , {
results : supportedCarriers ( legacyDraft ) . map ( ( carrier ) = > ( { carrier , status : legacyResults [ carrier ] ? . status ? ? 'pending' , approvedAt : legacyResults [ carrier ] ? . status === 'approved' && legacyResults [ carrier ] ? . approvedAt ? new Date ( legacyResults [ carrier ] . approvedAt ) . toISOString ( ) : undefined } ) ) ,
reason : '历史通道级报备按运营商人工确认' ,
} ) ;
setLegacyDraft ( null ) ; await loadData ( messagePage , appliedMessageFilters ) ;
}
const tenantOptions = tenants . map ( ( item ) = > ( { value : item.id , label : item.name } ) ) ;
const applicationOptions = applications
. filter ( ( item ) = > ! messageFilters . tenantId || item . tenantId === messageFilters . tenantId )
@@ -220,7 +190,6 @@ export function AdminSignatureRetirementPage() {
{ value : 'rules' , label : '检测规则' , content : < div className = "surface" > < div className = "section-heading" > < div > < h2 > 检 测 规 则 < / h2 > < p className = "muted" > 特 殊 规 则 优 先 于 全 局 规 则 ; 修 改 后 从 下 一 检 测 日 生 效 。 < / p > < / div > < Button icon = { < Plus size = { 16 } / > } onClick = { ( ) = > setRuleDraft ( { . . . emptyRule } ) } > 新 增 规 则 < / Button > < / div > < Table columns = { ruleColumns } data = { rules } emptyText = "暂无规则,未配置规则的维度不会进入检测" pagination = { false } rowKey = "id" / > < / div > } ,
{ value : 'webhooks' , label : 'Webhook' , content : < div className = "surface" > < div className = "section-heading" > < div > < h2 > 企 业 微 信 / 飞 书 通 知 < / h2 > < p className = "muted" > 地 址 加 密 保 存 , 发 送 失 败 自 动 退 避 重 试 。 < / p > < / div > < Button icon = { < Plus size = { 16 } / > } onClick = { ( ) = > setWebhookOpen ( true ) } > 新 增 Webhook < / Button > < / div > < div className = "settings-list" > { webhooks . map ( ( item ) = > < div className = "settings-list__item" key = { item . id } > < div > < strong > { item . name } < / strong > < p > { item . platform === 'wecom' ? '企业微信' : '飞书' } · { item . urlMasked } < / p > < / div > < Button icon = { < Trash2 size = { 15 } / > } onClick = { ( ) = > void deleteWebhook ( item . id ) } variant = "ghost" > 停 用 < / Button > < / div > ) } { ! webhooks . length ? < p className = "empty-state" > 暂 无 Webhook < / p > : null } < / div > < / div > } ,
{ value : 'suppressions' , label : ` 抑制管理( ${ suppressions . length } ) ` , content : < div className = "surface" > < Table columns = { suppressionColumns } data = { suppressions } emptyText = "暂无有效抑制" pagination = { false } rowKey = "id" / > < / div > } ,
{ value : 'legacy' , label : ` 历史待确认( ${ legacyTasks . length } ) ` , content : < div className = "surface" > < div className = "section-heading" > < div > < h2 > 历 史 运 营 商 待 确 认 < / h2 > < p className = "muted" > 不 自 动 把 三 网 通 道 解 释 为 三 个 运 营 商 均 已 通 过 ; 由 人 工 按 真 实 供 应 商 结 果 拆 分 。 < / p > < / div > < / div > < Table columns = { legacyColumns } data = { legacyTasks } emptyText = "历史通道级报备均已确认" pagination = { false } rowKey = "id" / > < / div > } ,
] ;
return < section className = "page-stack" >
@@ -234,7 +203,6 @@ export function AdminSignatureRetirementPage() {
< Modal dirty = { Boolean ( cancelSuppressionDraft ? . reason . trim ( ) ) } open = { Boolean ( cancelSuppressionDraft ) } title = "取消抑制" onClose = { ( ) = > { setCancelSuppressionDraft ( null ) ; setActionError ( '' ) ; } } footer = { < > < Button onClick = { ( ) = > { setCancelSuppressionDraft ( null ) ; setActionError ( '' ) ; } } variant = "ghost" > 取 消 < / Button > < Button disabled = { loading || ! cancelSuppressionDraft ? . reason . trim ( ) } onClick = { ( ) = > void confirmCancelSuppression ( ) } > 确 认 取 消 抑 制 < / Button > < / > } >
{ cancelSuppressionDraft ? < div className = "page-stack" > < p className = "muted" > 取 消 后 从 下 一 检 测 日 恢 复 预 警 , 不 补 发 抑 制 期 间 的 历 史 通 知 。 < / p > < Textarea label = "取消原因" maxLength = { 500 } onChange = { ( event ) = > setCancelSuppressionDraft ( ( value ) = > value ? { . . . value , reason : event.target.value } : value ) } placeholder = "请填写取消抑制原因" rows = { 4 } value = { cancelSuppressionDraft . reason } / > { actionError ? < p className = "form-error" > { actionError } < / p > : null } < / div > : null }
< / Modal >
< Modal open = { Boolean ( legacyDraft ) } title = "按运营商确认历史报备结果" onClose = { ( ) = > setLegacyDraft ( null ) } footer = { < > < Button onClick = { ( ) = > setLegacyDraft ( null ) } variant = "ghost" > 取 消 < / Button > < Button disabled = { ! legacyConfirmationReady ( legacyDraft , legacyResults ) } onClick = { ( ) = > void confirmLegacy ( ) } > 确 认 拆 分 < / Button > < / > } > < div className = "page-stack" > < p className = "muted" > 历 史 通 道 级 结 果 不 代 表 任 一 运 营 商 已 通 过 , 请 逐 项 依 据 供 应 商 真 实 结 果 确 认 ; 选 择 “ 已 通 过 ” 时 必 须 填 写 通 过 时 间 。 < / p > { legacyDraft ? supportedCarriers ( legacyDraft ) . map ( ( carrier ) = > < div className = "surface" key = { carrier } > < strong > { carrierLabels [ carrier ] } < / strong > < div className = "form-grid" > < Select label = "报备状态" onChange = { ( event ) = > setLegacyResults ( ( value ) = > ( { . . . value , [ carrier ] : { . . . value [ carrier ] , status : event.target.value , approvedAt : event.target.value === 'approved' ? value [ carrier ] ? . approvedAt ? ? '' : '' } } ) ) } options = { [ { value : '' , label : '请选择' } , . . . statusOptions ] } value = { legacyResults [ carrier ] ? . status ? ? '' } / > < Input disabled = { legacyResults [ carrier ] ? . status !== 'approved' } label = "通过时间" onChange = { ( event ) = > setLegacyResults ( ( value ) = > ( { . . . value , [ carrier ] : { . . . value [ carrier ] , approvedAt : event.target.value } } ) ) } type = "datetime-local" value = { legacyResults [ carrier ] ? . approvedAt ? ? '' } / > < / div > < / div > ) : null } < / div > < / Modal >
< / section > ;
}
@@ -247,15 +215,6 @@ function RuleModal({ applications, channels, draft, loading, onChange, onClose,
function ruleToDraft ( rule : SignatureRetirementRule ) : RuleDraft { return { ruleType : rule.ruleType , targetId : rule.targetId ? ? '' , enabled : rule.enabled , mobileWindowDays : String ( rule . mobileWindowDays ) , mobileThreshold : String ( rule . mobileThreshold ) , unicomWindowDays : String ( rule . unicomWindowDays ) , unicomThreshold : String ( rule . unicomThreshold ) , telecomWindowDays : String ( rule . telecomWindowDays ) , telecomThreshold : String ( rule . telecomThreshold ) , messageTemplate : rule.messageTemplate ? ? '' } ; }
function targetName ( rule : SignatureRetirementRule , applications : EnterpriseApplication [ ] , channels : AdminChannel [ ] ) { if ( ! rule . targetId ) return '全局默认' ; return rule . ruleType === 'channel' ? channels . find ( ( item ) = > item . id === rule . targetId ) ? . name ? ? rule.targetId : applications.find ( ( item ) = > item . id === rule . targetId ) ? . name ? ? rule . targetId ; }
function supportedCarriers ( task : LegacySignatureReportTask ) { const values = task . channel ? . carriers ? . length ? task.channel.carriers : task.channel?.carrier === 'all' ? [ . . . carriers ] : [ task . channel ? . carrier ] ; return values . filter ( ( item ) : item is typeof carriers [ number ] = > carriers . includes ( item as typeof carriers [ number ] ) ) ; }
function statusLabel ( status : string ) { return statusOptions . find ( ( item ) = > item . value === status ) ? . label ? ? status ; }
function legacyConfirmationReady ( task : LegacySignatureReportTask | null , results : Record < string , { status : string ; approvedAt : string } > ) {
return Boolean ( task && supportedCarriers ( task ) . every ( ( carrier ) = > {
const result = results [ carrier ] ;
return statusOptions . some ( ( option ) = > option . value === result ? . status ) && ( result . status !== 'approved' || Boolean ( result . approvedAt ) ) ;
} ) ) ;
}
function localDateTime ( value? : string | null ) { if ( ! value ) return '' ; const date = new Date ( value ) ; const offset = date . getTimezoneOffset ( ) * 60 _000 ; return new Date ( date . getTime ( ) - offset ) . toISOString ( ) . slice ( 0 , 16 ) ; }
function formatDate ( value? : string | null ) { return value ? new Date ( value ) . toLocaleDateString ( 'zh-CN' ) : '-' ; }
function formatDateTime ( value? : string | null ) { return value ? new Date ( value ) . toLocaleString ( 'zh-CN' , { hour12 : false } ) : '-' ; }
function errorMessage ( error : unknown , fallback : string ) { return error instanceof Error ? error.message : fallback ; }